├── my-app
├── app
│ ├── favicon.ico
│ ├── page.tsx
│ ├── api
│ │ └── chat
│ │ │ └── route.ts
│ ├── layout.tsx
│ ├── assistant.tsx
│ └── globals.css
├── postcss.config.mjs
├── next.config.ts
├── lib
│ └── utils.ts
├── next-env.d.ts
├── components
│ ├── ui
│ │ ├── skeleton.tsx
│ │ ├── separator.tsx
│ │ ├── input.tsx
│ │ ├── tooltip.tsx
│ │ ├── button.tsx
│ │ ├── breadcrumb.tsx
│ │ ├── sheet.tsx
│ │ └── sidebar.tsx
│ ├── assistant-ui
│ │ ├── tooltip-icon-button.tsx
│ │ ├── tool-fallback.tsx
│ │ ├── thread-list.tsx
│ │ ├── markdown-text.tsx
│ │ └── thread.tsx
│ └── app-sidebar.tsx
├── eslint.config.mjs
├── components.json
├── README.md
├── hooks
│ └── use-mobile.ts
├── tsconfig.json
└── package.json
└── README.md
/my-app/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gaurav-creater317/Prompt-Mate/HEAD/my-app/app/favicon.ico
--------------------------------------------------------------------------------
/my-app/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | const config = {
2 | plugins: ["@tailwindcss/postcss"],
3 | };
4 |
5 | export default config;
6 |
--------------------------------------------------------------------------------
/my-app/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { Assistant } from "./assistant";
2 |
3 | export default function Home() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/my-app/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from "next";
2 |
3 | const nextConfig: NextConfig = {
4 | /* config options here */
5 | };
6 |
7 | export default nextConfig;
8 |
--------------------------------------------------------------------------------
/my-app/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/my-app/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
6 |
--------------------------------------------------------------------------------
/my-app/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
4 | return (
5 |
10 | )
11 | }
12 |
13 | export { Skeleton }
14 |
--------------------------------------------------------------------------------
/my-app/app/api/chat/route.ts:
--------------------------------------------------------------------------------
1 | import { google } from "@ai-sdk/google";
2 | import { streamText } from "ai";
3 |
4 | export const maxDuration = 30;
5 |
6 | export async function POST(req: Request) {
7 | const { messages } = await req.json();
8 | const result = streamText({
9 | model: google("gemini-2.0-flash"),
10 | messages,
11 | });
12 | return result.toDataStreamResponse();
13 | }
--------------------------------------------------------------------------------
/my-app/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { dirname } from "path";
2 | import { fileURLToPath } from "url";
3 | import { FlatCompat } from "@eslint/eslintrc";
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = dirname(__filename);
7 |
8 | const compat = new FlatCompat({
9 | baseDirectory: __dirname,
10 | });
11 |
12 | const eslintConfig = [
13 | ...compat.extends("next/core-web-vitals", "next/typescript"),
14 | ];
15 |
16 | export default eslintConfig;
17 |
--------------------------------------------------------------------------------
/my-app/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "",
8 | "css": "app/globals.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/my-app/README.md:
--------------------------------------------------------------------------------
1 | This is the [assistant-ui](https://github.com/Yonom/assistant-ui) starter project.
2 |
3 | ## Getting Started
4 |
5 | First, add your OpenAI API key to `.env.local` file:
6 |
7 | ```
8 | OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
9 | ```
10 |
11 | Then, run the development server:
12 |
13 | ```bash
14 | npm run dev
15 | # or
16 | yarn dev
17 | # or
18 | pnpm dev
19 | # or
20 | bun dev
21 | ```
22 |
23 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
24 |
25 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
26 |
--------------------------------------------------------------------------------
/my-app/hooks/use-mobile.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | const MOBILE_BREAKPOINT = 768
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(undefined)
7 |
8 | React.useEffect(() => {
9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10 | const onChange = () => {
11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12 | }
13 | mql.addEventListener("change", onChange)
14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15 | return () => mql.removeEventListener("change", onChange)
16 | }, [])
17 |
18 | return !!isMobile
19 | }
20 |
--------------------------------------------------------------------------------
/my-app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/my-app/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function Separator({
9 | className,
10 | orientation = "horizontal",
11 | decorative = true,
12 | ...props
13 | }: React.ComponentProps) {
14 | return (
15 |
25 | )
26 | }
27 |
28 | export { Separator }
29 |
--------------------------------------------------------------------------------
/my-app/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Geist, Geist_Mono } from "next/font/google";
3 | import "./globals.css";
4 |
5 | const geistSans = Geist({
6 | variable: "--font-geist-sans",
7 | subsets: ["latin"],
8 | });
9 |
10 | const geistMono = Geist_Mono({
11 | variable: "--font-geist-mono",
12 | subsets: ["latin"],
13 | });
14 |
15 | export const metadata: Metadata = {
16 | title: "assistant-ui Starter App",
17 | description: "Generated by create-assistant-ui",
18 | };
19 |
20 | export default function RootLayout({
21 | children,
22 | }: Readonly<{
23 | children: React.ReactNode;
24 | }>) {
25 | return (
26 |
27 |
30 | {children}
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/my-app/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6 | return (
7 |
18 | )
19 | }
20 |
21 | export { Input }
22 |
--------------------------------------------------------------------------------
/my-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "assistant-ui-starter",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev --turbopack",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@ai-sdk/google": "^1.2.22",
13 | "@ai-sdk/openai": "^1.3.22",
14 | "@assistant-ui/react": "^0.10.9",
15 | "@assistant-ui/react-ai-sdk": "^0.10.14",
16 | "@assistant-ui/react-markdown": "^0.10.3",
17 | "@radix-ui/react-dialog": "^1.1.14",
18 | "@radix-ui/react-separator": "^1.1.7",
19 | "@radix-ui/react-slot": "^1.2.3",
20 | "@radix-ui/react-tooltip": "^1.2.7",
21 | "ai": "^4.3.16",
22 | "class-variance-authority": "^0.7.1",
23 | "clsx": "^2.1.1",
24 | "lucide-react": "^0.511.0",
25 | "next": "15.3.2",
26 | "react": "^19.1.0",
27 | "react-dom": "^19.1.0",
28 | "remark-gfm": "^4.0.1",
29 | "tailwind-merge": "^3.3.0",
30 | "tw-animate-css": "^1.3.0"
31 | },
32 | "devDependencies": {
33 | "@eslint/eslintrc": "^3",
34 | "@tailwindcss/postcss": "^4",
35 | "@types/node": "^22",
36 | "@types/react": "^19",
37 | "@types/react-dom": "^19",
38 | "eslint": "^9",
39 | "eslint-config-next": "15.3.2",
40 | "tailwindcss": "^4",
41 | "typescript": "^5"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/my-app/components/assistant-ui/tooltip-icon-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ComponentPropsWithoutRef, forwardRef } from "react";
4 |
5 | import {
6 | Tooltip,
7 | TooltipContent,
8 | TooltipProvider,
9 | TooltipTrigger,
10 | } from "@/components/ui/tooltip";
11 | import { Button } from "@/components/ui/button";
12 | import { cn } from "@/lib/utils";
13 |
14 | export type TooltipIconButtonProps = ComponentPropsWithoutRef & {
15 | tooltip: string;
16 | side?: "top" | "bottom" | "left" | "right";
17 | };
18 |
19 | export const TooltipIconButton = forwardRef<
20 | HTMLButtonElement,
21 | TooltipIconButtonProps
22 | >(({ children, tooltip, side = "bottom", className, ...rest }, ref) => {
23 | return (
24 |
25 |
26 |
27 |
34 | {children}
35 | {tooltip}
36 |
37 |
38 | {tooltip}
39 |
40 |
41 | );
42 | });
43 |
44 | TooltipIconButton.displayName = "TooltipIconButton";
45 |
--------------------------------------------------------------------------------
/my-app/components/assistant-ui/tool-fallback.tsx:
--------------------------------------------------------------------------------
1 | import { ToolCallContentPartComponent } from "@assistant-ui/react";
2 | import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
3 | import { useState } from "react";
4 | import { Button } from "../ui/button";
5 |
6 | export const ToolFallback: ToolCallContentPartComponent = ({
7 | toolName,
8 | argsText,
9 | result,
10 | }) => {
11 | const [isCollapsed, setIsCollapsed] = useState(true);
12 | return (
13 |
14 |
15 |
16 |
17 | Used tool: {toolName}
18 |
19 |
20 |
setIsCollapsed(!isCollapsed)}>
21 | {isCollapsed ? : }
22 |
23 |
24 | {!isCollapsed && (
25 |
26 |
29 | {result !== undefined && (
30 |
31 |
Result:
32 |
33 | {typeof result === "string"
34 | ? result
35 | : JSON.stringify(result, null, 2)}
36 |
37 |
38 | )}
39 |
40 | )}
41 |
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/my-app/app/assistant.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { AssistantRuntimeProvider } from "@assistant-ui/react";
4 | import { useChatRuntime } from "@assistant-ui/react-ai-sdk";
5 | import { Thread } from "@/components/assistant-ui/thread";
6 | import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
7 | import { AppSidebar } from "@/components/app-sidebar";
8 | import { Separator } from "@/components/ui/separator";
9 | import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "@/components/ui/breadcrumb";
10 |
11 | export const Assistant = () => {
12 | const runtime = useChatRuntime({
13 | api: "/api/chat",
14 | });
15 |
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | Build Your Own ChatGPT UX
29 |
30 |
31 |
32 |
33 |
34 | Starter Template
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | );
45 | };
46 |
--------------------------------------------------------------------------------
/my-app/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | function TooltipProvider({
9 | delayDuration = 0,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
18 | );
19 | }
20 |
21 | function Tooltip({
22 | ...props
23 | }: React.ComponentProps) {
24 | return (
25 |
26 |
27 |
28 | );
29 | }
30 |
31 | function TooltipTrigger({
32 | ...props
33 | }: React.ComponentProps) {
34 | return ;
35 | }
36 |
37 | function TooltipContent({
38 | className,
39 | sideOffset = 0,
40 | children,
41 | ...props
42 | }: React.ComponentProps) {
43 | return (
44 |
45 |
54 | {children}
55 |
56 |
57 |
58 | );
59 | }
60 |
61 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
62 |
--------------------------------------------------------------------------------
/my-app/components/assistant-ui/thread-list.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from "react";
2 | import {
3 | ThreadListItemPrimitive,
4 | ThreadListPrimitive,
5 | } from "@assistant-ui/react";
6 | import { ArchiveIcon, PlusIcon } from "lucide-react";
7 |
8 | import { Button } from "@/components/ui/button";
9 | import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
10 |
11 | export const ThreadList: FC = () => {
12 | return (
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | const ThreadListNew: FC = () => {
21 | return (
22 |
23 |
24 |
25 | New Thread
26 |
27 |
28 | );
29 | };
30 |
31 | const ThreadListItems: FC = () => {
32 | return ;
33 | };
34 |
35 | const ThreadListItem: FC = () => {
36 | return (
37 |
38 |
39 |
40 |
41 |
42 |
43 | );
44 | };
45 |
46 | const ThreadListItemTitle: FC = () => {
47 | return (
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | const ThreadListItemArchive: FC = () => {
55 | return (
56 |
57 |
62 |
63 |
64 |
65 | );
66 | };
67 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Prompt Mate - Your Personal AI Conversation Campaign
2 |
3 | # Education Track: Build Apps with Google AI Studio
4 |
5 | # 🚀 Prompt Mate – Your Personal AI Conversation Companion using Next.js & Gemini API 💬
6 |
7 | Hey DEV community! 👋
8 |
9 | I'm thrilled to introduce my latest project – Prompt Mate – an AI-powered chat interface built using Next.js and Google Cloud’s Gemini API via Google AI Studio.
10 |
11 | # 💡 What is Prompt Mate?
12 | Prompt Mate is a Next.js web application that enables users to:
13 |
14 | 🧠 Chat with an AI assistant powered by Google’s Gemini model.
15 | ✍️ Send custom prompts and get intelligent, real-time responses.
16 | 📱 Experience a responsive and clean UI across devices.
17 | 🔐 Interact securely with Google Cloud-hosted functions.
18 | Whether you're a developer, student, or AI enthusiast, Prompt Mate is designed to demonstrate the power of LLMs in a full-stack web application.
19 |
20 | # 🛠️ Tech Stack
21 | ⚛️ Next.js – Full-stack React framework.
22 | ☁️ Google Cloud Gemini API – Used via API key.
23 | 🧪 Google AI Studio – For prompt testing and optimization .
24 | 🌐 Tailwind CSS – For styling the UI.
25 | 🔐 .env.local – Securing API keys in environment variables.
26 | 🔁 API Routes – To safely call Gemini from the server side.
27 | 🔧 Key Features
28 | ✅ Fully responsive chat interface.
29 | 🧠 Gemini AI-generated responses.
30 | 🔒 API key protected backend route in Next.js.
31 | 🌙 Dark mode ready (optional toggle).
32 | 🧼 Clean code structure and modular components.
33 | 📸
34 |
35 |
36 | # 🔗 Live Demo & Code
37 | 🔍 Live Preview: Visit Prompt Mate
38 |
39 |
40 | # 🧠 What I Learned
41 | Seamlessly integrating Gemini API with Next.js serverless routes
42 | Protecting API keys using .env.local and getServerSideProps
43 | Building clean and intuitive UIs using Tailwind CSS
44 | Testing and refining prompts with Google AI Studio
45 |
46 | # 🎯 Final Thoughts
47 | Working on Prompt Mate gave me a practical dive into generative AI development and deploying it on a modern full-stack framework.
48 |
49 | It’s amazing how quickly you can build something powerful with the right APIs and tools.
50 |
51 | I’d love your feedback and thoughts! 🌟
52 |
53 | Feel free to fork, remix, or collaborate with me on future versions.
54 | # ✨ Also feel free to star this repository if you like it! ✨
55 |
--------------------------------------------------------------------------------
/my-app/components/app-sidebar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Github, MessagesSquare } from "lucide-react"
3 | import Link from "next/link"
4 | import {
5 | Sidebar,
6 | SidebarContent,
7 | SidebarFooter,
8 | SidebarHeader,
9 | SidebarMenu,
10 | SidebarMenuButton,
11 | SidebarMenuItem,
12 | SidebarRail,
13 | } from "@/components/ui/sidebar"
14 | import { ThreadList } from "./assistant-ui/thread-list"
15 |
16 | export function AppSidebar({ ...props }: React.ComponentProps) {
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | assistant-ui
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | GitHub
51 | View Source
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | )
61 | }
62 |
--------------------------------------------------------------------------------
/my-app/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
16 | outline:
17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20 | ghost:
21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
22 | link: "text-primary underline-offset-4 hover:underline",
23 | },
24 | size: {
25 | default: "h-9 px-4 py-2 has-[>svg]:px-3",
26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
28 | icon: "size-9",
29 | },
30 | },
31 | defaultVariants: {
32 | variant: "default",
33 | size: "default",
34 | },
35 | }
36 | );
37 |
38 | function Button({
39 | className,
40 | variant,
41 | size,
42 | asChild = false,
43 | ...props
44 | }: React.ComponentProps<"button"> &
45 | VariantProps & {
46 | asChild?: boolean;
47 | }) {
48 | const Comp = asChild ? Slot : "button";
49 |
50 | return (
51 |
56 | );
57 | }
58 |
59 | export { Button, buttonVariants };
60 |
--------------------------------------------------------------------------------
/my-app/components/ui/breadcrumb.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { ChevronRight, MoreHorizontal } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
8 | return
9 | }
10 |
11 | function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
12 | return (
13 |
21 | )
22 | }
23 |
24 | function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
25 | return (
26 |
31 | )
32 | }
33 |
34 | function BreadcrumbLink({
35 | asChild,
36 | className,
37 | ...props
38 | }: React.ComponentProps<"a"> & {
39 | asChild?: boolean
40 | }) {
41 | const Comp = asChild ? Slot : "a"
42 |
43 | return (
44 |
49 | )
50 | }
51 |
52 | function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
53 | return (
54 |
62 | )
63 | }
64 |
65 | function BreadcrumbSeparator({
66 | children,
67 | className,
68 | ...props
69 | }: React.ComponentProps<"li">) {
70 | return (
71 | svg]:size-3.5", className)}
76 | {...props}
77 | >
78 | {children ?? }
79 |
80 | )
81 | }
82 |
83 | function BreadcrumbEllipsis({
84 | className,
85 | ...props
86 | }: React.ComponentProps<"span">) {
87 | return (
88 |
95 |
96 | More
97 |
98 | )
99 | }
100 |
101 | export {
102 | Breadcrumb,
103 | BreadcrumbList,
104 | BreadcrumbItem,
105 | BreadcrumbLink,
106 | BreadcrumbPage,
107 | BreadcrumbSeparator,
108 | BreadcrumbEllipsis,
109 | }
110 |
--------------------------------------------------------------------------------
/my-app/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { XIcon } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | function Sheet({ ...props }: React.ComponentProps) {
10 | return
11 | }
12 |
13 | function SheetTrigger({
14 | ...props
15 | }: React.ComponentProps) {
16 | return
17 | }
18 |
19 | function SheetClose({
20 | ...props
21 | }: React.ComponentProps) {
22 | return
23 | }
24 |
25 | function SheetPortal({
26 | ...props
27 | }: React.ComponentProps) {
28 | return
29 | }
30 |
31 | function SheetOverlay({
32 | className,
33 | ...props
34 | }: React.ComponentProps) {
35 | return (
36 |
44 | )
45 | }
46 |
47 | function SheetContent({
48 | className,
49 | children,
50 | side = "right",
51 | ...props
52 | }: React.ComponentProps & {
53 | side?: "top" | "right" | "bottom" | "left"
54 | }) {
55 | return (
56 |
57 |
58 |
74 | {children}
75 |
76 |
77 | Close
78 |
79 |
80 |
81 | )
82 | }
83 |
84 | function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
85 | return (
86 |
91 | )
92 | }
93 |
94 | function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
95 | return (
96 |
101 | )
102 | }
103 |
104 | function SheetTitle({
105 | className,
106 | ...props
107 | }: React.ComponentProps) {
108 | return (
109 |
114 | )
115 | }
116 |
117 | function SheetDescription({
118 | className,
119 | ...props
120 | }: React.ComponentProps) {
121 | return (
122 |
127 | )
128 | }
129 |
130 | export {
131 | Sheet,
132 | SheetTrigger,
133 | SheetClose,
134 | SheetContent,
135 | SheetHeader,
136 | SheetFooter,
137 | SheetTitle,
138 | SheetDescription,
139 | }
140 |
--------------------------------------------------------------------------------
/my-app/app/globals.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 | @import "tw-animate-css";
3 |
4 | @custom-variant dark (&:is(.dark *));
5 |
6 | @theme inline {
7 | --color-background: var(--background);
8 | --color-foreground: var(--foreground);
9 | --font-sans: var(--font-geist-sans);
10 | --font-mono: var(--font-geist-mono);
11 | --color-sidebar-ring: var(--sidebar-ring);
12 | --color-sidebar-border: var(--sidebar-border);
13 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
14 | --color-sidebar-accent: var(--sidebar-accent);
15 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
16 | --color-sidebar-primary: var(--sidebar-primary);
17 | --color-sidebar-foreground: var(--sidebar-foreground);
18 | --color-sidebar: var(--sidebar);
19 | --color-chart-5: var(--chart-5);
20 | --color-chart-4: var(--chart-4);
21 | --color-chart-3: var(--chart-3);
22 | --color-chart-2: var(--chart-2);
23 | --color-chart-1: var(--chart-1);
24 | --color-ring: var(--ring);
25 | --color-input: var(--input);
26 | --color-border: var(--border);
27 | --color-destructive: var(--destructive);
28 | --color-accent-foreground: var(--accent-foreground);
29 | --color-accent: var(--accent);
30 | --color-muted-foreground: var(--muted-foreground);
31 | --color-muted: var(--muted);
32 | --color-secondary-foreground: var(--secondary-foreground);
33 | --color-secondary: var(--secondary);
34 | --color-primary-foreground: var(--primary-foreground);
35 | --color-primary: var(--primary);
36 | --color-popover-foreground: var(--popover-foreground);
37 | --color-popover: var(--popover);
38 | --color-card-foreground: var(--card-foreground);
39 | --color-card: var(--card);
40 | --radius-sm: calc(var(--radius) - 4px);
41 | --radius-md: calc(var(--radius) - 2px);
42 | --radius-lg: var(--radius);
43 | --radius-xl: calc(var(--radius) + 4px);
44 | }
45 |
46 | :root {
47 | --radius: 0.625rem;
48 | --background: oklch(1 0 0);
49 | --foreground: oklch(0.141 0.005 285.823);
50 | --card: oklch(1 0 0);
51 | --card-foreground: oklch(0.141 0.005 285.823);
52 | --popover: oklch(1 0 0);
53 | --popover-foreground: oklch(0.141 0.005 285.823);
54 | --primary: oklch(0.21 0.006 285.885);
55 | --primary-foreground: oklch(0.985 0 0);
56 | --secondary: oklch(0.967 0.001 286.375);
57 | --secondary-foreground: oklch(0.21 0.006 285.885);
58 | --muted: oklch(0.967 0.001 286.375);
59 | --muted-foreground: oklch(0.552 0.016 285.938);
60 | --accent: oklch(0.967 0.001 286.375);
61 | --accent-foreground: oklch(0.21 0.006 285.885);
62 | --destructive: oklch(0.577 0.245 27.325);
63 | --border: oklch(0.92 0.004 286.32);
64 | --input: oklch(0.92 0.004 286.32);
65 | --ring: oklch(0.705 0.015 286.067);
66 | --chart-1: oklch(0.646 0.222 41.116);
67 | --chart-2: oklch(0.6 0.118 184.704);
68 | --chart-3: oklch(0.398 0.07 227.392);
69 | --chart-4: oklch(0.828 0.189 84.429);
70 | --chart-5: oklch(0.769 0.188 70.08);
71 | --sidebar: oklch(0.985 0 0);
72 | --sidebar-foreground: oklch(0.141 0.005 285.823);
73 | --sidebar-primary: oklch(0.21 0.006 285.885);
74 | --sidebar-primary-foreground: oklch(0.985 0 0);
75 | --sidebar-accent: oklch(0.967 0.001 286.375);
76 | --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
77 | --sidebar-border: oklch(0.92 0.004 286.32);
78 | --sidebar-ring: oklch(0.705 0.015 286.067);
79 | }
80 |
81 | .dark {
82 | --background: oklch(0.141 0.005 285.823);
83 | --foreground: oklch(0.985 0 0);
84 | --card: oklch(0.21 0.006 285.885);
85 | --card-foreground: oklch(0.985 0 0);
86 | --popover: oklch(0.21 0.006 285.885);
87 | --popover-foreground: oklch(0.985 0 0);
88 | --primary: oklch(0.92 0.004 286.32);
89 | --primary-foreground: oklch(0.21 0.006 285.885);
90 | --secondary: oklch(0.274 0.006 286.033);
91 | --secondary-foreground: oklch(0.985 0 0);
92 | --muted: oklch(0.274 0.006 286.033);
93 | --muted-foreground: oklch(0.705 0.015 286.067);
94 | --accent: oklch(0.274 0.006 286.033);
95 | --accent-foreground: oklch(0.985 0 0);
96 | --destructive: oklch(0.704 0.191 22.216);
97 | --border: oklch(1 0 0 / 10%);
98 | --input: oklch(1 0 0 / 15%);
99 | --ring: oklch(0.552 0.016 285.938);
100 | --chart-1: oklch(0.488 0.243 264.376);
101 | --chart-2: oklch(0.696 0.17 162.48);
102 | --chart-3: oklch(0.769 0.188 70.08);
103 | --chart-4: oklch(0.627 0.265 303.9);
104 | --chart-5: oklch(0.645 0.246 16.439);
105 | --sidebar: oklch(0.21 0.006 285.885);
106 | --sidebar-foreground: oklch(0.985 0 0);
107 | --sidebar-primary: oklch(0.488 0.243 264.376);
108 | --sidebar-primary-foreground: oklch(0.985 0 0);
109 | --sidebar-accent: oklch(0.274 0.006 286.033);
110 | --sidebar-accent-foreground: oklch(0.985 0 0);
111 | --sidebar-border: oklch(1 0 0 / 10%);
112 | --sidebar-ring: oklch(0.552 0.016 285.938);
113 | }
114 |
115 | @layer base {
116 | * {
117 | @apply border-border outline-ring/50;
118 | }
119 | body {
120 | @apply bg-background text-foreground;
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/my-app/components/assistant-ui/markdown-text.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import "@assistant-ui/react-markdown/styles/dot.css";
4 |
5 | import {
6 | CodeHeaderProps,
7 | MarkdownTextPrimitive,
8 | unstable_memoizeMarkdownComponents as memoizeMarkdownComponents,
9 | useIsMarkdownCodeBlock,
10 | } from "@assistant-ui/react-markdown";
11 | import remarkGfm from "remark-gfm";
12 | import { FC, memo, useState } from "react";
13 | import { CheckIcon, CopyIcon } from "lucide-react";
14 |
15 | import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
16 | import { cn } from "@/lib/utils";
17 |
18 | const MarkdownTextImpl = () => {
19 | return (
20 |
25 | );
26 | };
27 |
28 | export const MarkdownText = memo(MarkdownTextImpl);
29 |
30 | const CodeHeader: FC = ({ language, code }) => {
31 | const { isCopied, copyToClipboard } = useCopyToClipboard();
32 | const onCopy = () => {
33 | if (!code || isCopied) return;
34 | copyToClipboard(code);
35 | };
36 |
37 | return (
38 |
39 | {language}
40 |
41 | {!isCopied && }
42 | {isCopied && }
43 |
44 |
45 | );
46 | };
47 |
48 | const useCopyToClipboard = ({
49 | copiedDuration = 3000,
50 | }: {
51 | copiedDuration?: number;
52 | } = {}) => {
53 | const [isCopied, setIsCopied] = useState(false);
54 |
55 | const copyToClipboard = (value: string) => {
56 | if (!value) return;
57 |
58 | navigator.clipboard.writeText(value).then(() => {
59 | setIsCopied(true);
60 | setTimeout(() => setIsCopied(false), copiedDuration);
61 | });
62 | };
63 |
64 | return { isCopied, copyToClipboard };
65 | };
66 |
67 | const defaultComponents = memoizeMarkdownComponents({
68 | h1: ({ className, ...props }) => (
69 |
70 | ),
71 | h2: ({ className, ...props }) => (
72 |
73 | ),
74 | h3: ({ className, ...props }) => (
75 |
76 | ),
77 | h4: ({ className, ...props }) => (
78 |
79 | ),
80 | h5: ({ className, ...props }) => (
81 |
82 | ),
83 | h6: ({ className, ...props }) => (
84 |
85 | ),
86 | p: ({ className, ...props }) => (
87 |
88 | ),
89 | a: ({ className, ...props }) => (
90 |
91 | ),
92 | blockquote: ({ className, ...props }) => (
93 |
94 | ),
95 | ul: ({ className, ...props }) => (
96 | li]:mt-2", className)} {...props} />
97 | ),
98 | ol: ({ className, ...props }) => (
99 | li]:mt-2", className)} {...props} />
100 | ),
101 | hr: ({ className, ...props }) => (
102 |
103 | ),
104 | table: ({ className, ...props }) => (
105 |
106 | ),
107 | th: ({ className, ...props }) => (
108 |
109 | ),
110 | td: ({ className, ...props }) => (
111 |
112 | ),
113 | tr: ({ className, ...props }) => (
114 | td:first-child]:rounded-bl-lg [&:last-child>td:last-child]:rounded-br-lg", className)} {...props} />
115 | ),
116 | sup: ({ className, ...props }) => (
117 | a]:text-xs [&>a]:no-underline", className)} {...props} />
118 | ),
119 | pre: ({ className, ...props }) => (
120 |
121 | ),
122 | code: function Code({ className, ...props }) {
123 | const isCodeBlock = useIsMarkdownCodeBlock();
124 | return (
125 |
129 | );
130 | },
131 | CodeHeader,
132 | });
133 |
--------------------------------------------------------------------------------
/my-app/components/assistant-ui/thread.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ActionBarPrimitive,
3 | BranchPickerPrimitive,
4 | ComposerPrimitive,
5 | MessagePrimitive,
6 | ThreadPrimitive,
7 | } from "@assistant-ui/react";
8 | import type { FC } from "react";
9 | import {
10 | ArrowDownIcon,
11 | CheckIcon,
12 | ChevronLeftIcon,
13 | ChevronRightIcon,
14 | CopyIcon,
15 | PencilIcon,
16 | RefreshCwIcon,
17 | SendHorizontalIcon,
18 | } from "lucide-react";
19 | import { cn } from "@/lib/utils";
20 |
21 | import { Button } from "@/components/ui/button";
22 | import { MarkdownText } from "@/components/assistant-ui/markdown-text";
23 | import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
24 | import { ToolFallback } from "./tool-fallback";
25 |
26 | export const Thread: FC = () => {
27 | return (
28 |
34 |
35 |
36 |
37 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | );
56 | };
57 |
58 | const ThreadScrollToBottom: FC = () => {
59 | return (
60 |
61 |
66 |
67 |
68 |
69 | );
70 | };
71 |
72 | const ThreadWelcome: FC = () => {
73 | return (
74 |
75 |
76 |
77 |
How can I help you today?
78 |
79 |
80 |
81 |
82 | );
83 | };
84 |
85 | const ThreadWelcomeSuggestions: FC = () => {
86 | return (
87 |
88 |
94 |
95 | What is the weather in Tokyo?
96 |
97 |
98 |
104 |
105 | What is assistant-ui?
106 |
107 |
108 |
109 | );
110 | };
111 |
112 | const Composer: FC = () => {
113 | return (
114 |
115 |
121 |
122 |
123 | );
124 | };
125 |
126 | const ComposerAction: FC = () => {
127 | return (
128 | <>
129 |
130 |
131 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
147 |
148 |
149 |
150 |
151 | >
152 | );
153 | };
154 |
155 | const UserMessage: FC = () => {
156 | return (
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 | );
167 | };
168 |
169 | const UserActionBar: FC = () => {
170 | return (
171 |
176 |
177 |
178 |
179 |
180 |
181 |
182 | );
183 | };
184 |
185 | const EditComposer: FC = () => {
186 | return (
187 |
188 |
189 |
190 |
191 |
192 | Cancel
193 |
194 |
195 | Send
196 |
197 |
198 |
199 | );
200 | };
201 |
202 | const AssistantMessage: FC = () => {
203 | return (
204 |
205 |
206 |
209 |
210 |
211 |
212 |
213 |
214 |
215 | );
216 | };
217 |
218 | const AssistantActionBar: FC = () => {
219 | return (
220 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 | );
243 | };
244 |
245 | const BranchPicker: FC = ({
246 | className,
247 | ...rest
248 | }) => {
249 | return (
250 |
258 |
259 |
260 |
261 |
262 |
263 |
264 | /
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 | );
273 | };
274 |
275 | const CircleStopIcon = () => {
276 | return (
277 |
284 |
285 |
286 | );
287 | };
288 |
--------------------------------------------------------------------------------
/my-app/components/ui/sidebar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { Slot } from "@radix-ui/react-slot"
5 | import { VariantProps, cva } from "class-variance-authority"
6 | import { PanelLeftIcon } from "lucide-react"
7 |
8 | import { useIsMobile } from "@/hooks/use-mobile"
9 | import { cn } from "@/lib/utils"
10 | import { Button } from "@/components/ui/button"
11 | import { Input } from "@/components/ui/input"
12 | import { Separator } from "@/components/ui/separator"
13 | import {
14 | Sheet,
15 | SheetContent,
16 | SheetDescription,
17 | SheetHeader,
18 | SheetTitle,
19 | } from "@/components/ui/sheet"
20 | import { Skeleton } from "@/components/ui/skeleton"
21 | import {
22 | Tooltip,
23 | TooltipContent,
24 | TooltipProvider,
25 | TooltipTrigger,
26 | } from "@/components/ui/tooltip"
27 |
28 | const SIDEBAR_COOKIE_NAME = "sidebar_state"
29 | const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
30 | const SIDEBAR_WIDTH = "16rem"
31 | const SIDEBAR_WIDTH_MOBILE = "18rem"
32 | const SIDEBAR_WIDTH_ICON = "3rem"
33 | const SIDEBAR_KEYBOARD_SHORTCUT = "b"
34 |
35 | type SidebarContextProps = {
36 | state: "expanded" | "collapsed"
37 | open: boolean
38 | setOpen: (open: boolean) => void
39 | openMobile: boolean
40 | setOpenMobile: (open: boolean) => void
41 | isMobile: boolean
42 | toggleSidebar: () => void
43 | }
44 |
45 | const SidebarContext = React.createContext(null)
46 |
47 | function useSidebar() {
48 | const context = React.useContext(SidebarContext)
49 | if (!context) {
50 | throw new Error("useSidebar must be used within a SidebarProvider.")
51 | }
52 |
53 | return context
54 | }
55 |
56 | function SidebarProvider({
57 | defaultOpen = true,
58 | open: openProp,
59 | onOpenChange: setOpenProp,
60 | className,
61 | style,
62 | children,
63 | ...props
64 | }: React.ComponentProps<"div"> & {
65 | defaultOpen?: boolean
66 | open?: boolean
67 | onOpenChange?: (open: boolean) => void
68 | }) {
69 | const isMobile = useIsMobile()
70 | const [openMobile, setOpenMobile] = React.useState(false)
71 |
72 | // This is the internal state of the sidebar.
73 | // We use openProp and setOpenProp for control from outside the component.
74 | const [_open, _setOpen] = React.useState(defaultOpen)
75 | const open = openProp ?? _open
76 | const setOpen = React.useCallback(
77 | (value: boolean | ((value: boolean) => boolean)) => {
78 | const openState = typeof value === "function" ? value(open) : value
79 | if (setOpenProp) {
80 | setOpenProp(openState)
81 | } else {
82 | _setOpen(openState)
83 | }
84 |
85 | // This sets the cookie to keep the sidebar state.
86 | document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
87 | },
88 | [setOpenProp, open]
89 | )
90 |
91 | // Helper to toggle the sidebar.
92 | const toggleSidebar = React.useCallback(() => {
93 | return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
94 | }, [isMobile, setOpen, setOpenMobile])
95 |
96 | // Adds a keyboard shortcut to toggle the sidebar.
97 | React.useEffect(() => {
98 | const handleKeyDown = (event: KeyboardEvent) => {
99 | if (
100 | event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
101 | (event.metaKey || event.ctrlKey)
102 | ) {
103 | event.preventDefault()
104 | toggleSidebar()
105 | }
106 | }
107 |
108 | window.addEventListener("keydown", handleKeyDown)
109 | return () => window.removeEventListener("keydown", handleKeyDown)
110 | }, [toggleSidebar])
111 |
112 | // We add a state so that we can do data-state="expanded" or "collapsed".
113 | // This makes it easier to style the sidebar with Tailwind classes.
114 | const state = open ? "expanded" : "collapsed"
115 |
116 | const contextValue = React.useMemo(
117 | () => ({
118 | state,
119 | open,
120 | setOpen,
121 | isMobile,
122 | openMobile,
123 | setOpenMobile,
124 | toggleSidebar,
125 | }),
126 | [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
127 | )
128 |
129 | return (
130 |
131 |
132 |
147 | {children}
148 |
149 |
150 |
151 | )
152 | }
153 |
154 | function Sidebar({
155 | side = "left",
156 | variant = "sidebar",
157 | collapsible = "offcanvas",
158 | className,
159 | children,
160 | ...props
161 | }: React.ComponentProps<"div"> & {
162 | side?: "left" | "right"
163 | variant?: "sidebar" | "floating" | "inset"
164 | collapsible?: "offcanvas" | "icon" | "none"
165 | }) {
166 | const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
167 |
168 | if (collapsible === "none") {
169 | return (
170 |
178 | {children}
179 |
180 | )
181 | }
182 |
183 | if (isMobile) {
184 | return (
185 |
186 |
198 |
199 | Sidebar
200 | Displays the mobile sidebar.
201 |
202 | {children}
203 |
204 |
205 | )
206 | }
207 |
208 | return (
209 |
217 | {/* This is what handles the sidebar gap on desktop */}
218 |
229 |
244 |
249 | {children}
250 |
251 |
252 |
253 | )
254 | }
255 |
256 | function SidebarTrigger({
257 | className,
258 | onClick,
259 | ...props
260 | }: React.ComponentProps) {
261 | const { toggleSidebar } = useSidebar()
262 |
263 | return (
264 | {
271 | onClick?.(event)
272 | toggleSidebar()
273 | }}
274 | {...props}
275 | >
276 |
277 | Toggle Sidebar
278 |
279 | )
280 | }
281 |
282 | function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
283 | const { toggleSidebar } = useSidebar()
284 |
285 | return (
286 |
304 | )
305 | }
306 |
307 | function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
308 | return (
309 |
318 | )
319 | }
320 |
321 | function SidebarInput({
322 | className,
323 | ...props
324 | }: React.ComponentProps) {
325 | return (
326 |
332 | )
333 | }
334 |
335 | function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
336 | return (
337 |
343 | )
344 | }
345 |
346 | function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
347 | return (
348 |
354 | )
355 | }
356 |
357 | function SidebarSeparator({
358 | className,
359 | ...props
360 | }: React.ComponentProps) {
361 | return (
362 |
368 | )
369 | }
370 |
371 | function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
372 | return (
373 |
382 | )
383 | }
384 |
385 | function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
386 | return (
387 |
393 | )
394 | }
395 |
396 | function SidebarGroupLabel({
397 | className,
398 | asChild = false,
399 | ...props
400 | }: React.ComponentProps<"div"> & { asChild?: boolean }) {
401 | const Comp = asChild ? Slot : "div"
402 |
403 | return (
404 | svg]:size-4 [&>svg]:shrink-0",
409 | "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
410 | className
411 | )}
412 | {...props}
413 | />
414 | )
415 | }
416 |
417 | function SidebarGroupAction({
418 | className,
419 | asChild = false,
420 | ...props
421 | }: React.ComponentProps<"button"> & { asChild?: boolean }) {
422 | const Comp = asChild ? Slot : "button"
423 |
424 | return (
425 | svg]:size-4 [&>svg]:shrink-0",
430 | // Increases the hit area of the button on mobile.
431 | "after:absolute after:-inset-2 md:after:hidden",
432 | "group-data-[collapsible=icon]:hidden",
433 | className
434 | )}
435 | {...props}
436 | />
437 | )
438 | }
439 |
440 | function SidebarGroupContent({
441 | className,
442 | ...props
443 | }: React.ComponentProps<"div">) {
444 | return (
445 |
451 | )
452 | }
453 |
454 | function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
455 | return (
456 |
462 | )
463 | }
464 |
465 | function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
466 | return (
467 |
473 | )
474 | }
475 |
476 | const sidebarMenuButtonVariants = cva(
477 | "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
478 | {
479 | variants: {
480 | variant: {
481 | default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
482 | outline:
483 | "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
484 | },
485 | size: {
486 | default: "h-8 text-sm",
487 | sm: "h-7 text-xs",
488 | lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
489 | },
490 | },
491 | defaultVariants: {
492 | variant: "default",
493 | size: "default",
494 | },
495 | }
496 | )
497 |
498 | function SidebarMenuButton({
499 | asChild = false,
500 | isActive = false,
501 | variant = "default",
502 | size = "default",
503 | tooltip,
504 | className,
505 | ...props
506 | }: React.ComponentProps<"button"> & {
507 | asChild?: boolean
508 | isActive?: boolean
509 | tooltip?: string | React.ComponentProps
510 | } & VariantProps) {
511 | const Comp = asChild ? Slot : "button"
512 | const { isMobile, state } = useSidebar()
513 |
514 | const button = (
515 |
523 | )
524 |
525 | if (!tooltip) {
526 | return button
527 | }
528 |
529 | if (typeof tooltip === "string") {
530 | tooltip = {
531 | children: tooltip,
532 | }
533 | }
534 |
535 | return (
536 |
537 | {button}
538 |
544 |
545 | )
546 | }
547 |
548 | function SidebarMenuAction({
549 | className,
550 | asChild = false,
551 | showOnHover = false,
552 | ...props
553 | }: React.ComponentProps<"button"> & {
554 | asChild?: boolean
555 | showOnHover?: boolean
556 | }) {
557 | const Comp = asChild ? Slot : "button"
558 |
559 | return (
560 | svg]:size-4 [&>svg]:shrink-0",
565 | // Increases the hit area of the button on mobile.
566 | "after:absolute after:-inset-2 md:after:hidden",
567 | "peer-data-[size=sm]/menu-button:top-1",
568 | "peer-data-[size=default]/menu-button:top-1.5",
569 | "peer-data-[size=lg]/menu-button:top-2.5",
570 | "group-data-[collapsible=icon]:hidden",
571 | showOnHover &&
572 | "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
573 | className
574 | )}
575 | {...props}
576 | />
577 | )
578 | }
579 |
580 | function SidebarMenuBadge({
581 | className,
582 | ...props
583 | }: React.ComponentProps<"div">) {
584 | return (
585 |
599 | )
600 | }
601 |
602 | function SidebarMenuSkeleton({
603 | className,
604 | showIcon = false,
605 | ...props
606 | }: React.ComponentProps<"div"> & {
607 | showIcon?: boolean
608 | }) {
609 | // Random width between 50 to 90%.
610 | const width = React.useMemo(() => {
611 | return `${Math.floor(Math.random() * 40) + 50}%`
612 | }, [])
613 |
614 | return (
615 |
621 | {showIcon && (
622 |
626 | )}
627 |
636 |
637 | )
638 | }
639 |
640 | function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
641 | return (
642 |
652 | )
653 | }
654 |
655 | function SidebarMenuSubItem({
656 | className,
657 | ...props
658 | }: React.ComponentProps<"li">) {
659 | return (
660 |
666 | )
667 | }
668 |
669 | function SidebarMenuSubButton({
670 | asChild = false,
671 | size = "md",
672 | isActive = false,
673 | className,
674 | ...props
675 | }: React.ComponentProps<"a"> & {
676 | asChild?: boolean
677 | size?: "sm" | "md"
678 | isActive?: boolean
679 | }) {
680 | const Comp = asChild ? Slot : "a"
681 |
682 | return (
683 | svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
690 | "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
691 | size === "sm" && "text-xs",
692 | size === "md" && "text-sm",
693 | "group-data-[collapsible=icon]:hidden",
694 | className
695 | )}
696 | {...props}
697 | />
698 | )
699 | }
700 |
701 | export {
702 | Sidebar,
703 | SidebarContent,
704 | SidebarFooter,
705 | SidebarGroup,
706 | SidebarGroupAction,
707 | SidebarGroupContent,
708 | SidebarGroupLabel,
709 | SidebarHeader,
710 | SidebarInput,
711 | SidebarInset,
712 | SidebarMenu,
713 | SidebarMenuAction,
714 | SidebarMenuBadge,
715 | SidebarMenuButton,
716 | SidebarMenuItem,
717 | SidebarMenuSkeleton,
718 | SidebarMenuSub,
719 | SidebarMenuSubButton,
720 | SidebarMenuSubItem,
721 | SidebarProvider,
722 | SidebarRail,
723 | SidebarSeparator,
724 | SidebarTrigger,
725 | useSidebar,
726 | }
727 |
--------------------------------------------------------------------------------