NeoPy commited on
Commit
1ae64c5
·
verified ·
1 Parent(s): b796eda

Upload folder using huggingface_hub

Browse files
.gitignore ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+
36
+ # vercel
37
+ .vercel
38
+
39
+ # typescript
40
+ *.tsbuildinfo
41
+ next-env.d.ts
README.md CHANGED
@@ -1,10 +1,25 @@
1
- ---
2
- title: Ui Stater
3
- emoji: 🏢
4
- colorFrom: yellow
5
- colorTo: purple
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.
app/api/chat/route.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { openai } from "@ai-sdk/openai";
2
+ import { streamText, convertToModelMessages, type UIMessage } from "ai";
3
+
4
+ export async function POST(req: Request) {
5
+ const { messages }: { messages: UIMessage[] } = await req.json();
6
+
7
+ const result = streamText({
8
+ model: openai.responses("gpt-5-nano"),
9
+ messages: convertToModelMessages(messages),
10
+ providerOptions: {
11
+ openai: {
12
+ reasoningEffort: "low",
13
+ reasoningSummary: "auto",
14
+ },
15
+ },
16
+ });
17
+
18
+ return result.toUIMessageStreamResponse({
19
+ sendReasoning: true,
20
+ });
21
+ }
app/assistant.tsx ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { AssistantRuntimeProvider } from "@assistant-ui/react";
4
+ import {
5
+ useChatRuntime,
6
+ AssistantChatTransport,
7
+ } from "@assistant-ui/react-ai-sdk";
8
+ import { Thread } from "@/components/assistant-ui/thread";
9
+ import {
10
+ SidebarInset,
11
+ SidebarProvider,
12
+ SidebarTrigger,
13
+ } from "@/components/ui/sidebar";
14
+ import { ThreadListSidebar } from "@/components/assistant-ui/threadlist-sidebar";
15
+ import { Separator } from "@/components/ui/separator";
16
+ import {
17
+ Breadcrumb,
18
+ BreadcrumbItem,
19
+ BreadcrumbLink,
20
+ BreadcrumbList,
21
+ BreadcrumbPage,
22
+ BreadcrumbSeparator,
23
+ } from "@/components/ui/breadcrumb";
24
+
25
+ export const Assistant = () => {
26
+ const runtime = useChatRuntime({
27
+ transport: new AssistantChatTransport({
28
+ api: "/api/chat",
29
+ }),
30
+ });
31
+
32
+ return (
33
+ <AssistantRuntimeProvider runtime={runtime}>
34
+ <SidebarProvider>
35
+ <div className="flex h-dvh w-full pr-0.5">
36
+ <ThreadListSidebar />
37
+ <SidebarInset>
38
+ <header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
39
+ <SidebarTrigger />
40
+ <Separator orientation="vertical" className="mr-2 h-4" />
41
+ <Breadcrumb>
42
+ <BreadcrumbList>
43
+ <BreadcrumbItem className="hidden md:block">
44
+ <BreadcrumbLink
45
+ href="https://www.assistant-ui.com/docs/getting-started"
46
+ target="_blank"
47
+ rel="noopener noreferrer"
48
+ >
49
+ Build Your Own ChatGPT UX
50
+ </BreadcrumbLink>
51
+ </BreadcrumbItem>
52
+ <BreadcrumbSeparator className="hidden md:block" />
53
+ <BreadcrumbItem>
54
+ <BreadcrumbPage>Starter Template</BreadcrumbPage>
55
+ </BreadcrumbItem>
56
+ </BreadcrumbList>
57
+ </Breadcrumb>
58
+ </header>
59
+ <div className="flex-1 overflow-hidden">
60
+ <Thread />
61
+ </div>
62
+ </SidebarInset>
63
+ </div>
64
+ </SidebarProvider>
65
+ </AssistantRuntimeProvider>
66
+ );
67
+ };
app/favicon.ico ADDED
app/globals.css ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ --animate-shimmer: shimmer-sweep var(--shimmer-duration, 1000ms) linear
45
+ infinite both;
46
+ @keyframes shimmer-sweep {
47
+ from {
48
+ background-position: 150% 0;
49
+ }
50
+ to {
51
+ background-position: -100% 0;
52
+ }
53
+ }
54
+ }
55
+
56
+ :root {
57
+ --radius: 0.625rem;
58
+ --background: oklch(1 0 0);
59
+ --foreground: oklch(0.141 0.005 285.823);
60
+ --card: oklch(1 0 0);
61
+ --card-foreground: oklch(0.141 0.005 285.823);
62
+ --popover: oklch(1 0 0);
63
+ --popover-foreground: oklch(0.141 0.005 285.823);
64
+ --primary: oklch(0.21 0.006 285.885);
65
+ --primary-foreground: oklch(0.985 0 0);
66
+ --secondary: oklch(0.967 0.001 286.375);
67
+ --secondary-foreground: oklch(0.21 0.006 285.885);
68
+ --muted: oklch(0.967 0.001 286.375);
69
+ --muted-foreground: oklch(0.552 0.016 285.938);
70
+ --accent: oklch(0.967 0.001 286.375);
71
+ --accent-foreground: oklch(0.21 0.006 285.885);
72
+ --destructive: oklch(0.577 0.245 27.325);
73
+ --border: oklch(0.92 0.004 286.32);
74
+ --input: oklch(0.92 0.004 286.32);
75
+ --ring: oklch(0.705 0.015 286.067);
76
+ --chart-1: oklch(0.646 0.222 41.116);
77
+ --chart-2: oklch(0.6 0.118 184.704);
78
+ --chart-3: oklch(0.398 0.07 227.392);
79
+ --chart-4: oklch(0.828 0.189 84.429);
80
+ --chart-5: oklch(0.769 0.188 70.08);
81
+ --sidebar: oklch(0.985 0 0);
82
+ --sidebar-foreground: oklch(0.141 0.005 285.823);
83
+ --sidebar-primary: oklch(0.21 0.006 285.885);
84
+ --sidebar-primary-foreground: oklch(0.985 0 0);
85
+ --sidebar-accent: oklch(0.967 0.001 286.375);
86
+ --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
87
+ --sidebar-border: oklch(0.92 0.004 286.32);
88
+ --sidebar-ring: oklch(0.705 0.015 286.067);
89
+ }
90
+
91
+ .dark {
92
+ --background: oklch(0.141 0.005 285.823);
93
+ --foreground: oklch(0.985 0 0);
94
+ --card: oklch(0.21 0.006 285.885);
95
+ --card-foreground: oklch(0.985 0 0);
96
+ --popover: oklch(0.21 0.006 285.885);
97
+ --popover-foreground: oklch(0.985 0 0);
98
+ --primary: oklch(0.92 0.004 286.32);
99
+ --primary-foreground: oklch(0.21 0.006 285.885);
100
+ --secondary: oklch(0.274 0.006 286.033);
101
+ --secondary-foreground: oklch(0.985 0 0);
102
+ --muted: oklch(0.274 0.006 286.033);
103
+ --muted-foreground: oklch(0.705 0.015 286.067);
104
+ --accent: oklch(0.274 0.006 286.033);
105
+ --accent-foreground: oklch(0.985 0 0);
106
+ --destructive: oklch(0.704 0.191 22.216);
107
+ --border: oklch(1 0 0 / 10%);
108
+ --input: oklch(1 0 0 / 15%);
109
+ --ring: oklch(0.552 0.016 285.938);
110
+ --chart-1: oklch(0.488 0.243 264.376);
111
+ --chart-2: oklch(0.696 0.17 162.48);
112
+ --chart-3: oklch(0.769 0.188 70.08);
113
+ --chart-4: oklch(0.627 0.265 303.9);
114
+ --chart-5: oklch(0.645 0.246 16.439);
115
+ --sidebar: oklch(0.21 0.006 285.885);
116
+ --sidebar-foreground: oklch(0.985 0 0);
117
+ --sidebar-primary: oklch(0.488 0.243 264.376);
118
+ --sidebar-primary-foreground: oklch(0.985 0 0);
119
+ --sidebar-accent: oklch(0.274 0.006 286.033);
120
+ --sidebar-accent-foreground: oklch(0.985 0 0);
121
+ --sidebar-border: oklch(1 0 0 / 10%);
122
+ --sidebar-ring: oklch(0.552 0.016 285.938);
123
+ }
124
+
125
+ @layer base {
126
+ * {
127
+ @apply border-border outline-ring/50;
128
+ }
129
+
130
+ :root {
131
+ color-scheme: light;
132
+ }
133
+
134
+ :root.dark {
135
+ color-scheme: dark;
136
+ }
137
+
138
+ body {
139
+ @apply bg-background text-foreground;
140
+ }
141
+ }
app/layout.tsx ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ <html lang="en">
27
+ <body
28
+ className={`${geistSans.variable} ${geistMono.variable} antialiased`}
29
+ >
30
+ {children}
31
+ </body>
32
+ </html>
33
+ );
34
+ }
app/page.tsx ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import { Assistant } from "./assistant";
2
+
3
+ export default function Home() {
4
+ return <Assistant />;
5
+ }
components.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
components/assistant-ui/attachment.tsx ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { PropsWithChildren, useEffect, useState, type FC } from "react";
4
+ import Image from "next/image";
5
+ import { XIcon, PlusIcon, FileText } from "lucide-react";
6
+ import {
7
+ AttachmentPrimitive,
8
+ ComposerPrimitive,
9
+ MessagePrimitive,
10
+ useAssistantState,
11
+ useAssistantApi,
12
+ } from "@assistant-ui/react";
13
+ import { useShallow } from "zustand/shallow";
14
+ import {
15
+ Tooltip,
16
+ TooltipContent,
17
+ TooltipTrigger,
18
+ } from "@/components/ui/tooltip";
19
+ import {
20
+ Dialog,
21
+ DialogTitle,
22
+ DialogContent,
23
+ DialogTrigger,
24
+ } from "@/components/ui/dialog";
25
+ import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
26
+ import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
27
+ import { cn } from "@/lib/utils";
28
+
29
+ const useFileSrc = (file: File | undefined) => {
30
+ const [src, setSrc] = useState<string | undefined>(undefined);
31
+
32
+ useEffect(() => {
33
+ if (!file) {
34
+ setSrc(undefined);
35
+ return;
36
+ }
37
+
38
+ const objectUrl = URL.createObjectURL(file);
39
+ setSrc(objectUrl);
40
+
41
+ return () => {
42
+ URL.revokeObjectURL(objectUrl);
43
+ };
44
+ }, [file]);
45
+
46
+ return src;
47
+ };
48
+
49
+ const useAttachmentSrc = () => {
50
+ const { file, src } = useAssistantState(
51
+ useShallow(({ attachment }): { file?: File; src?: string } => {
52
+ if (attachment.type !== "image") return {};
53
+ if (attachment.file) return { file: attachment.file };
54
+ const src = attachment.content?.filter((c) => c.type === "image")[0]
55
+ ?.image;
56
+ if (!src) return {};
57
+ return { src };
58
+ }),
59
+ );
60
+
61
+ return useFileSrc(file) ?? src;
62
+ };
63
+
64
+ type AttachmentPreviewProps = {
65
+ src: string;
66
+ };
67
+
68
+ const AttachmentPreview: FC<AttachmentPreviewProps> = ({ src }) => {
69
+ const [isLoaded, setIsLoaded] = useState(false);
70
+ return (
71
+ <Image
72
+ src={src}
73
+ alt="Image Preview"
74
+ width={1}
75
+ height={1}
76
+ className={
77
+ isLoaded
78
+ ? "aui-attachment-preview-image-loaded block h-auto max-h-[80vh] w-auto max-w-full object-contain"
79
+ : "aui-attachment-preview-image-loading hidden"
80
+ }
81
+ onLoadingComplete={() => setIsLoaded(true)}
82
+ priority={false}
83
+ />
84
+ );
85
+ };
86
+
87
+ const AttachmentPreviewDialog: FC<PropsWithChildren> = ({ children }) => {
88
+ const src = useAttachmentSrc();
89
+
90
+ if (!src) return children;
91
+
92
+ return (
93
+ <Dialog>
94
+ <DialogTrigger
95
+ className="aui-attachment-preview-trigger cursor-pointer transition-colors hover:bg-accent/50"
96
+ asChild
97
+ >
98
+ {children}
99
+ </DialogTrigger>
100
+ <DialogContent className="aui-attachment-preview-dialog-content p-2 sm:max-w-3xl [&_svg]:text-background [&>button]:rounded-full [&>button]:bg-foreground/60 [&>button]:p-1 [&>button]:opacity-100 [&>button]:!ring-0 [&>button]:hover:[&_svg]:text-destructive">
101
+ <DialogTitle className="aui-sr-only sr-only">
102
+ Image Attachment Preview
103
+ </DialogTitle>
104
+ <div className="aui-attachment-preview relative mx-auto flex max-h-[80dvh] w-full items-center justify-center overflow-hidden bg-background">
105
+ <AttachmentPreview src={src} />
106
+ </div>
107
+ </DialogContent>
108
+ </Dialog>
109
+ );
110
+ };
111
+
112
+ const AttachmentThumb: FC = () => {
113
+ const isImage = useAssistantState(
114
+ ({ attachment }) => attachment.type === "image",
115
+ );
116
+ const src = useAttachmentSrc();
117
+
118
+ return (
119
+ <Avatar className="aui-attachment-tile-avatar h-full w-full rounded-none">
120
+ <AvatarImage
121
+ src={src}
122
+ alt="Attachment preview"
123
+ className="aui-attachment-tile-image object-cover"
124
+ />
125
+ <AvatarFallback delayMs={isImage ? 200 : 0}>
126
+ <FileText className="aui-attachment-tile-fallback-icon size-8 text-muted-foreground" />
127
+ </AvatarFallback>
128
+ </Avatar>
129
+ );
130
+ };
131
+
132
+ const AttachmentUI: FC = () => {
133
+ const api = useAssistantApi();
134
+ const isComposer = api.attachment.source === "composer";
135
+
136
+ const isImage = useAssistantState(
137
+ ({ attachment }) => attachment.type === "image",
138
+ );
139
+ const typeLabel = useAssistantState(({ attachment }) => {
140
+ const type = attachment.type;
141
+ switch (type) {
142
+ case "image":
143
+ return "Image";
144
+ case "document":
145
+ return "Document";
146
+ case "file":
147
+ return "File";
148
+ default:
149
+ const _exhaustiveCheck: never = type;
150
+ throw new Error(`Unknown attachment type: ${_exhaustiveCheck}`);
151
+ }
152
+ });
153
+
154
+ return (
155
+ <Tooltip>
156
+ <AttachmentPrimitive.Root
157
+ className={cn(
158
+ "aui-attachment-root relative",
159
+ isImage &&
160
+ "aui-attachment-root-composer only:[&>#attachment-tile]:size-24",
161
+ )}
162
+ >
163
+ <AttachmentPreviewDialog>
164
+ <TooltipTrigger asChild>
165
+ <div
166
+ className={cn(
167
+ "aui-attachment-tile size-14 cursor-pointer overflow-hidden rounded-[14px] border bg-muted transition-opacity hover:opacity-75",
168
+ isComposer &&
169
+ "aui-attachment-tile-composer border-foreground/20",
170
+ )}
171
+ role="button"
172
+ id="attachment-tile"
173
+ aria-label={`${typeLabel} attachment`}
174
+ >
175
+ <AttachmentThumb />
176
+ </div>
177
+ </TooltipTrigger>
178
+ </AttachmentPreviewDialog>
179
+ {isComposer && <AttachmentRemove />}
180
+ </AttachmentPrimitive.Root>
181
+ <TooltipContent side="top">
182
+ <AttachmentPrimitive.Name />
183
+ </TooltipContent>
184
+ </Tooltip>
185
+ );
186
+ };
187
+
188
+ const AttachmentRemove: FC = () => {
189
+ return (
190
+ <AttachmentPrimitive.Remove asChild>
191
+ <TooltipIconButton
192
+ tooltip="Remove file"
193
+ className="aui-attachment-tile-remove absolute top-1.5 right-1.5 size-3.5 rounded-full bg-white text-muted-foreground opacity-100 shadow-sm hover:!bg-white [&_svg]:text-black hover:[&_svg]:text-destructive"
194
+ side="top"
195
+ >
196
+ <XIcon className="aui-attachment-remove-icon size-3 dark:stroke-[2.5px]" />
197
+ </TooltipIconButton>
198
+ </AttachmentPrimitive.Remove>
199
+ );
200
+ };
201
+
202
+ export const UserMessageAttachments: FC = () => {
203
+ return (
204
+ <div className="aui-user-message-attachments-end col-span-full col-start-1 row-start-1 flex w-full flex-row justify-end gap-2">
205
+ <MessagePrimitive.Attachments components={{ Attachment: AttachmentUI }} />
206
+ </div>
207
+ );
208
+ };
209
+
210
+ export const ComposerAttachments: FC = () => {
211
+ return (
212
+ <div className="aui-composer-attachments mb-2 flex w-full flex-row items-center gap-2 overflow-x-auto px-1.5 pt-0.5 pb-1 empty:hidden">
213
+ <ComposerPrimitive.Attachments
214
+ components={{ Attachment: AttachmentUI }}
215
+ />
216
+ </div>
217
+ );
218
+ };
219
+
220
+ export const ComposerAddAttachment: FC = () => {
221
+ return (
222
+ <ComposerPrimitive.AddAttachment asChild>
223
+ <TooltipIconButton
224
+ tooltip="Add Attachment"
225
+ side="bottom"
226
+ variant="ghost"
227
+ size="icon"
228
+ className="aui-composer-add-attachment size-[34px] rounded-full p-1 text-xs font-semibold hover:bg-muted-foreground/15 dark:border-muted-foreground/15 dark:hover:bg-muted-foreground/30"
229
+ aria-label="Add Attachment"
230
+ >
231
+ <PlusIcon className="aui-attachment-add-icon size-5 stroke-[1.5px]" />
232
+ </TooltipIconButton>
233
+ </ComposerPrimitive.AddAttachment>
234
+ );
235
+ };
components/assistant-ui/markdown-text.tsx ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import "@assistant-ui/react-markdown/styles/dot.css";
4
+
5
+ import {
6
+ type CodeHeaderProps,
7
+ MarkdownTextPrimitive,
8
+ unstable_memoizeMarkdownComponents as memoizeMarkdownComponents,
9
+ useIsMarkdownCodeBlock,
10
+ } from "@assistant-ui/react-markdown";
11
+ import remarkGfm from "remark-gfm";
12
+ import { type 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
+ <MarkdownTextPrimitive
21
+ remarkPlugins={[remarkGfm]}
22
+ className="aui-md"
23
+ components={defaultComponents}
24
+ />
25
+ );
26
+ };
27
+
28
+ export const MarkdownText = memo(MarkdownTextImpl);
29
+
30
+ const CodeHeader: FC<CodeHeaderProps> = ({ language, code }) => {
31
+ const { isCopied, copyToClipboard } = useCopyToClipboard();
32
+ const onCopy = () => {
33
+ if (!code || isCopied) return;
34
+ copyToClipboard(code);
35
+ };
36
+
37
+ return (
38
+ <div className="aui-code-header-root mt-4 flex items-center justify-between gap-4 rounded-t-lg bg-muted-foreground/15 px-4 py-2 text-sm font-semibold text-foreground dark:bg-muted-foreground/20">
39
+ <span className="aui-code-header-language lowercase [&>span]:text-xs">
40
+ {language}
41
+ </span>
42
+ <TooltipIconButton tooltip="Copy" onClick={onCopy}>
43
+ {!isCopied && <CopyIcon />}
44
+ {isCopied && <CheckIcon />}
45
+ </TooltipIconButton>
46
+ </div>
47
+ );
48
+ };
49
+
50
+ const useCopyToClipboard = ({
51
+ copiedDuration = 3000,
52
+ }: {
53
+ copiedDuration?: number;
54
+ } = {}) => {
55
+ const [isCopied, setIsCopied] = useState<boolean>(false);
56
+
57
+ const copyToClipboard = (value: string) => {
58
+ if (!value) return;
59
+
60
+ navigator.clipboard.writeText(value).then(() => {
61
+ setIsCopied(true);
62
+ setTimeout(() => setIsCopied(false), copiedDuration);
63
+ });
64
+ };
65
+
66
+ return { isCopied, copyToClipboard };
67
+ };
68
+
69
+ const defaultComponents = memoizeMarkdownComponents({
70
+ h1: ({ className, ...props }) => (
71
+ <h1
72
+ className={cn(
73
+ "aui-md-h1 mb-8 scroll-m-20 text-4xl font-extrabold tracking-tight last:mb-0",
74
+ className,
75
+ )}
76
+ {...props}
77
+ />
78
+ ),
79
+ h2: ({ className, ...props }) => (
80
+ <h2
81
+ className={cn(
82
+ "aui-md-h2 mt-8 mb-4 scroll-m-20 text-3xl font-semibold tracking-tight first:mt-0 last:mb-0",
83
+ className,
84
+ )}
85
+ {...props}
86
+ />
87
+ ),
88
+ h3: ({ className, ...props }) => (
89
+ <h3
90
+ className={cn(
91
+ "aui-md-h3 mt-6 mb-4 scroll-m-20 text-2xl font-semibold tracking-tight first:mt-0 last:mb-0",
92
+ className,
93
+ )}
94
+ {...props}
95
+ />
96
+ ),
97
+ h4: ({ className, ...props }) => (
98
+ <h4
99
+ className={cn(
100
+ "aui-md-h4 mt-6 mb-4 scroll-m-20 text-xl font-semibold tracking-tight first:mt-0 last:mb-0",
101
+ className,
102
+ )}
103
+ {...props}
104
+ />
105
+ ),
106
+ h5: ({ className, ...props }) => (
107
+ <h5
108
+ className={cn(
109
+ "aui-md-h5 my-4 text-lg font-semibold first:mt-0 last:mb-0",
110
+ className,
111
+ )}
112
+ {...props}
113
+ />
114
+ ),
115
+ h6: ({ className, ...props }) => (
116
+ <h6
117
+ className={cn(
118
+ "aui-md-h6 my-4 font-semibold first:mt-0 last:mb-0",
119
+ className,
120
+ )}
121
+ {...props}
122
+ />
123
+ ),
124
+ p: ({ className, ...props }) => (
125
+ <p
126
+ className={cn(
127
+ "aui-md-p mt-5 mb-5 leading-7 first:mt-0 last:mb-0",
128
+ className,
129
+ )}
130
+ {...props}
131
+ />
132
+ ),
133
+ a: ({ className, ...props }) => (
134
+ <a
135
+ className={cn(
136
+ "aui-md-a font-medium text-primary underline underline-offset-4",
137
+ className,
138
+ )}
139
+ {...props}
140
+ />
141
+ ),
142
+ blockquote: ({ className, ...props }) => (
143
+ <blockquote
144
+ className={cn("aui-md-blockquote border-l-2 pl-6 italic", className)}
145
+ {...props}
146
+ />
147
+ ),
148
+ ul: ({ className, ...props }) => (
149
+ <ul
150
+ className={cn("aui-md-ul my-5 ml-6 list-disc [&>li]:mt-2", className)}
151
+ {...props}
152
+ />
153
+ ),
154
+ ol: ({ className, ...props }) => (
155
+ <ol
156
+ className={cn("aui-md-ol my-5 ml-6 list-decimal [&>li]:mt-2", className)}
157
+ {...props}
158
+ />
159
+ ),
160
+ hr: ({ className, ...props }) => (
161
+ <hr className={cn("aui-md-hr my-5 border-b", className)} {...props} />
162
+ ),
163
+ table: ({ className, ...props }) => (
164
+ <table
165
+ className={cn(
166
+ "aui-md-table my-5 w-full border-separate border-spacing-0 overflow-y-auto",
167
+ className,
168
+ )}
169
+ {...props}
170
+ />
171
+ ),
172
+ th: ({ className, ...props }) => (
173
+ <th
174
+ className={cn(
175
+ "aui-md-th bg-muted px-4 py-2 text-left font-bold first:rounded-tl-lg last:rounded-tr-lg [&[align=center]]:text-center [&[align=right]]:text-right",
176
+ className,
177
+ )}
178
+ {...props}
179
+ />
180
+ ),
181
+ td: ({ className, ...props }) => (
182
+ <td
183
+ className={cn(
184
+ "aui-md-td border-b border-l px-4 py-2 text-left last:border-r [&[align=center]]:text-center [&[align=right]]:text-right",
185
+ className,
186
+ )}
187
+ {...props}
188
+ />
189
+ ),
190
+ tr: ({ className, ...props }) => (
191
+ <tr
192
+ className={cn(
193
+ "aui-md-tr m-0 border-b p-0 first:border-t [&:last-child>td:first-child]:rounded-bl-lg [&:last-child>td:last-child]:rounded-br-lg",
194
+ className,
195
+ )}
196
+ {...props}
197
+ />
198
+ ),
199
+ sup: ({ className, ...props }) => (
200
+ <sup
201
+ className={cn("aui-md-sup [&>a]:text-xs [&>a]:no-underline", className)}
202
+ {...props}
203
+ />
204
+ ),
205
+ pre: ({ className, ...props }) => (
206
+ <pre
207
+ className={cn(
208
+ "aui-md-pre overflow-x-auto !rounded-t-none rounded-b-lg bg-black p-4 text-white",
209
+ className,
210
+ )}
211
+ {...props}
212
+ />
213
+ ),
214
+ code: function Code({ className, ...props }) {
215
+ const isCodeBlock = useIsMarkdownCodeBlock();
216
+ return (
217
+ <code
218
+ className={cn(
219
+ !isCodeBlock &&
220
+ "aui-md-inline-code rounded border bg-muted font-semibold",
221
+ className,
222
+ )}
223
+ {...props}
224
+ />
225
+ );
226
+ },
227
+ CodeHeader,
228
+ });
components/assistant-ui/reasoning.tsx ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { BrainIcon, ChevronDownIcon } from "lucide-react";
4
+ import {
5
+ memo,
6
+ useCallback,
7
+ useRef,
8
+ useState,
9
+ type FC,
10
+ type PropsWithChildren,
11
+ } from "react";
12
+
13
+ import {
14
+ useScrollLock,
15
+ useAssistantState,
16
+ type ReasoningMessagePartComponent,
17
+ type ReasoningGroupComponent,
18
+ } from "@assistant-ui/react";
19
+
20
+ import { MarkdownText } from "@/components/assistant-ui/markdown-text";
21
+ import {
22
+ Collapsible,
23
+ CollapsibleContent,
24
+ CollapsibleTrigger,
25
+ } from "@/components/ui/collapsible";
26
+ import { cn } from "@/lib/utils";
27
+
28
+ const ANIMATION_DURATION = 200;
29
+ const SHIMMER_DURATION = 1000;
30
+
31
+ /**
32
+ * Root collapsible container that manages open/closed state and scroll lock.
33
+ * Provides animation timing via CSS variable and prevents scroll jumps on collapse.
34
+ */
35
+ const ReasoningRoot: FC<
36
+ PropsWithChildren<{
37
+ className?: string;
38
+ }>
39
+ > = ({ className, children }) => {
40
+ const collapsibleRef = useRef<HTMLDivElement>(null);
41
+ const [isOpen, setIsOpen] = useState(false);
42
+ const lockScroll = useScrollLock(collapsibleRef, ANIMATION_DURATION);
43
+
44
+ const handleOpenChange = useCallback(
45
+ (open: boolean) => {
46
+ if (!open) {
47
+ lockScroll();
48
+ }
49
+ setIsOpen(open);
50
+ },
51
+ [lockScroll],
52
+ );
53
+
54
+ return (
55
+ <Collapsible
56
+ ref={collapsibleRef}
57
+ open={isOpen}
58
+ onOpenChange={handleOpenChange}
59
+ className={cn("aui-reasoning-root mb-4 w-full", className)}
60
+ style={
61
+ {
62
+ "--animation-duration": `${ANIMATION_DURATION}ms`,
63
+ "--shimmer-duration": `${SHIMMER_DURATION}ms`,
64
+ } as React.CSSProperties
65
+ }
66
+ >
67
+ {children}
68
+ </Collapsible>
69
+ );
70
+ };
71
+
72
+ ReasoningRoot.displayName = "ReasoningRoot";
73
+
74
+ /**
75
+ * Gradient overlay that softens the bottom edge during expand/collapse animations.
76
+ * Animation: Fades out with delay when opening and fades back in when closing.
77
+ */
78
+ const GradientFade: FC<{ className?: string }> = ({ className }) => (
79
+ <div
80
+ className={cn(
81
+ "aui-reasoning-fade pointer-events-none absolute inset-x-0 bottom-0 z-10 h-16",
82
+ "bg-[linear-gradient(to_top,var(--color-background),transparent)]",
83
+ "animate-in fade-in-0",
84
+ "group-data-[state=open]/collapsible-content:animate-out",
85
+ "group-data-[state=open]/collapsible-content:fade-out-0",
86
+ "group-data-[state=open]/collapsible-content:delay-[calc(var(--animation-duration)*0.75)]", // calc for timing the delay
87
+ "group-data-[state=open]/collapsible-content:fill-mode-forwards",
88
+ "duration-(--animation-duration)",
89
+ "group-data-[state=open]/collapsible-content:duration-(--animation-duration)",
90
+ className,
91
+ )}
92
+ />
93
+ );
94
+
95
+ /**
96
+ * Trigger button for the Reasoning collapsible.
97
+ * Composed of icons, label, and text shimmer animation when reasoning is being streamed.
98
+ */
99
+ const ReasoningTrigger: FC<{ active: boolean; className?: string }> = ({
100
+ active,
101
+ className,
102
+ }) => (
103
+ <CollapsibleTrigger
104
+ className={cn(
105
+ "aui-reasoning-trigger group/trigger -mb-2 flex max-w-[75%] items-center gap-2 py-2 text-sm text-muted-foreground transition-colors hover:text-foreground",
106
+ className,
107
+ )}
108
+ >
109
+ <BrainIcon className="aui-reasoning-trigger-icon size-4 shrink-0" />
110
+ <span className="aui-reasoning-trigger-label-wrapper relative inline-block leading-none">
111
+ <span>Reasoning</span>
112
+ {active ? (
113
+ <span
114
+ aria-hidden
115
+ className={cn(
116
+ "aui-reasoning-trigger-shimmer pointer-events-none absolute inset-0 bg-clip-text bg-no-repeat text-transparent motion-reduce:animate-none",
117
+ "animate-shimmer will-change-[background-position]",
118
+ "bg-size-[200%_100%]",
119
+ "bg-[linear-gradient(90deg,transparent_0%,transparent_40%,color-mix(in_oklch,var(--foreground)_75%,transparent)_56%,transparent_80%,transparent_100%)]",
120
+ )}
121
+ >
122
+ Reasoning
123
+ </span>
124
+ ) : null}
125
+ </span>
126
+ <ChevronDownIcon
127
+ className={cn(
128
+ "aui-reasoning-trigger-chevron mt-0.5 size-4 shrink-0",
129
+ "transition-transform duration-(--animation-duration) ease-out",
130
+ "group-data-[state=closed]/trigger:-rotate-90",
131
+ "group-data-[state=open]/trigger:rotate-0",
132
+ )}
133
+ />
134
+ </CollapsibleTrigger>
135
+ );
136
+
137
+ /**
138
+ * Collapsible content wrapper that handles height expand/collapse animation.
139
+ * Animation: Height animates up (collapse) and down (expand).
140
+ * Also provides group context for child animations via data-state attributes.
141
+ */
142
+ const ReasoningContent: FC<
143
+ PropsWithChildren<{
144
+ className?: string;
145
+ "aria-busy"?: boolean;
146
+ }>
147
+ > = ({ className, children, "aria-busy": ariaBusy }) => (
148
+ <CollapsibleContent
149
+ className={cn(
150
+ "aui-reasoning-content relative overflow-hidden text-sm text-muted-foreground outline-none",
151
+ "group/collapsible-content ease-out",
152
+ "data-[state=closed]:animate-collapsible-up",
153
+ "data-[state=open]:animate-collapsible-down",
154
+ "data-[state=closed]:fill-mode-forwards",
155
+ "data-[state=closed]:pointer-events-none",
156
+ "data-[state=open]:duration-(--animation-duration)",
157
+ "data-[state=closed]:duration-(--animation-duration)",
158
+ className,
159
+ )}
160
+ aria-busy={ariaBusy}
161
+ >
162
+ {children}
163
+ <GradientFade />
164
+ </CollapsibleContent>
165
+ );
166
+
167
+ ReasoningContent.displayName = "ReasoningContent";
168
+
169
+ /**
170
+ * Text content wrapper that animates the reasoning text visibility.
171
+ * Animation: Slides in from top + fades in when opening, reverses when closing.
172
+ * Reacts to parent ReasoningContent's data-state via Radix group selectors.
173
+ */
174
+ const ReasoningText: FC<
175
+ PropsWithChildren<{
176
+ className?: string;
177
+ }>
178
+ > = ({ className, children }) => (
179
+ <div
180
+ className={cn(
181
+ "aui-reasoning-text relative z-0 space-y-4 pt-4 pl-6 leading-relaxed",
182
+ "transform-gpu transition-[transform,opacity]",
183
+ "group-data-[state=open]/collapsible-content:animate-in",
184
+ "group-data-[state=closed]/collapsible-content:animate-out",
185
+ "group-data-[state=open]/collapsible-content:fade-in-0",
186
+ "group-data-[state=closed]/collapsible-content:fade-out-0",
187
+ "group-data-[state=open]/collapsible-content:slide-in-from-top-4",
188
+ "group-data-[state=closed]/collapsible-content:slide-out-to-top-4",
189
+ "group-data-[state=open]/collapsible-content:duration-(--animation-duration)",
190
+ "group-data-[state=closed]/collapsible-content:duration-(--animation-duration)",
191
+ "[&_p]:-mb-2",
192
+ className,
193
+ )}
194
+ >
195
+ {children}
196
+ </div>
197
+ );
198
+
199
+ ReasoningText.displayName = "ReasoningText";
200
+
201
+ /**
202
+ * Renders a single reasoning part's text with markdown support.
203
+ * Consecutive reasoning parts are automatically grouped by ReasoningGroup.
204
+ *
205
+ * Pass Reasoning to MessagePrimitive.Parts in thread.tsx
206
+ *
207
+ * @example:
208
+ * ```tsx
209
+ * <MessagePrimitive.Parts
210
+ * components={{
211
+ * Reasoning: Reasoning,
212
+ * ReasoningGroup: ReasoningGroup,
213
+ * }}
214
+ * />
215
+ * ```
216
+ */
217
+ const ReasoningImpl: ReasoningMessagePartComponent = () => <MarkdownText />;
218
+
219
+ /**
220
+ * Collapsible wrapper that groups consecutive reasoning parts together.
221
+ * Includes scroll lock to prevent page jumps during collapse animation.
222
+ *
223
+ * Pass ReasoningGroup to MessagePrimitive.Parts in thread.tsx
224
+ *
225
+ * @example:
226
+ * ```tsx
227
+ * <MessagePrimitive.Parts
228
+ * components={{
229
+ * Reasoning: Reasoning,
230
+ * ReasoningGroup: ReasoningGroup,
231
+ * }}
232
+ * />
233
+ * ```
234
+ */
235
+ const ReasoningGroupImpl: ReasoningGroupComponent = ({
236
+ children,
237
+ startIndex,
238
+ endIndex,
239
+ }) => {
240
+ /**
241
+ * Detects if reasoning is currently streaming within this group's range.
242
+ */
243
+ const isReasoningStreaming = useAssistantState(({ message }) => {
244
+ if (message.status?.type !== "running") return false;
245
+ const lastIndex = message.parts.length - 1;
246
+ if (lastIndex < 0) return false;
247
+ const lastType = message.parts[lastIndex]?.type;
248
+ if (lastType !== "reasoning") return false;
249
+ return lastIndex >= startIndex && lastIndex <= endIndex;
250
+ });
251
+
252
+ return (
253
+ <ReasoningRoot>
254
+ <ReasoningTrigger active={isReasoningStreaming} />
255
+
256
+ <ReasoningContent aria-busy={isReasoningStreaming}>
257
+ <ReasoningText>{children}</ReasoningText>
258
+ </ReasoningContent>
259
+ </ReasoningRoot>
260
+ );
261
+ };
262
+
263
+ export const Reasoning = memo(ReasoningImpl);
264
+ Reasoning.displayName = "Reasoning";
265
+
266
+ export const ReasoningGroup = memo(ReasoningGroupImpl);
267
+ ReasoningGroup.displayName = "ReasoningGroup";
components/assistant-ui/thread-list.tsx ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { FC } from "react";
2
+ import {
3
+ ThreadListItemPrimitive,
4
+ ThreadListPrimitive,
5
+ useAssistantState,
6
+ } from "@assistant-ui/react";
7
+ import { ArchiveIcon, PlusIcon } from "lucide-react";
8
+
9
+ import { Button } from "@/components/ui/button";
10
+ import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
11
+ import { Skeleton } from "@/components/ui/skeleton";
12
+
13
+ export const ThreadList: FC = () => {
14
+ return (
15
+ <ThreadListPrimitive.Root className="aui-root aui-thread-list-root flex flex-col items-stretch gap-1.5">
16
+ <ThreadListNew />
17
+ <ThreadListItems />
18
+ </ThreadListPrimitive.Root>
19
+ );
20
+ };
21
+
22
+ const ThreadListNew: FC = () => {
23
+ return (
24
+ <ThreadListPrimitive.New asChild>
25
+ <Button
26
+ className="aui-thread-list-new flex items-center justify-start gap-1 rounded-lg px-2.5 py-2 text-start hover:bg-muted data-active:bg-muted"
27
+ variant="ghost"
28
+ >
29
+ <PlusIcon />
30
+ New Thread
31
+ </Button>
32
+ </ThreadListPrimitive.New>
33
+ );
34
+ };
35
+
36
+ const ThreadListItems: FC = () => {
37
+ const isLoading = useAssistantState(({ threads }) => threads.isLoading);
38
+
39
+ if (isLoading) {
40
+ return <ThreadListSkeleton />;
41
+ }
42
+
43
+ return <ThreadListPrimitive.Items components={{ ThreadListItem }} />;
44
+ };
45
+
46
+ const ThreadListSkeleton: FC = () => {
47
+ return (
48
+ <>
49
+ {Array.from({ length: 5 }, (_, i) => (
50
+ <div
51
+ key={i}
52
+ role="status"
53
+ aria-label="Loading threads"
54
+ aria-live="polite"
55
+ className="aui-thread-list-skeleton-wrapper flex items-center gap-2 rounded-md px-3 py-2"
56
+ >
57
+ <Skeleton className="aui-thread-list-skeleton h-[22px] flex-grow" />
58
+ </div>
59
+ ))}
60
+ </>
61
+ );
62
+ };
63
+
64
+ const ThreadListItem: FC = () => {
65
+ return (
66
+ <ThreadListItemPrimitive.Root className="aui-thread-list-item flex items-center gap-2 rounded-lg transition-all hover:bg-muted focus-visible:bg-muted focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none data-active:bg-muted">
67
+ <ThreadListItemPrimitive.Trigger className="aui-thread-list-item-trigger flex-grow px-3 py-2 text-start">
68
+ <ThreadListItemTitle />
69
+ </ThreadListItemPrimitive.Trigger>
70
+ <ThreadListItemArchive />
71
+ </ThreadListItemPrimitive.Root>
72
+ );
73
+ };
74
+
75
+ const ThreadListItemTitle: FC = () => {
76
+ return (
77
+ <span className="aui-thread-list-item-title text-sm">
78
+ <ThreadListItemPrimitive.Title fallback="New Chat" />
79
+ </span>
80
+ );
81
+ };
82
+
83
+ const ThreadListItemArchive: FC = () => {
84
+ return (
85
+ <ThreadListItemPrimitive.Archive asChild>
86
+ <TooltipIconButton
87
+ className="aui-thread-list-item-archive mr-3 ml-auto size-4 p-0 text-foreground hover:text-primary"
88
+ variant="ghost"
89
+ tooltip="Archive thread"
90
+ >
91
+ <ArchiveIcon />
92
+ </TooltipIconButton>
93
+ </ThreadListItemPrimitive.Archive>
94
+ );
95
+ };
components/assistant-ui/thread.tsx ADDED
@@ -0,0 +1,392 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ ArrowDownIcon,
3
+ ArrowUpIcon,
4
+ CheckIcon,
5
+ ChevronLeftIcon,
6
+ ChevronRightIcon,
7
+ CopyIcon,
8
+ PencilIcon,
9
+ RefreshCwIcon,
10
+ Square,
11
+ } from "lucide-react";
12
+
13
+ import {
14
+ ActionBarPrimitive,
15
+ BranchPickerPrimitive,
16
+ ComposerPrimitive,
17
+ ErrorPrimitive,
18
+ MessagePrimitive,
19
+ ThreadPrimitive,
20
+ } from "@assistant-ui/react";
21
+
22
+ import type { FC } from "react";
23
+ import { LazyMotion, MotionConfig, domAnimation } from "motion/react";
24
+ import * as m from "motion/react-m";
25
+
26
+ import { Button } from "@/components/ui/button";
27
+ import { MarkdownText } from "@/components/assistant-ui/markdown-text";
28
+ import { Reasoning, ReasoningGroup } from "@/components/assistant-ui/reasoning";
29
+ import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
30
+ import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
31
+ import {
32
+ ComposerAddAttachment,
33
+ ComposerAttachments,
34
+ UserMessageAttachments,
35
+ } from "@/components/assistant-ui/attachment";
36
+
37
+ import { cn } from "@/lib/utils";
38
+
39
+ export const Thread: FC = () => {
40
+ return (
41
+ <LazyMotion features={domAnimation}>
42
+ <MotionConfig reducedMotion="user">
43
+ <ThreadPrimitive.Root
44
+ className="aui-root aui-thread-root @container flex h-full flex-col bg-background"
45
+ style={{
46
+ ["--thread-max-width" as string]: "44rem",
47
+ }}
48
+ >
49
+ <ThreadPrimitive.Viewport className="aui-thread-viewport relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll px-4">
50
+ <ThreadPrimitive.If empty>
51
+ <ThreadWelcome />
52
+ </ThreadPrimitive.If>
53
+
54
+ <ThreadPrimitive.Messages
55
+ components={{
56
+ UserMessage,
57
+ EditComposer,
58
+ AssistantMessage,
59
+ }}
60
+ />
61
+
62
+ <ThreadPrimitive.If empty={false}>
63
+ <div className="aui-thread-viewport-spacer min-h-8 grow" />
64
+ </ThreadPrimitive.If>
65
+
66
+ <Composer />
67
+ </ThreadPrimitive.Viewport>
68
+ </ThreadPrimitive.Root>
69
+ </MotionConfig>
70
+ </LazyMotion>
71
+ );
72
+ };
73
+
74
+ const ThreadScrollToBottom: FC = () => {
75
+ return (
76
+ <ThreadPrimitive.ScrollToBottom asChild>
77
+ <TooltipIconButton
78
+ tooltip="Scroll to bottom"
79
+ variant="outline"
80
+ className="aui-thread-scroll-to-bottom absolute -top-12 z-10 self-center rounded-full p-4 disabled:invisible dark:bg-background dark:hover:bg-accent"
81
+ >
82
+ <ArrowDownIcon />
83
+ </TooltipIconButton>
84
+ </ThreadPrimitive.ScrollToBottom>
85
+ );
86
+ };
87
+
88
+ const ThreadWelcome: FC = () => {
89
+ return (
90
+ <div className="aui-thread-welcome-root mx-auto my-auto flex w-full max-w-[var(--thread-max-width)] flex-grow flex-col">
91
+ <div className="aui-thread-welcome-center flex w-full flex-grow flex-col items-center justify-center">
92
+ <div className="aui-thread-welcome-message flex size-full flex-col justify-center px-8">
93
+ <m.div
94
+ initial={{ opacity: 0, y: 10 }}
95
+ animate={{ opacity: 1, y: 0 }}
96
+ exit={{ opacity: 0, y: 10 }}
97
+ className="aui-thread-welcome-message-motion-1 text-2xl font-semibold"
98
+ >
99
+ Hello there!
100
+ </m.div>
101
+ <m.div
102
+ initial={{ opacity: 0, y: 10 }}
103
+ animate={{ opacity: 1, y: 0 }}
104
+ exit={{ opacity: 0, y: 10 }}
105
+ transition={{ delay: 0.1 }}
106
+ className="aui-thread-welcome-message-motion-2 text-2xl text-muted-foreground/65"
107
+ >
108
+ How can I help you today?
109
+ </m.div>
110
+ </div>
111
+ </div>
112
+ <ThreadSuggestions />
113
+ </div>
114
+ );
115
+ };
116
+
117
+ const ThreadSuggestions: FC = () => {
118
+ return (
119
+ <div className="aui-thread-welcome-suggestions grid w-full gap-2 pb-4 @md:grid-cols-2">
120
+ {[
121
+ {
122
+ title: "What's the weather",
123
+ label: "in San Francisco?",
124
+ action: "What's the weather in San Francisco?",
125
+ },
126
+ {
127
+ title: "Explain React hooks",
128
+ label: "like useState and useEffect",
129
+ action: "Explain React hooks like useState and useEffect",
130
+ },
131
+ {
132
+ title: "Write a SQL query",
133
+ label: "to find top customers",
134
+ action: "Write a SQL query to find top customers",
135
+ },
136
+ {
137
+ title: "Create a meal plan",
138
+ label: "for healthy weight loss",
139
+ action: "Create a meal plan for healthy weight loss",
140
+ },
141
+ ].map((suggestedAction, index) => (
142
+ <m.div
143
+ initial={{ opacity: 0, y: 20 }}
144
+ animate={{ opacity: 1, y: 0 }}
145
+ exit={{ opacity: 0, y: 20 }}
146
+ transition={{ delay: 0.05 * index }}
147
+ key={`suggested-action-${suggestedAction.title}-${index}`}
148
+ className="aui-thread-welcome-suggestion-display [&:nth-child(n+3)]:hidden @md:[&:nth-child(n+3)]:block"
149
+ >
150
+ <ThreadPrimitive.Suggestion
151
+ prompt={suggestedAction.action}
152
+ send
153
+ asChild
154
+ >
155
+ <Button
156
+ variant="ghost"
157
+ className="aui-thread-welcome-suggestion h-auto w-full flex-1 flex-wrap items-start justify-start gap-1 rounded-3xl border px-5 py-4 text-left text-sm @md:flex-col dark:hover:bg-accent/60"
158
+ aria-label={suggestedAction.action}
159
+ >
160
+ <span className="aui-thread-welcome-suggestion-text-1 font-medium">
161
+ {suggestedAction.title}
162
+ </span>
163
+ <span className="aui-thread-welcome-suggestion-text-2 text-muted-foreground">
164
+ {suggestedAction.label}
165
+ </span>
166
+ </Button>
167
+ </ThreadPrimitive.Suggestion>
168
+ </m.div>
169
+ ))}
170
+ </div>
171
+ );
172
+ };
173
+
174
+ const Composer: FC = () => {
175
+ return (
176
+ <div className="aui-composer-wrapper sticky bottom-0 mx-auto flex w-full max-w-[var(--thread-max-width)] flex-col gap-4 overflow-visible rounded-t-3xl bg-background pb-4 md:pb-6">
177
+ <ThreadScrollToBottom />
178
+ <ComposerPrimitive.Root className="aui-composer-root relative flex w-full flex-col">
179
+ <ComposerPrimitive.AttachmentDropzone className="aui-composer-attachment-dropzone group/input-group flex w-full flex-col rounded-3xl border border-input bg-background px-1 pt-2 shadow-xs transition-[color,box-shadow] outline-none has-[textarea:focus-visible]:border-ring has-[textarea:focus-visible]:ring-[3px] has-[textarea:focus-visible]:ring-ring/50 data-[dragging=true]:border-dashed data-[dragging=true]:border-ring data-[dragging=true]:bg-accent/50 dark:bg-background">
180
+ <ComposerAttachments />
181
+ <ComposerPrimitive.Input
182
+ placeholder="Send a message..."
183
+ className="aui-composer-input mb-1 max-h-32 min-h-16 w-full resize-none bg-transparent px-3.5 pt-1.5 pb-3 text-base outline-none placeholder:text-muted-foreground focus-visible:ring-0"
184
+ rows={1}
185
+ autoFocus
186
+ aria-label="Message input"
187
+ />
188
+ <ComposerAction />
189
+ </ComposerPrimitive.AttachmentDropzone>
190
+ </ComposerPrimitive.Root>
191
+ </div>
192
+ );
193
+ };
194
+
195
+ const ComposerAction: FC = () => {
196
+ return (
197
+ <div className="aui-composer-action-wrapper relative mx-1 mt-2 mb-2 flex items-center justify-between">
198
+ <ComposerAddAttachment />
199
+
200
+ <ThreadPrimitive.If running={false}>
201
+ <ComposerPrimitive.Send asChild>
202
+ <TooltipIconButton
203
+ tooltip="Send message"
204
+ side="bottom"
205
+ type="submit"
206
+ variant="default"
207
+ size="icon"
208
+ className="aui-composer-send size-[34px] rounded-full p-1"
209
+ aria-label="Send message"
210
+ >
211
+ <ArrowUpIcon className="aui-composer-send-icon size-5" />
212
+ </TooltipIconButton>
213
+ </ComposerPrimitive.Send>
214
+ </ThreadPrimitive.If>
215
+
216
+ <ThreadPrimitive.If running>
217
+ <ComposerPrimitive.Cancel asChild>
218
+ <Button
219
+ type="button"
220
+ variant="default"
221
+ size="icon"
222
+ className="aui-composer-cancel size-[34px] rounded-full border border-muted-foreground/60 hover:bg-primary/75 dark:border-muted-foreground/90"
223
+ aria-label="Stop generating"
224
+ >
225
+ <Square className="aui-composer-cancel-icon size-3.5 fill-white dark:fill-black" />
226
+ </Button>
227
+ </ComposerPrimitive.Cancel>
228
+ </ThreadPrimitive.If>
229
+ </div>
230
+ );
231
+ };
232
+
233
+ const MessageError: FC = () => {
234
+ return (
235
+ <MessagePrimitive.Error>
236
+ <ErrorPrimitive.Root className="aui-message-error-root mt-2 rounded-md border border-destructive bg-destructive/10 p-3 text-sm text-destructive dark:bg-destructive/5 dark:text-red-200">
237
+ <ErrorPrimitive.Message className="aui-message-error-message line-clamp-2" />
238
+ </ErrorPrimitive.Root>
239
+ </MessagePrimitive.Error>
240
+ );
241
+ };
242
+
243
+ const AssistantMessage: FC = () => {
244
+ return (
245
+ <MessagePrimitive.Root asChild>
246
+ <div
247
+ className="aui-assistant-message-root relative mx-auto w-full max-w-[var(--thread-max-width)] animate-in py-4 duration-150 ease-out fade-in slide-in-from-bottom-1 last:mb-24"
248
+ data-role="assistant"
249
+ >
250
+ <div className="aui-assistant-message-content mx-2 leading-7 break-words text-foreground">
251
+ <MessagePrimitive.Parts
252
+ components={{
253
+ Text: MarkdownText,
254
+ Reasoning: Reasoning,
255
+ ReasoningGroup: ReasoningGroup,
256
+ tools: { Fallback: ToolFallback },
257
+ }}
258
+ />
259
+ <MessageError />
260
+ </div>
261
+
262
+ <div className="aui-assistant-message-footer mt-2 ml-2 flex">
263
+ <BranchPicker />
264
+ <AssistantActionBar />
265
+ </div>
266
+ </div>
267
+ </MessagePrimitive.Root>
268
+ );
269
+ };
270
+
271
+ const AssistantActionBar: FC = () => {
272
+ return (
273
+ <ActionBarPrimitive.Root
274
+ hideWhenRunning
275
+ autohide="not-last"
276
+ autohideFloat="single-branch"
277
+ className="aui-assistant-action-bar-root col-start-3 row-start-2 -ml-1 flex gap-1 text-muted-foreground data-floating:absolute data-floating:rounded-md data-floating:border data-floating:bg-background data-floating:p-1 data-floating:shadow-sm"
278
+ >
279
+ <ActionBarPrimitive.Copy asChild>
280
+ <TooltipIconButton tooltip="Copy">
281
+ <MessagePrimitive.If copied>
282
+ <CheckIcon />
283
+ </MessagePrimitive.If>
284
+ <MessagePrimitive.If copied={false}>
285
+ <CopyIcon />
286
+ </MessagePrimitive.If>
287
+ </TooltipIconButton>
288
+ </ActionBarPrimitive.Copy>
289
+ <ActionBarPrimitive.Reload asChild>
290
+ <TooltipIconButton tooltip="Refresh">
291
+ <RefreshCwIcon />
292
+ </TooltipIconButton>
293
+ </ActionBarPrimitive.Reload>
294
+ </ActionBarPrimitive.Root>
295
+ );
296
+ };
297
+
298
+ const UserMessage: FC = () => {
299
+ return (
300
+ <MessagePrimitive.Root asChild>
301
+ <div
302
+ className="aui-user-message-root mx-auto grid w-full max-w-[var(--thread-max-width)] animate-in auto-rows-auto grid-cols-[minmax(72px,1fr)_auto] gap-y-2 px-2 py-4 duration-150 ease-out fade-in slide-in-from-bottom-1 first:mt-3 last:mb-5 [&:where(>*)]:col-start-2"
303
+ data-role="user"
304
+ >
305
+ <UserMessageAttachments />
306
+
307
+ <div className="aui-user-message-content-wrapper relative col-start-2 min-w-0">
308
+ <div className="aui-user-message-content rounded-3xl bg-muted px-5 py-2.5 break-words text-foreground">
309
+ <MessagePrimitive.Parts />
310
+ </div>
311
+ <div className="aui-user-action-bar-wrapper absolute top-1/2 left-0 -translate-x-full -translate-y-1/2 pr-2">
312
+ <UserActionBar />
313
+ </div>
314
+ </div>
315
+
316
+ <BranchPicker className="aui-user-branch-picker col-span-full col-start-1 row-start-3 -mr-1 justify-end" />
317
+ </div>
318
+ </MessagePrimitive.Root>
319
+ );
320
+ };
321
+
322
+ const UserActionBar: FC = () => {
323
+ return (
324
+ <ActionBarPrimitive.Root
325
+ hideWhenRunning
326
+ autohide="not-last"
327
+ className="aui-user-action-bar-root flex flex-col items-end"
328
+ >
329
+ <ActionBarPrimitive.Edit asChild>
330
+ <TooltipIconButton tooltip="Edit" className="aui-user-action-edit p-4">
331
+ <PencilIcon />
332
+ </TooltipIconButton>
333
+ </ActionBarPrimitive.Edit>
334
+ </ActionBarPrimitive.Root>
335
+ );
336
+ };
337
+
338
+ const EditComposer: FC = () => {
339
+ return (
340
+ <div className="aui-edit-composer-wrapper mx-auto flex w-full max-w-[var(--thread-max-width)] flex-col gap-4 px-2 first:mt-4">
341
+ <ComposerPrimitive.Root className="aui-edit-composer-root ml-auto flex w-full max-w-7/8 flex-col rounded-xl bg-muted">
342
+ <ComposerPrimitive.Input
343
+ className="aui-edit-composer-input flex min-h-[60px] w-full resize-none bg-transparent p-4 text-foreground outline-none"
344
+ autoFocus
345
+ />
346
+
347
+ <div className="aui-edit-composer-footer mx-3 mb-3 flex items-center justify-center gap-2 self-end">
348
+ <ComposerPrimitive.Cancel asChild>
349
+ <Button variant="ghost" size="sm" aria-label="Cancel edit">
350
+ Cancel
351
+ </Button>
352
+ </ComposerPrimitive.Cancel>
353
+ <ComposerPrimitive.Send asChild>
354
+ <Button size="sm" aria-label="Update message">
355
+ Update
356
+ </Button>
357
+ </ComposerPrimitive.Send>
358
+ </div>
359
+ </ComposerPrimitive.Root>
360
+ </div>
361
+ );
362
+ };
363
+
364
+ const BranchPicker: FC<BranchPickerPrimitive.Root.Props> = ({
365
+ className,
366
+ ...rest
367
+ }) => {
368
+ return (
369
+ <BranchPickerPrimitive.Root
370
+ hideWhenSingleBranch
371
+ className={cn(
372
+ "aui-branch-picker-root mr-2 -ml-2 inline-flex items-center text-xs text-muted-foreground",
373
+ className,
374
+ )}
375
+ {...rest}
376
+ >
377
+ <BranchPickerPrimitive.Previous asChild>
378
+ <TooltipIconButton tooltip="Previous">
379
+ <ChevronLeftIcon />
380
+ </TooltipIconButton>
381
+ </BranchPickerPrimitive.Previous>
382
+ <span className="aui-branch-picker-state font-medium">
383
+ <BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />
384
+ </span>
385
+ <BranchPickerPrimitive.Next asChild>
386
+ <TooltipIconButton tooltip="Next">
387
+ <ChevronRightIcon />
388
+ </TooltipIconButton>
389
+ </BranchPickerPrimitive.Next>
390
+ </BranchPickerPrimitive.Root>
391
+ );
392
+ };
components/assistant-ui/threadlist-sidebar.tsx ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 "@/components/assistant-ui/thread-list";
15
+
16
+ export function ThreadListSidebar({
17
+ ...props
18
+ }: React.ComponentProps<typeof Sidebar>) {
19
+ return (
20
+ <Sidebar {...props}>
21
+ <SidebarHeader className="aui-sidebar-header mb-2 border-b">
22
+ <div className="aui-sidebar-header-content flex items-center justify-between">
23
+ <SidebarMenu>
24
+ <SidebarMenuItem>
25
+ <SidebarMenuButton size="lg" asChild>
26
+ <Link
27
+ href="https://assistant-ui.com"
28
+ target="_blank"
29
+ rel="noopener noreferrer"
30
+ >
31
+ <div className="aui-sidebar-header-icon-wrapper flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
32
+ <MessagesSquare className="aui-sidebar-header-icon size-4" />
33
+ </div>
34
+ <div className="aui-sidebar-header-heading mr-6 flex flex-col gap-0.5 leading-none">
35
+ <span className="aui-sidebar-header-title font-semibold">
36
+ assistant-ui
37
+ </span>
38
+ </div>
39
+ </Link>
40
+ </SidebarMenuButton>
41
+ </SidebarMenuItem>
42
+ </SidebarMenu>
43
+ </div>
44
+ </SidebarHeader>
45
+ <SidebarContent className="aui-sidebar-content px-2">
46
+ <ThreadList />
47
+ </SidebarContent>
48
+ <SidebarRail />
49
+ <SidebarFooter className="aui-sidebar-footer border-t">
50
+ <SidebarMenu>
51
+ <SidebarMenuItem>
52
+ <SidebarMenuButton size="lg" asChild>
53
+ <Link
54
+ href="https://github.com/assistant-ui/assistant-ui"
55
+ target="_blank"
56
+ >
57
+ <div className="aui-sidebar-footer-icon-wrapper flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
58
+ <Github className="aui-sidebar-footer-icon size-4" />
59
+ </div>
60
+ <div className="aui-sidebar-footer-heading flex flex-col gap-0.5 leading-none">
61
+ <span className="aui-sidebar-footer-title font-semibold">
62
+ GitHub
63
+ </span>
64
+ <span>View Source</span>
65
+ </div>
66
+ </Link>
67
+ </SidebarMenuButton>
68
+ </SidebarMenuItem>
69
+ </SidebarMenu>
70
+ </SidebarFooter>
71
+ </Sidebar>
72
+ );
73
+ }
components/assistant-ui/tool-fallback.tsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
2
+ import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
3
+ import { useState } from "react";
4
+ import { Button } from "@/components/ui/button";
5
+
6
+ export const ToolFallback: ToolCallMessagePartComponent = ({
7
+ toolName,
8
+ argsText,
9
+ result,
10
+ }) => {
11
+ const [isCollapsed, setIsCollapsed] = useState(true);
12
+ return (
13
+ <div className="aui-tool-fallback-root mb-4 flex w-full flex-col gap-3 rounded-lg border py-3">
14
+ <div className="aui-tool-fallback-header flex items-center gap-2 px-4">
15
+ <CheckIcon className="aui-tool-fallback-icon size-4" />
16
+ <p className="aui-tool-fallback-title flex-grow">
17
+ Used tool: <b>{toolName}</b>
18
+ </p>
19
+ <Button onClick={() => setIsCollapsed(!isCollapsed)}>
20
+ {isCollapsed ? <ChevronUpIcon /> : <ChevronDownIcon />}
21
+ </Button>
22
+ </div>
23
+ {!isCollapsed && (
24
+ <div className="aui-tool-fallback-content flex flex-col gap-2 border-t pt-2">
25
+ <div className="aui-tool-fallback-args-root px-4">
26
+ <pre className="aui-tool-fallback-args-value whitespace-pre-wrap">
27
+ {argsText}
28
+ </pre>
29
+ </div>
30
+ {result !== undefined && (
31
+ <div className="aui-tool-fallback-result-root border-t border-dashed px-4 pt-2">
32
+ <p className="aui-tool-fallback-result-header font-semibold">
33
+ Result:
34
+ </p>
35
+ <pre className="aui-tool-fallback-result-content whitespace-pre-wrap">
36
+ {typeof result === "string"
37
+ ? result
38
+ : JSON.stringify(result, null, 2)}
39
+ </pre>
40
+ </div>
41
+ )}
42
+ </div>
43
+ )}
44
+ </div>
45
+ );
46
+ };
components/assistant-ui/tooltip-icon-button.tsx ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { ComponentPropsWithRef, forwardRef } from "react";
4
+ import { Slottable } from "@radix-ui/react-slot";
5
+
6
+ import {
7
+ Tooltip,
8
+ TooltipContent,
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 = ComponentPropsWithRef<typeof Button> & {
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
+ <Tooltip>
25
+ <TooltipTrigger asChild>
26
+ <Button
27
+ variant="ghost"
28
+ size="icon"
29
+ {...rest}
30
+ className={cn("aui-button-icon size-6 p-1", className)}
31
+ ref={ref}
32
+ >
33
+ <Slottable>{children}</Slottable>
34
+ <span className="aui-sr-only sr-only">{tooltip}</span>
35
+ </Button>
36
+ </TooltipTrigger>
37
+ <TooltipContent side={side}>{tooltip}</TooltipContent>
38
+ </Tooltip>
39
+ );
40
+ });
41
+
42
+ TooltipIconButton.displayName = "TooltipIconButton";
components/ui/avatar.tsx ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as AvatarPrimitive from "@radix-ui/react-avatar";
5
+
6
+ import { cn } from "@/lib/utils";
7
+
8
+ function Avatar({
9
+ className,
10
+ ...props
11
+ }: React.ComponentProps<typeof AvatarPrimitive.Root>) {
12
+ return (
13
+ <AvatarPrimitive.Root
14
+ data-slot="avatar"
15
+ className={cn(
16
+ "relative flex size-8 shrink-0 overflow-hidden rounded-full",
17
+ className,
18
+ )}
19
+ {...props}
20
+ />
21
+ );
22
+ }
23
+
24
+ function AvatarImage({
25
+ className,
26
+ ...props
27
+ }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
28
+ return (
29
+ <AvatarPrimitive.Image
30
+ data-slot="avatar-image"
31
+ className={cn("aspect-square size-full", className)}
32
+ {...props}
33
+ />
34
+ );
35
+ }
36
+
37
+ function AvatarFallback({
38
+ className,
39
+ ...props
40
+ }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
41
+ return (
42
+ <AvatarPrimitive.Fallback
43
+ data-slot="avatar-fallback"
44
+ className={cn(
45
+ "flex size-full items-center justify-center rounded-full bg-muted",
46
+ className,
47
+ )}
48
+ {...props}
49
+ />
50
+ );
51
+ }
52
+
53
+ export { Avatar, AvatarImage, AvatarFallback };
components/ui/breadcrumb.tsx ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
9
+ }
10
+
11
+ function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
12
+ return (
13
+ <ol
14
+ data-slot="breadcrumb-list"
15
+ className={cn(
16
+ "flex flex-wrap items-center gap-1.5 text-sm break-words text-muted-foreground sm:gap-2.5",
17
+ className,
18
+ )}
19
+ {...props}
20
+ />
21
+ );
22
+ }
23
+
24
+ function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
25
+ return (
26
+ <li
27
+ data-slot="breadcrumb-item"
28
+ className={cn("inline-flex items-center gap-1.5", className)}
29
+ {...props}
30
+ />
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
+ <Comp
45
+ data-slot="breadcrumb-link"
46
+ className={cn("transition-colors hover:text-foreground", className)}
47
+ {...props}
48
+ />
49
+ );
50
+ }
51
+
52
+ function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
53
+ return (
54
+ <span
55
+ data-slot="breadcrumb-page"
56
+ role="link"
57
+ aria-disabled="true"
58
+ aria-current="page"
59
+ className={cn("font-normal text-foreground", className)}
60
+ {...props}
61
+ />
62
+ );
63
+ }
64
+
65
+ function BreadcrumbSeparator({
66
+ children,
67
+ className,
68
+ ...props
69
+ }: React.ComponentProps<"li">) {
70
+ return (
71
+ <li
72
+ data-slot="breadcrumb-separator"
73
+ role="presentation"
74
+ aria-hidden="true"
75
+ className={cn("[&>svg]:size-3.5", className)}
76
+ {...props}
77
+ >
78
+ {children ?? <ChevronRight />}
79
+ </li>
80
+ );
81
+ }
82
+
83
+ function BreadcrumbEllipsis({
84
+ className,
85
+ ...props
86
+ }: React.ComponentProps<"span">) {
87
+ return (
88
+ <span
89
+ data-slot="breadcrumb-ellipsis"
90
+ role="presentation"
91
+ aria-hidden="true"
92
+ className={cn("flex size-9 items-center justify-center", className)}
93
+ {...props}
94
+ >
95
+ <MoreHorizontal className="size-4" />
96
+ <span className="sr-only">More</span>
97
+ </span>
98
+ );
99
+ }
100
+
101
+ export {
102
+ Breadcrumb,
103
+ BreadcrumbList,
104
+ BreadcrumbItem,
105
+ BreadcrumbLink,
106
+ BreadcrumbPage,
107
+ BreadcrumbSeparator,
108
+ BreadcrumbEllipsis,
109
+ };
components/ui/button.tsx ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
15
+ outline:
16
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost:
20
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ },
23
+ size: {
24
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
25
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
26
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
27
+ icon: "size-9",
28
+ "icon-sm": "size-8",
29
+ "icon-lg": "size-10",
30
+ },
31
+ },
32
+ defaultVariants: {
33
+ variant: "default",
34
+ size: "default",
35
+ },
36
+ },
37
+ );
38
+
39
+ function Button({
40
+ className,
41
+ variant,
42
+ size,
43
+ asChild = false,
44
+ ...props
45
+ }: React.ComponentProps<"button"> &
46
+ VariantProps<typeof buttonVariants> & {
47
+ asChild?: boolean;
48
+ }) {
49
+ const Comp = asChild ? Slot : "button";
50
+
51
+ return (
52
+ <Comp
53
+ data-slot="button"
54
+ className={cn(buttonVariants({ variant, size, className }))}
55
+ {...props}
56
+ />
57
+ );
58
+ }
59
+
60
+ export { Button, buttonVariants };
components/ui/collapsible.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
4
+
5
+ function Collapsible({
6
+ ...props
7
+ }: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
8
+ return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
9
+ }
10
+
11
+ function CollapsibleTrigger({
12
+ ...props
13
+ }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
14
+ return (
15
+ <CollapsiblePrimitive.CollapsibleTrigger
16
+ data-slot="collapsible-trigger"
17
+ {...props}
18
+ />
19
+ );
20
+ }
21
+
22
+ function CollapsibleContent({
23
+ ...props
24
+ }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
25
+ return (
26
+ <CollapsiblePrimitive.CollapsibleContent
27
+ data-slot="collapsible-content"
28
+ {...props}
29
+ />
30
+ );
31
+ }
32
+
33
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent };
components/ui/dialog.tsx ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
5
+ import { XIcon } from "lucide-react";
6
+
7
+ import { cn } from "@/lib/utils";
8
+
9
+ function Dialog({
10
+ ...props
11
+ }: React.ComponentProps<typeof DialogPrimitive.Root>) {
12
+ return <DialogPrimitive.Root data-slot="dialog" {...props} />;
13
+ }
14
+
15
+ function DialogTrigger({
16
+ ...props
17
+ }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
18
+ return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
19
+ }
20
+
21
+ function DialogPortal({
22
+ ...props
23
+ }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
24
+ return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
25
+ }
26
+
27
+ function DialogClose({
28
+ ...props
29
+ }: React.ComponentProps<typeof DialogPrimitive.Close>) {
30
+ return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
31
+ }
32
+
33
+ function DialogOverlay({
34
+ className,
35
+ ...props
36
+ }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
37
+ return (
38
+ <DialogPrimitive.Overlay
39
+ data-slot="dialog-overlay"
40
+ className={cn(
41
+ "fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
42
+ className,
43
+ )}
44
+ {...props}
45
+ />
46
+ );
47
+ }
48
+
49
+ function DialogContent({
50
+ className,
51
+ children,
52
+ showCloseButton = true,
53
+ ...props
54
+ }: React.ComponentProps<typeof DialogPrimitive.Content> & {
55
+ showCloseButton?: boolean;
56
+ }) {
57
+ return (
58
+ <DialogPortal data-slot="dialog-portal">
59
+ <DialogOverlay />
60
+ <DialogPrimitive.Content
61
+ data-slot="dialog-content"
62
+ className={cn(
63
+ "fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg",
64
+ className,
65
+ )}
66
+ {...props}
67
+ >
68
+ {children}
69
+ {showCloseButton && (
70
+ <DialogPrimitive.Close
71
+ data-slot="dialog-close"
72
+ className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
73
+ >
74
+ <XIcon />
75
+ <span className="sr-only">Close</span>
76
+ </DialogPrimitive.Close>
77
+ )}
78
+ </DialogPrimitive.Content>
79
+ </DialogPortal>
80
+ );
81
+ }
82
+
83
+ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
84
+ return (
85
+ <div
86
+ data-slot="dialog-header"
87
+ className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
88
+ {...props}
89
+ />
90
+ );
91
+ }
92
+
93
+ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
94
+ return (
95
+ <div
96
+ data-slot="dialog-footer"
97
+ className={cn(
98
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
99
+ className,
100
+ )}
101
+ {...props}
102
+ />
103
+ );
104
+ }
105
+
106
+ function DialogTitle({
107
+ className,
108
+ ...props
109
+ }: React.ComponentProps<typeof DialogPrimitive.Title>) {
110
+ return (
111
+ <DialogPrimitive.Title
112
+ data-slot="dialog-title"
113
+ className={cn("text-lg leading-none font-semibold", className)}
114
+ {...props}
115
+ />
116
+ );
117
+ }
118
+
119
+ function DialogDescription({
120
+ className,
121
+ ...props
122
+ }: React.ComponentProps<typeof DialogPrimitive.Description>) {
123
+ return (
124
+ <DialogPrimitive.Description
125
+ data-slot="dialog-description"
126
+ className={cn("text-sm text-muted-foreground", className)}
127
+ {...props}
128
+ />
129
+ );
130
+ }
131
+
132
+ export {
133
+ Dialog,
134
+ DialogClose,
135
+ DialogContent,
136
+ DialogDescription,
137
+ DialogFooter,
138
+ DialogHeader,
139
+ DialogOverlay,
140
+ DialogPortal,
141
+ DialogTitle,
142
+ DialogTrigger,
143
+ };
components/ui/input.tsx ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ <input
8
+ type={type}
9
+ data-slot="input"
10
+ className={cn(
11
+ "h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
12
+ "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
13
+ "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
14
+ className,
15
+ )}
16
+ {...props}
17
+ />
18
+ );
19
+ }
20
+
21
+ export { Input };
components/ui/separator.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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<typeof SeparatorPrimitive.Root>) {
14
+ return (
15
+ <SeparatorPrimitive.Root
16
+ data-slot="separator"
17
+ decorative={decorative}
18
+ orientation={orientation}
19
+ className={cn(
20
+ "shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
21
+ className,
22
+ )}
23
+ {...props}
24
+ />
25
+ );
26
+ }
27
+
28
+ export { Separator };
components/ui/sheet.tsx ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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<typeof SheetPrimitive.Root>) {
10
+ return <SheetPrimitive.Root data-slot="sheet" {...props} />;
11
+ }
12
+
13
+ function SheetTrigger({
14
+ ...props
15
+ }: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
16
+ return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
17
+ }
18
+
19
+ function SheetClose({
20
+ ...props
21
+ }: React.ComponentProps<typeof SheetPrimitive.Close>) {
22
+ return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
23
+ }
24
+
25
+ function SheetPortal({
26
+ ...props
27
+ }: React.ComponentProps<typeof SheetPrimitive.Portal>) {
28
+ return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
29
+ }
30
+
31
+ function SheetOverlay({
32
+ className,
33
+ ...props
34
+ }: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
35
+ return (
36
+ <SheetPrimitive.Overlay
37
+ data-slot="sheet-overlay"
38
+ className={cn(
39
+ "fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
40
+ className,
41
+ )}
42
+ {...props}
43
+ />
44
+ );
45
+ }
46
+
47
+ function SheetContent({
48
+ className,
49
+ children,
50
+ side = "right",
51
+ ...props
52
+ }: React.ComponentProps<typeof SheetPrimitive.Content> & {
53
+ side?: "top" | "right" | "bottom" | "left";
54
+ }) {
55
+ return (
56
+ <SheetPortal>
57
+ <SheetOverlay />
58
+ <SheetPrimitive.Content
59
+ data-slot="sheet-content"
60
+ className={cn(
61
+ "fixed z-50 flex flex-col gap-4 bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
62
+ side === "right" &&
63
+ "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
64
+ side === "left" &&
65
+ "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
66
+ side === "top" &&
67
+ "inset-x-0 top-0 h-auto border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
68
+ side === "bottom" &&
69
+ "inset-x-0 bottom-0 h-auto border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
70
+ className,
71
+ )}
72
+ {...props}
73
+ >
74
+ {children}
75
+ <SheetPrimitive.Close className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-secondary">
76
+ <XIcon className="size-4" />
77
+ <span className="sr-only">Close</span>
78
+ </SheetPrimitive.Close>
79
+ </SheetPrimitive.Content>
80
+ </SheetPortal>
81
+ );
82
+ }
83
+
84
+ function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
85
+ return (
86
+ <div
87
+ data-slot="sheet-header"
88
+ className={cn("flex flex-col gap-1.5 p-4", className)}
89
+ {...props}
90
+ />
91
+ );
92
+ }
93
+
94
+ function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
95
+ return (
96
+ <div
97
+ data-slot="sheet-footer"
98
+ className={cn("mt-auto flex flex-col gap-2 p-4", className)}
99
+ {...props}
100
+ />
101
+ );
102
+ }
103
+
104
+ function SheetTitle({
105
+ className,
106
+ ...props
107
+ }: React.ComponentProps<typeof SheetPrimitive.Title>) {
108
+ return (
109
+ <SheetPrimitive.Title
110
+ data-slot="sheet-title"
111
+ className={cn("font-semibold text-foreground", className)}
112
+ {...props}
113
+ />
114
+ );
115
+ }
116
+
117
+ function SheetDescription({
118
+ className,
119
+ ...props
120
+ }: React.ComponentProps<typeof SheetPrimitive.Description>) {
121
+ return (
122
+ <SheetPrimitive.Description
123
+ data-slot="sheet-description"
124
+ className={cn("text-sm text-muted-foreground", className)}
125
+ {...props}
126
+ />
127
+ );
128
+ }
129
+
130
+ export {
131
+ Sheet,
132
+ SheetTrigger,
133
+ SheetClose,
134
+ SheetContent,
135
+ SheetHeader,
136
+ SheetFooter,
137
+ SheetTitle,
138
+ SheetDescription,
139
+ };
components/ui/sidebar.tsx ADDED
@@ -0,0 +1,726 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Slot } from "@radix-ui/react-slot";
5
+ import { cva, type VariantProps } 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<SidebarContextProps | null>(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<SidebarContextProps>(
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
+ <SidebarContext.Provider value={contextValue}>
131
+ <TooltipProvider delayDuration={0}>
132
+ <div
133
+ data-slot="sidebar-wrapper"
134
+ style={
135
+ {
136
+ "--sidebar-width": SIDEBAR_WIDTH,
137
+ "--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
138
+ ...style,
139
+ } as React.CSSProperties
140
+ }
141
+ className={cn(
142
+ "group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar",
143
+ className,
144
+ )}
145
+ {...props}
146
+ >
147
+ {children}
148
+ </div>
149
+ </TooltipProvider>
150
+ </SidebarContext.Provider>
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
+ <div
171
+ data-slot="sidebar"
172
+ className={cn(
173
+ "flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground",
174
+ className,
175
+ )}
176
+ {...props}
177
+ >
178
+ {children}
179
+ </div>
180
+ );
181
+ }
182
+
183
+ if (isMobile) {
184
+ return (
185
+ <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
186
+ <SheetContent
187
+ data-sidebar="sidebar"
188
+ data-slot="sidebar"
189
+ data-mobile="true"
190
+ className="w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
191
+ style={
192
+ {
193
+ "--sidebar-width": SIDEBAR_WIDTH_MOBILE,
194
+ } as React.CSSProperties
195
+ }
196
+ side={side}
197
+ >
198
+ <SheetHeader className="sr-only">
199
+ <SheetTitle>Sidebar</SheetTitle>
200
+ <SheetDescription>Displays the mobile sidebar.</SheetDescription>
201
+ </SheetHeader>
202
+ <div className="flex h-full w-full flex-col">{children}</div>
203
+ </SheetContent>
204
+ </Sheet>
205
+ );
206
+ }
207
+
208
+ return (
209
+ <div
210
+ className="group peer hidden text-sidebar-foreground md:block"
211
+ data-state={state}
212
+ data-collapsible={state === "collapsed" ? collapsible : ""}
213
+ data-variant={variant}
214
+ data-side={side}
215
+ data-slot="sidebar"
216
+ >
217
+ {/* This is what handles the sidebar gap on desktop */}
218
+ <div
219
+ data-slot="sidebar-gap"
220
+ className={cn(
221
+ "relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
222
+ "group-data-[collapsible=offcanvas]:w-0",
223
+ "group-data-[side=right]:rotate-180",
224
+ variant === "floating" || variant === "inset"
225
+ ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
226
+ : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
227
+ )}
228
+ />
229
+ <div
230
+ data-slot="sidebar-container"
231
+ className={cn(
232
+ "fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
233
+ side === "left"
234
+ ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
235
+ : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
236
+ // Adjust the padding for floating and inset variants.
237
+ variant === "floating" || variant === "inset"
238
+ ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
239
+ : "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
240
+ className,
241
+ )}
242
+ {...props}
243
+ >
244
+ <div
245
+ data-sidebar="sidebar"
246
+ data-slot="sidebar-inner"
247
+ className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow-sm"
248
+ >
249
+ {children}
250
+ </div>
251
+ </div>
252
+ </div>
253
+ );
254
+ }
255
+
256
+ function SidebarTrigger({
257
+ className,
258
+ onClick,
259
+ ...props
260
+ }: React.ComponentProps<typeof Button>) {
261
+ const { toggleSidebar } = useSidebar();
262
+
263
+ return (
264
+ <Button
265
+ data-sidebar="trigger"
266
+ data-slot="sidebar-trigger"
267
+ variant="ghost"
268
+ size="icon"
269
+ className={cn("size-7", className)}
270
+ onClick={(event) => {
271
+ onClick?.(event);
272
+ toggleSidebar();
273
+ }}
274
+ {...props}
275
+ >
276
+ <PanelLeftIcon />
277
+ <span className="sr-only">Toggle Sidebar</span>
278
+ </Button>
279
+ );
280
+ }
281
+
282
+ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
283
+ const { toggleSidebar } = useSidebar();
284
+
285
+ return (
286
+ <button
287
+ data-sidebar="rail"
288
+ data-slot="sidebar-rail"
289
+ aria-label="Toggle Sidebar"
290
+ tabIndex={-1}
291
+ onClick={toggleSidebar}
292
+ title="Toggle Sidebar"
293
+ className={cn(
294
+ "absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border sm:flex",
295
+ "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
296
+ "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
297
+ "group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-sidebar",
298
+ "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
299
+ "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
300
+ className,
301
+ )}
302
+ {...props}
303
+ />
304
+ );
305
+ }
306
+
307
+ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
308
+ return (
309
+ <main
310
+ data-slot="sidebar-inset"
311
+ className={cn(
312
+ "relative flex w-full flex-1 flex-col bg-background",
313
+ "md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
314
+ className,
315
+ )}
316
+ {...props}
317
+ />
318
+ );
319
+ }
320
+
321
+ function SidebarInput({
322
+ className,
323
+ ...props
324
+ }: React.ComponentProps<typeof Input>) {
325
+ return (
326
+ <Input
327
+ data-slot="sidebar-input"
328
+ data-sidebar="input"
329
+ className={cn("h-8 w-full bg-background shadow-none", className)}
330
+ {...props}
331
+ />
332
+ );
333
+ }
334
+
335
+ function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
336
+ return (
337
+ <div
338
+ data-slot="sidebar-header"
339
+ data-sidebar="header"
340
+ className={cn("flex flex-col gap-2 p-2", className)}
341
+ {...props}
342
+ />
343
+ );
344
+ }
345
+
346
+ function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
347
+ return (
348
+ <div
349
+ data-slot="sidebar-footer"
350
+ data-sidebar="footer"
351
+ className={cn("flex flex-col gap-2 p-2", className)}
352
+ {...props}
353
+ />
354
+ );
355
+ }
356
+
357
+ function SidebarSeparator({
358
+ className,
359
+ ...props
360
+ }: React.ComponentProps<typeof Separator>) {
361
+ return (
362
+ <Separator
363
+ data-slot="sidebar-separator"
364
+ data-sidebar="separator"
365
+ className={cn("mx-2 w-auto bg-sidebar-border", className)}
366
+ {...props}
367
+ />
368
+ );
369
+ }
370
+
371
+ function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
372
+ return (
373
+ <div
374
+ data-slot="sidebar-content"
375
+ data-sidebar="content"
376
+ className={cn(
377
+ "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
378
+ className,
379
+ )}
380
+ {...props}
381
+ />
382
+ );
383
+ }
384
+
385
+ function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
386
+ return (
387
+ <div
388
+ data-slot="sidebar-group"
389
+ data-sidebar="group"
390
+ className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
391
+ {...props}
392
+ />
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
+ <Comp
405
+ data-slot="sidebar-group-label"
406
+ data-sidebar="group-label"
407
+ className={cn(
408
+ "flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 ring-sidebar-ring outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>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
+ <Comp
426
+ data-slot="sidebar-group-action"
427
+ data-sidebar="group-action"
428
+ className={cn(
429
+ "absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>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
+ <div
446
+ data-slot="sidebar-group-content"
447
+ data-sidebar="group-content"
448
+ className={cn("w-full text-sm", className)}
449
+ {...props}
450
+ />
451
+ );
452
+ }
453
+
454
+ function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
455
+ return (
456
+ <ul
457
+ data-slot="sidebar-menu"
458
+ data-sidebar="menu"
459
+ className={cn("flex w-full min-w-0 flex-col gap-1", className)}
460
+ {...props}
461
+ />
462
+ );
463
+ }
464
+
465
+ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
466
+ return (
467
+ <li
468
+ data-slot="sidebar-menu-item"
469
+ data-sidebar="menu-item"
470
+ className={cn("group/menu-item relative", className)}
471
+ {...props}
472
+ />
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<typeof TooltipContent>;
510
+ } & VariantProps<typeof sidebarMenuButtonVariants>) {
511
+ const Comp = asChild ? Slot : "button";
512
+ const { isMobile, state } = useSidebar();
513
+
514
+ const button = (
515
+ <Comp
516
+ data-slot="sidebar-menu-button"
517
+ data-sidebar="menu-button"
518
+ data-size={size}
519
+ data-active={isActive}
520
+ className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
521
+ {...props}
522
+ />
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
+ <Tooltip>
537
+ <TooltipTrigger asChild>{button}</TooltipTrigger>
538
+ <TooltipContent
539
+ side="right"
540
+ align="center"
541
+ hidden={state !== "collapsed" || isMobile}
542
+ {...tooltip}
543
+ />
544
+ </Tooltip>
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
+ <Comp
561
+ data-slot="sidebar-menu-action"
562
+ data-sidebar="menu-action"
563
+ className={cn(
564
+ "absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform peer-hover/menu-button:text-sidebar-accent-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>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
+ "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground 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
+ <div
586
+ data-slot="sidebar-menu-badge"
587
+ data-sidebar="menu-badge"
588
+ className={cn(
589
+ "pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium text-sidebar-foreground tabular-nums select-none",
590
+ "peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
591
+ "peer-data-[size=sm]/menu-button:top-1",
592
+ "peer-data-[size=default]/menu-button:top-1.5",
593
+ "peer-data-[size=lg]/menu-button:top-2.5",
594
+ "group-data-[collapsible=icon]:hidden",
595
+ className,
596
+ )}
597
+ {...props}
598
+ />
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
+ <div
616
+ data-slot="sidebar-menu-skeleton"
617
+ data-sidebar="menu-skeleton"
618
+ className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
619
+ {...props}
620
+ >
621
+ {showIcon && (
622
+ <Skeleton
623
+ className="size-4 rounded-md"
624
+ data-sidebar="menu-skeleton-icon"
625
+ />
626
+ )}
627
+ <Skeleton
628
+ className="h-4 max-w-(--skeleton-width) flex-1"
629
+ data-sidebar="menu-skeleton-text"
630
+ style={
631
+ {
632
+ "--skeleton-width": width,
633
+ } as React.CSSProperties
634
+ }
635
+ />
636
+ </div>
637
+ );
638
+ }
639
+
640
+ function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
641
+ return (
642
+ <ul
643
+ data-slot="sidebar-menu-sub"
644
+ data-sidebar="menu-sub"
645
+ className={cn(
646
+ "mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
647
+ "group-data-[collapsible=icon]:hidden",
648
+ className,
649
+ )}
650
+ {...props}
651
+ />
652
+ );
653
+ }
654
+
655
+ function SidebarMenuSubItem({
656
+ className,
657
+ ...props
658
+ }: React.ComponentProps<"li">) {
659
+ return (
660
+ <li
661
+ data-slot="sidebar-menu-sub-item"
662
+ data-sidebar="menu-sub-item"
663
+ className={cn("group/menu-sub-item relative", className)}
664
+ {...props}
665
+ />
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
+ <Comp
684
+ data-slot="sidebar-menu-sub-button"
685
+ data-sidebar="menu-sub-button"
686
+ data-size={size}
687
+ data-active={isActive}
688
+ className={cn(
689
+ "flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground ring-sidebar-ring outline-hidden 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 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
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
+ };
components/ui/skeleton.tsx ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cn } from "@/lib/utils";
2
+
3
+ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
4
+ return (
5
+ <div
6
+ data-slot="skeleton"
7
+ className={cn("animate-pulse rounded-md bg-accent", className)}
8
+ {...props}
9
+ />
10
+ );
11
+ }
12
+
13
+ export { Skeleton };
components/ui/tooltip.tsx ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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<typeof TooltipPrimitive.Provider>) {
12
+ return (
13
+ <TooltipPrimitive.Provider
14
+ data-slot="tooltip-provider"
15
+ delayDuration={delayDuration}
16
+ {...props}
17
+ />
18
+ );
19
+ }
20
+
21
+ function Tooltip({
22
+ ...props
23
+ }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
24
+ return (
25
+ <TooltipProvider>
26
+ <TooltipPrimitive.Root data-slot="tooltip" {...props} />
27
+ </TooltipProvider>
28
+ );
29
+ }
30
+
31
+ function TooltipTrigger({
32
+ ...props
33
+ }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
34
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
35
+ }
36
+
37
+ function TooltipContent({
38
+ className,
39
+ sideOffset = 0,
40
+ children,
41
+ ...props
42
+ }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
43
+ return (
44
+ <TooltipPrimitive.Portal>
45
+ <TooltipPrimitive.Content
46
+ data-slot="tooltip-content"
47
+ sideOffset={sideOffset}
48
+ className={cn(
49
+ "z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
50
+ className,
51
+ )}
52
+ {...props}
53
+ >
54
+ {children}
55
+ <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
56
+ </TooltipPrimitive.Content>
57
+ </TooltipPrimitive.Portal>
58
+ );
59
+ }
60
+
61
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
eslint.config.mjs ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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;
hooks/use-mobile.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+
3
+ const MOBILE_BREAKPOINT = 768;
4
+
5
+ export function useIsMobile() {
6
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
7
+ undefined,
8
+ );
9
+
10
+ React.useEffect(() => {
11
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
12
+ const onChange = () => {
13
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
14
+ };
15
+ mql.addEventListener("change", onChange);
16
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
17
+ return () => mql.removeEventListener("change", onChange);
18
+ }, []);
19
+
20
+ return !!isMobile;
21
+ }
lib/utils.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
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
+ }
next.config.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ /* config options here */
5
+ };
6
+
7
+ export default nextConfig;
package.json ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ "prettier": "prettier --check .",
11
+ "prettier:fix": "prettier --write ."
12
+ },
13
+ "prettier": {
14
+ "plugins": [
15
+ "prettier-plugin-tailwindcss"
16
+ ],
17
+ "tailwindStylesheet": "app/globals.css"
18
+ },
19
+ "dependencies": {
20
+ "@ai-sdk/openai": "^2.0.86",
21
+ "@assistant-ui/react": "^0.11.51",
22
+ "@assistant-ui/react-ai-sdk": "^1.1.19",
23
+ "@assistant-ui/react-markdown": "^0.11.8",
24
+ "@radix-ui/react-avatar": "^1.1.11",
25
+ "@radix-ui/react-collapsible": "^1.1.12",
26
+ "@radix-ui/react-dialog": "^1.1.15",
27
+ "@radix-ui/react-separator": "^1.1.8",
28
+ "@radix-ui/react-slot": "^1.2.4",
29
+ "@radix-ui/react-tooltip": "^1.2.8",
30
+ "ai": "^5.0.113",
31
+ "class-variance-authority": "^0.7.1",
32
+ "clsx": "^2.1.1",
33
+ "framer-motion": "^12.23.26",
34
+ "lucide-react": "^0.561.0",
35
+ "motion": "^12.23.26",
36
+ "next": "16.0.10",
37
+ "react": "^19.2.3",
38
+ "react-dom": "^19.2.3",
39
+ "remark-gfm": "^4.0.1",
40
+ "tailwind-merge": "^3.4.0",
41
+ "tw-animate-css": "^1.4.0",
42
+ "zustand": "^5.0.9"
43
+ },
44
+ "devDependencies": {
45
+ "@eslint/eslintrc": "^3",
46
+ "@tailwindcss/postcss": "^4",
47
+ "@types/node": "^25",
48
+ "@types/react": "^19",
49
+ "@types/react-dom": "^19",
50
+ "eslint": "^9",
51
+ "eslint-config-next": "16.0.10",
52
+ "prettier": "^3.7.4",
53
+ "prettier-plugin-tailwindcss": "^0.7.2",
54
+ "tailwindcss": "^4",
55
+ "typescript": "^5"
56
+ }
57
+ }
pnpm-lock.yaml ADDED
The diff for this file is too large to render. See raw diff
 
postcss.config.mjs ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ const config = {
2
+ plugins: ["@tailwindcss/postcss"],
3
+ };
4
+
5
+ export default config;
tsconfig.json ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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": "react-jsx",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./*"]
23
+ }
24
+ },
25
+ "include": [
26
+ "next-env.d.ts",
27
+ "**/*.ts",
28
+ "**/*.tsx",
29
+ ".next/types/**/*.ts",
30
+ ".next/dev/types/**/*.ts"
31
+ ],
32
+ "exclude": ["node_modules"]
33
+ }