@assistant-ui/mcp-docs-server 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/.docs/organized/code-examples/store-example.md +628 -0
  2. package/.docs/organized/code-examples/with-ag-ui.md +792 -178
  3. package/.docs/organized/code-examples/with-ai-sdk-v5.md +762 -209
  4. package/.docs/organized/code-examples/with-assistant-transport.md +707 -254
  5. package/.docs/organized/code-examples/with-cloud.md +848 -202
  6. package/.docs/organized/code-examples/with-custom-thread-list.md +1855 -0
  7. package/.docs/organized/code-examples/with-external-store.md +788 -172
  8. package/.docs/organized/code-examples/with-ffmpeg.md +796 -196
  9. package/.docs/organized/code-examples/with-langgraph.md +864 -230
  10. package/.docs/organized/code-examples/with-parent-id-grouping.md +785 -255
  11. package/.docs/organized/code-examples/with-react-hook-form.md +804 -226
  12. package/.docs/organized/code-examples/with-tanstack.md +1574 -0
  13. package/.docs/raw/blog/2024-07-29-hello/index.mdx +2 -3
  14. package/.docs/raw/docs/api-reference/overview.mdx +6 -6
  15. package/.docs/raw/docs/api-reference/primitives/ActionBar.mdx +85 -4
  16. package/.docs/raw/docs/api-reference/primitives/AssistantIf.mdx +200 -0
  17. package/.docs/raw/docs/api-reference/primitives/Composer.mdx +0 -20
  18. package/.docs/raw/docs/api-reference/primitives/Message.mdx +0 -45
  19. package/.docs/raw/docs/api-reference/primitives/Thread.mdx +0 -50
  20. package/.docs/raw/docs/cli.mdx +396 -0
  21. package/.docs/raw/docs/cloud/persistence/ai-sdk.mdx +2 -3
  22. package/.docs/raw/docs/cloud/persistence/langgraph.mdx +2 -3
  23. package/.docs/raw/docs/devtools.mdx +2 -3
  24. package/.docs/raw/docs/getting-started.mdx +37 -1109
  25. package/.docs/raw/docs/guides/Attachments.mdx +3 -25
  26. package/.docs/raw/docs/guides/Branching.mdx +1 -1
  27. package/.docs/raw/docs/guides/Speech.mdx +1 -1
  28. package/.docs/raw/docs/guides/ToolUI.mdx +1 -1
  29. package/.docs/raw/docs/legacy/styled/AssistantModal.mdx +2 -3
  30. package/.docs/raw/docs/legacy/styled/Decomposition.mdx +6 -5
  31. package/.docs/raw/docs/legacy/styled/Markdown.mdx +2 -3
  32. package/.docs/raw/docs/legacy/styled/Thread.mdx +2 -3
  33. package/.docs/raw/docs/react-compatibility.mdx +2 -5
  34. package/.docs/raw/docs/runtimes/ai-sdk/use-chat.mdx +3 -4
  35. package/.docs/raw/docs/runtimes/ai-sdk/v4-legacy.mdx +3 -6
  36. package/.docs/raw/docs/runtimes/assistant-transport.mdx +891 -0
  37. package/.docs/raw/docs/runtimes/custom/external-store.mdx +2 -3
  38. package/.docs/raw/docs/runtimes/custom/local.mdx +11 -41
  39. package/.docs/raw/docs/runtimes/data-stream.mdx +15 -11
  40. package/.docs/raw/docs/runtimes/langgraph/index.mdx +4 -4
  41. package/.docs/raw/docs/runtimes/langgraph/tutorial/part-2.mdx +1 -1
  42. package/.docs/raw/docs/runtimes/langgraph/tutorial/part-3.mdx +2 -3
  43. package/.docs/raw/docs/runtimes/langserve.mdx +2 -3
  44. package/.docs/raw/docs/runtimes/mastra/full-stack-integration.mdx +2 -3
  45. package/.docs/raw/docs/runtimes/mastra/separate-server-integration.mdx +2 -3
  46. package/.docs/raw/docs/ui/AssistantModal.mdx +3 -25
  47. package/.docs/raw/docs/ui/AssistantSidebar.mdx +2 -24
  48. package/.docs/raw/docs/ui/Attachment.mdx +3 -25
  49. package/.docs/raw/docs/ui/Markdown.mdx +2 -24
  50. package/.docs/raw/docs/ui/Mermaid.mdx +2 -24
  51. package/.docs/raw/docs/ui/Reasoning.mdx +2 -24
  52. package/.docs/raw/docs/ui/Scrollbar.mdx +4 -6
  53. package/.docs/raw/docs/ui/SyntaxHighlighting.mdx +3 -47
  54. package/.docs/raw/docs/ui/Thread.mdx +38 -53
  55. package/.docs/raw/docs/ui/ThreadList.mdx +4 -47
  56. package/.docs/raw/docs/ui/ToolFallback.mdx +2 -24
  57. package/package.json +15 -8
@@ -0,0 +1,1855 @@
1
+ # Example: with-custom-thread-list
2
+
3
+ ## app/api/chat/route.ts
4
+
5
+ ```typescript
6
+ import { openai } from "@ai-sdk/openai";
7
+ import {
8
+ streamText,
9
+ UIMessage,
10
+ convertToModelMessages,
11
+ tool,
12
+ stepCountIs,
13
+ } from "ai";
14
+ import { z } from "zod";
15
+
16
+ // Allow streaming responses up to 30 seconds
17
+ export const maxDuration = 30;
18
+
19
+ export async function POST(req: Request) {
20
+ const { messages }: { messages: UIMessage[] } = await req.json();
21
+
22
+ const result = streamText({
23
+ model: openai("gpt-4o"),
24
+ messages: convertToModelMessages(messages),
25
+ stopWhen: stepCountIs(10),
26
+ tools: {
27
+ get_current_weather: tool({
28
+ name: "",
29
+ description: "Get the current weather",
30
+ inputSchema: z.object({
31
+ city: z.string(),
32
+ }),
33
+ execute: async ({ city }) => {
34
+ return `The weather in ${city} is sunny`;
35
+ },
36
+ }),
37
+ },
38
+ });
39
+
40
+ return result.toUIMessageStreamResponse();
41
+ }
42
+
43
+ ```
44
+
45
+ ## app/globals.css
46
+
47
+ ```css
48
+ @import "tailwindcss";
49
+ @import "tw-animate-css";
50
+
51
+ @custom-variant dark (&:is(.dark *));
52
+
53
+ @theme inline {
54
+ --radius-sm: calc(var(--radius) - 4px);
55
+ --radius-md: calc(var(--radius) - 2px);
56
+ --radius-lg: var(--radius);
57
+ --radius-xl: calc(var(--radius) + 4px);
58
+ --color-background: var(--background);
59
+ --color-foreground: var(--foreground);
60
+ --color-card: var(--card);
61
+ --color-card-foreground: var(--card-foreground);
62
+ --color-popover: var(--popover);
63
+ --color-popover-foreground: var(--popover-foreground);
64
+ --color-primary: var(--primary);
65
+ --color-primary-foreground: var(--primary-foreground);
66
+ --color-secondary: var(--secondary);
67
+ --color-secondary-foreground: var(--secondary-foreground);
68
+ --color-muted: var(--muted);
69
+ --color-muted-foreground: var(--muted-foreground);
70
+ --color-accent: var(--accent);
71
+ --color-accent-foreground: var(--accent-foreground);
72
+ --color-destructive: var(--destructive);
73
+ --color-border: var(--border);
74
+ --color-input: var(--input);
75
+ --color-ring: var(--ring);
76
+ --color-chart-1: var(--chart-1);
77
+ --color-chart-2: var(--chart-2);
78
+ --color-chart-3: var(--chart-3);
79
+ --color-chart-4: var(--chart-4);
80
+ --color-chart-5: var(--chart-5);
81
+ --color-sidebar: var(--sidebar);
82
+ --color-sidebar-foreground: var(--sidebar-foreground);
83
+ --color-sidebar-primary: var(--sidebar-primary);
84
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
85
+ --color-sidebar-accent: var(--sidebar-accent);
86
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
87
+ --color-sidebar-border: var(--sidebar-border);
88
+ --color-sidebar-ring: var(--sidebar-ring);
89
+ }
90
+
91
+ :root {
92
+ --radius: 0.625rem;
93
+ --background: oklch(1 0 0);
94
+ --foreground: oklch(0.141 0.005 285.823);
95
+ --card: oklch(1 0 0);
96
+ --card-foreground: oklch(0.141 0.005 285.823);
97
+ --popover: oklch(1 0 0);
98
+ --popover-foreground: oklch(0.141 0.005 285.823);
99
+ --primary: oklch(0.21 0.006 285.885);
100
+ --primary-foreground: oklch(0.985 0 0);
101
+ --secondary: oklch(0.967 0.001 286.375);
102
+ --secondary-foreground: oklch(0.21 0.006 285.885);
103
+ --muted: oklch(0.967 0.001 286.375);
104
+ --muted-foreground: oklch(0.552 0.016 285.938);
105
+ --accent: oklch(0.967 0.001 286.375);
106
+ --accent-foreground: oklch(0.21 0.006 285.885);
107
+ --destructive: oklch(0.577 0.245 27.325);
108
+ --border: oklch(0.92 0.004 286.32);
109
+ --input: oklch(0.92 0.004 286.32);
110
+ --ring: oklch(0.705 0.015 286.067);
111
+ --chart-1: oklch(0.646 0.222 41.116);
112
+ --chart-2: oklch(0.6 0.118 184.704);
113
+ --chart-3: oklch(0.398 0.07 227.392);
114
+ --chart-4: oklch(0.828 0.189 84.429);
115
+ --chart-5: oklch(0.769 0.188 70.08);
116
+ --sidebar: oklch(0.985 0 0);
117
+ --sidebar-foreground: oklch(0.141 0.005 285.823);
118
+ --sidebar-primary: oklch(0.21 0.006 285.885);
119
+ --sidebar-primary-foreground: oklch(0.985 0 0);
120
+ --sidebar-accent: oklch(0.967 0.001 286.375);
121
+ --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
122
+ --sidebar-border: oklch(0.92 0.004 286.32);
123
+ --sidebar-ring: oklch(0.705 0.015 286.067);
124
+ }
125
+
126
+ .dark {
127
+ --background: oklch(0.141 0.005 285.823);
128
+ --foreground: oklch(0.985 0 0);
129
+ --card: oklch(0.21 0.006 285.885);
130
+ --card-foreground: oklch(0.985 0 0);
131
+ --popover: oklch(0.21 0.006 285.885);
132
+ --popover-foreground: oklch(0.985 0 0);
133
+ --primary: oklch(0.92 0.004 286.32);
134
+ --primary-foreground: oklch(0.21 0.006 285.885);
135
+ --secondary: oklch(0.274 0.006 286.033);
136
+ --secondary-foreground: oklch(0.985 0 0);
137
+ --muted: oklch(0.274 0.006 286.033);
138
+ --muted-foreground: oklch(0.705 0.015 286.067);
139
+ --accent: oklch(0.274 0.006 286.033);
140
+ --accent-foreground: oklch(0.985 0 0);
141
+ --destructive: oklch(0.704 0.191 22.216);
142
+ --border: oklch(1 0 0 / 10%);
143
+ --input: oklch(1 0 0 / 15%);
144
+ --ring: oklch(0.552 0.016 285.938);
145
+ --chart-1: oklch(0.488 0.243 264.376);
146
+ --chart-2: oklch(0.696 0.17 162.48);
147
+ --chart-3: oklch(0.769 0.188 70.08);
148
+ --chart-4: oklch(0.627 0.265 303.9);
149
+ --chart-5: oklch(0.645 0.246 16.439);
150
+ --sidebar: oklch(0.21 0.006 285.885);
151
+ --sidebar-foreground: oklch(0.985 0 0);
152
+ --sidebar-primary: oklch(0.488 0.243 264.376);
153
+ --sidebar-primary-foreground: oklch(0.985 0 0);
154
+ --sidebar-accent: oklch(0.274 0.006 286.033);
155
+ --sidebar-accent-foreground: oklch(0.985 0 0);
156
+ --sidebar-border: oklch(1 0 0 / 10%);
157
+ --sidebar-ring: oklch(0.552 0.016 285.938);
158
+ }
159
+
160
+ @layer base {
161
+ * {
162
+ @apply border-border outline-ring/50;
163
+ }
164
+ body {
165
+ @apply bg-background text-foreground;
166
+ }
167
+ }
168
+
169
+ ```
170
+
171
+ ## app/layout.tsx
172
+
173
+ ```tsx
174
+ import type { Metadata } from "next";
175
+ import "./globals.css";
176
+ import { MyRuntimeProvider } from "./MyRuntimeProvider";
177
+
178
+ export const metadata: Metadata = {
179
+ title: "Custom Thread List Example",
180
+ description:
181
+ "Example using @assistant-ui/react with a custom thread list adapter",
182
+ };
183
+
184
+ export default function RootLayout({
185
+ children,
186
+ }: Readonly<{
187
+ children: React.ReactNode;
188
+ }>) {
189
+ return (
190
+ <html lang="en" className="h-dvh">
191
+ <body className="h-dvh antialiased">
192
+ <MyRuntimeProvider>{children}</MyRuntimeProvider>
193
+ </body>
194
+ </html>
195
+ );
196
+ }
197
+
198
+ ```
199
+
200
+ ## app/MyRuntimeProvider.tsx
201
+
202
+ ```tsx
203
+ "use client";
204
+
205
+ import type { ReactNode } from "react";
206
+ import {
207
+ AssistantRuntimeProvider,
208
+ unstable_useRemoteThreadListRuntime as useRemoteThreadListRuntime,
209
+ type unstable_RemoteThreadListAdapter as RemoteThreadListAdapter,
210
+ } from "@assistant-ui/react";
211
+ import { useChatRuntime } from "@assistant-ui/react-ai-sdk";
212
+ import { createAssistantStream } from "assistant-stream";
213
+
214
+ // In-memory storage for threads (simulating a database)
215
+ const threadsStore = new Map<
216
+ string,
217
+ {
218
+ remoteId: string;
219
+ status: "regular" | "archived";
220
+ title?: string;
221
+ }
222
+ >();
223
+
224
+ const threadListAdapter: RemoteThreadListAdapter = {
225
+ async list() {
226
+ return {
227
+ threads: Array.from(threadsStore.values()).map((thread) => ({
228
+ remoteId: thread.remoteId,
229
+ status: thread.status,
230
+ title: thread.title,
231
+ })),
232
+ };
233
+ },
234
+
235
+ async initialize(localId) {
236
+ const remoteId = localId;
237
+ threadsStore.set(remoteId, {
238
+ remoteId,
239
+ status: "regular",
240
+ });
241
+ return { remoteId, externalId: undefined };
242
+ },
243
+
244
+ async rename(remoteId, title) {
245
+ const thread = threadsStore.get(remoteId);
246
+ if (thread) {
247
+ thread.title = title;
248
+ }
249
+ },
250
+
251
+ async archive(remoteId) {
252
+ const thread = threadsStore.get(remoteId);
253
+ if (thread) {
254
+ thread.status = "archived";
255
+ }
256
+ },
257
+
258
+ async unarchive(remoteId) {
259
+ const thread = threadsStore.get(remoteId);
260
+ if (thread) {
261
+ thread.status = "regular";
262
+ }
263
+ },
264
+
265
+ async delete(remoteId) {
266
+ threadsStore.delete(remoteId);
267
+ },
268
+
269
+ async fetch(remoteId) {
270
+ const thread = threadsStore.get(remoteId);
271
+ if (!thread) {
272
+ throw new Error("Thread not found");
273
+ }
274
+ return {
275
+ remoteId: thread.remoteId,
276
+ status: thread.status,
277
+ title: thread.title,
278
+ };
279
+ },
280
+
281
+ async generateTitle(_remoteId, messages) {
282
+ // Generate a simple title from the first user message
283
+ return createAssistantStream(async (controller) => {
284
+ const firstUserMessage = messages.find((m) => m.role === "user");
285
+ if (firstUserMessage) {
286
+ const content = firstUserMessage.content
287
+ .filter((c) => c.type === "text")
288
+ .map((c) => c.text)
289
+ .join(" ");
290
+ const title = content.slice(0, 50) + (content.length > 50 ? "..." : "");
291
+ controller.appendText(title);
292
+ } else {
293
+ controller.appendText("New Chat");
294
+ }
295
+ });
296
+ },
297
+ };
298
+
299
+ export function MyRuntimeProvider({
300
+ children,
301
+ }: Readonly<{ children: ReactNode }>) {
302
+ const runtime = useRemoteThreadListRuntime({
303
+ runtimeHook: useChatRuntime,
304
+ adapter: threadListAdapter,
305
+ });
306
+
307
+ return (
308
+ <AssistantRuntimeProvider runtime={runtime}>
309
+ {children}
310
+ </AssistantRuntimeProvider>
311
+ );
312
+ }
313
+
314
+ ```
315
+
316
+ ## app/page.tsx
317
+
318
+ ```tsx
319
+ "use client";
320
+
321
+ import { Thread } from "@/components/assistant-ui/thread";
322
+ import { ThreadList } from "@/components/assistant-ui/thread-list";
323
+
324
+ export default function Home() {
325
+ return (
326
+ <main className="grid h-dvh grid-cols-[200px_1fr] gap-4 p-4">
327
+ <ThreadList />
328
+ <Thread />
329
+ </main>
330
+ );
331
+ }
332
+
333
+ ```
334
+
335
+ ## components/assistant-ui/attachment.tsx
336
+
337
+ ```tsx
338
+ "use client";
339
+
340
+ import { PropsWithChildren, useEffect, useState, type FC } from "react";
341
+ import Image from "next/image";
342
+ import { XIcon, PlusIcon, FileText } from "lucide-react";
343
+ import {
344
+ AttachmentPrimitive,
345
+ ComposerPrimitive,
346
+ MessagePrimitive,
347
+ useAssistantState,
348
+ useAssistantApi,
349
+ } from "@assistant-ui/react";
350
+ import { useShallow } from "zustand/shallow";
351
+ import {
352
+ Tooltip,
353
+ TooltipContent,
354
+ TooltipTrigger,
355
+ } from "@/components/ui/tooltip";
356
+ import {
357
+ Dialog,
358
+ DialogTitle,
359
+ DialogContent,
360
+ DialogTrigger,
361
+ } from "@/components/ui/dialog";
362
+ import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
363
+ import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
364
+ import { cn } from "@/lib/utils";
365
+
366
+ const useFileSrc = (file: File | undefined) => {
367
+ const [src, setSrc] = useState<string | undefined>(undefined);
368
+
369
+ useEffect(() => {
370
+ if (!file) {
371
+ setSrc(undefined);
372
+ return;
373
+ }
374
+
375
+ const objectUrl = URL.createObjectURL(file);
376
+ setSrc(objectUrl);
377
+
378
+ return () => {
379
+ URL.revokeObjectURL(objectUrl);
380
+ };
381
+ }, [file]);
382
+
383
+ return src;
384
+ };
385
+
386
+ const useAttachmentSrc = () => {
387
+ const { file, src } = useAssistantState(
388
+ useShallow(({ attachment }): { file?: File; src?: string } => {
389
+ if (attachment.type !== "image") return {};
390
+ if (attachment.file) return { file: attachment.file };
391
+ const src = attachment.content?.filter((c) => c.type === "image")[0]
392
+ ?.image;
393
+ if (!src) return {};
394
+ return { src };
395
+ }),
396
+ );
397
+
398
+ return useFileSrc(file) ?? src;
399
+ };
400
+
401
+ type AttachmentPreviewProps = {
402
+ src: string;
403
+ };
404
+
405
+ const AttachmentPreview: FC<AttachmentPreviewProps> = ({ src }) => {
406
+ const [isLoaded, setIsLoaded] = useState(false);
407
+ return (
408
+ <Image
409
+ src={src}
410
+ alt="Image Preview"
411
+ width={1}
412
+ height={1}
413
+ className={
414
+ isLoaded
415
+ ? "aui-attachment-preview-image-loaded block h-auto max-h-[80vh] w-auto max-w-full object-contain"
416
+ : "aui-attachment-preview-image-loading hidden"
417
+ }
418
+ onLoadingComplete={() => setIsLoaded(true)}
419
+ priority={false}
420
+ />
421
+ );
422
+ };
423
+
424
+ const AttachmentPreviewDialog: FC<PropsWithChildren> = ({ children }) => {
425
+ const src = useAttachmentSrc();
426
+
427
+ if (!src) return children;
428
+
429
+ return (
430
+ <Dialog>
431
+ <DialogTrigger
432
+ className="aui-attachment-preview-trigger cursor-pointer transition-colors hover:bg-accent/50"
433
+ asChild
434
+ >
435
+ {children}
436
+ </DialogTrigger>
437
+ <DialogContent className="aui-attachment-preview-dialog-content p-2 sm:max-w-3xl [&>button]:rounded-full [&>button]:bg-foreground/60 [&>button]:p-1 [&>button]:opacity-100 [&>button]:ring-0! [&_svg]:text-background [&>button]:hover:[&_svg]:text-destructive">
438
+ <DialogTitle className="aui-sr-only sr-only">
439
+ Image Attachment Preview
440
+ </DialogTitle>
441
+ <div className="aui-attachment-preview relative mx-auto flex max-h-[80dvh] w-full items-center justify-center overflow-hidden bg-background">
442
+ <AttachmentPreview src={src} />
443
+ </div>
444
+ </DialogContent>
445
+ </Dialog>
446
+ );
447
+ };
448
+
449
+ const AttachmentThumb: FC = () => {
450
+ const isImage = useAssistantState(
451
+ ({ attachment }) => attachment.type === "image",
452
+ );
453
+ const src = useAttachmentSrc();
454
+
455
+ return (
456
+ <Avatar className="aui-attachment-tile-avatar h-full w-full rounded-none">
457
+ <AvatarImage
458
+ src={src}
459
+ alt="Attachment preview"
460
+ className="aui-attachment-tile-image object-cover"
461
+ />
462
+ <AvatarFallback delayMs={isImage ? 200 : 0}>
463
+ <FileText className="aui-attachment-tile-fallback-icon size-8 text-muted-foreground" />
464
+ </AvatarFallback>
465
+ </Avatar>
466
+ );
467
+ };
468
+
469
+ const AttachmentUI: FC = () => {
470
+ const api = useAssistantApi();
471
+ const isComposer = api.attachment.source === "composer";
472
+
473
+ const isImage = useAssistantState(
474
+ ({ attachment }) => attachment.type === "image",
475
+ );
476
+ const typeLabel = useAssistantState(({ attachment }) => {
477
+ const type = attachment.type;
478
+ switch (type) {
479
+ case "image":
480
+ return "Image";
481
+ case "document":
482
+ return "Document";
483
+ case "file":
484
+ return "File";
485
+ default:
486
+ const _exhaustiveCheck: never = type;
487
+ throw new Error(`Unknown attachment type: ${_exhaustiveCheck}`);
488
+ }
489
+ });
490
+
491
+ return (
492
+ <Tooltip>
493
+ <AttachmentPrimitive.Root
494
+ className={cn(
495
+ "aui-attachment-root relative",
496
+ isImage &&
497
+ "aui-attachment-root-composer only:[&>#attachment-tile]:size-24",
498
+ )}
499
+ >
500
+ <AttachmentPreviewDialog>
501
+ <TooltipTrigger asChild>
502
+ <div
503
+ className={cn(
504
+ "aui-attachment-tile size-14 cursor-pointer overflow-hidden rounded-[14px] border bg-muted transition-opacity hover:opacity-75",
505
+ isComposer &&
506
+ "aui-attachment-tile-composer border-foreground/20",
507
+ )}
508
+ role="button"
509
+ id="attachment-tile"
510
+ aria-label={`${typeLabel} attachment`}
511
+ >
512
+ <AttachmentThumb />
513
+ </div>
514
+ </TooltipTrigger>
515
+ </AttachmentPreviewDialog>
516
+ {isComposer && <AttachmentRemove />}
517
+ </AttachmentPrimitive.Root>
518
+ <TooltipContent side="top">
519
+ <AttachmentPrimitive.Name />
520
+ </TooltipContent>
521
+ </Tooltip>
522
+ );
523
+ };
524
+
525
+ const AttachmentRemove: FC = () => {
526
+ return (
527
+ <AttachmentPrimitive.Remove asChild>
528
+ <TooltipIconButton
529
+ tooltip="Remove file"
530
+ 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"
531
+ side="top"
532
+ >
533
+ <XIcon className="aui-attachment-remove-icon size-3 dark:stroke-[2.5px]" />
534
+ </TooltipIconButton>
535
+ </AttachmentPrimitive.Remove>
536
+ );
537
+ };
538
+
539
+ export const UserMessageAttachments: FC = () => {
540
+ return (
541
+ <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">
542
+ <MessagePrimitive.Attachments components={{ Attachment: AttachmentUI }} />
543
+ </div>
544
+ );
545
+ };
546
+
547
+ export const ComposerAttachments: FC = () => {
548
+ return (
549
+ <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">
550
+ <ComposerPrimitive.Attachments
551
+ components={{ Attachment: AttachmentUI }}
552
+ />
553
+ </div>
554
+ );
555
+ };
556
+
557
+ export const ComposerAddAttachment: FC = () => {
558
+ return (
559
+ <ComposerPrimitive.AddAttachment asChild>
560
+ <TooltipIconButton
561
+ tooltip="Add Attachment"
562
+ side="bottom"
563
+ variant="ghost"
564
+ size="icon"
565
+ className="aui-composer-add-attachment size-[34px] rounded-full p-1 font-semibold text-xs hover:bg-muted-foreground/15 dark:border-muted-foreground/15 dark:hover:bg-muted-foreground/30"
566
+ aria-label="Add Attachment"
567
+ >
568
+ <PlusIcon className="aui-attachment-add-icon size-5 stroke-[1.5px]" />
569
+ </TooltipIconButton>
570
+ </ComposerPrimitive.AddAttachment>
571
+ );
572
+ };
573
+
574
+ ```
575
+
576
+ ## components/assistant-ui/markdown-text.tsx
577
+
578
+ ```tsx
579
+ "use client";
580
+
581
+ import "@assistant-ui/react-markdown/styles/dot.css";
582
+
583
+ import {
584
+ type CodeHeaderProps,
585
+ MarkdownTextPrimitive,
586
+ unstable_memoizeMarkdownComponents as memoizeMarkdownComponents,
587
+ useIsMarkdownCodeBlock,
588
+ } from "@assistant-ui/react-markdown";
589
+ import remarkGfm from "remark-gfm";
590
+ import { type FC, memo, useState } from "react";
591
+ import { CheckIcon, CopyIcon } from "lucide-react";
592
+
593
+ import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
594
+ import { cn } from "@/lib/utils";
595
+
596
+ const MarkdownTextImpl = () => {
597
+ return (
598
+ <MarkdownTextPrimitive
599
+ remarkPlugins={[remarkGfm]}
600
+ className="aui-md"
601
+ components={defaultComponents}
602
+ />
603
+ );
604
+ };
605
+
606
+ export const MarkdownText = memo(MarkdownTextImpl);
607
+
608
+ const CodeHeader: FC<CodeHeaderProps> = ({ language, code }) => {
609
+ const { isCopied, copyToClipboard } = useCopyToClipboard();
610
+ const onCopy = () => {
611
+ if (!code || isCopied) return;
612
+ copyToClipboard(code);
613
+ };
614
+
615
+ return (
616
+ <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 font-semibold text-foreground text-sm dark:bg-muted-foreground/20">
617
+ <span className="aui-code-header-language lowercase [&>span]:text-xs">
618
+ {language}
619
+ </span>
620
+ <TooltipIconButton tooltip="Copy" onClick={onCopy}>
621
+ {!isCopied && <CopyIcon />}
622
+ {isCopied && <CheckIcon />}
623
+ </TooltipIconButton>
624
+ </div>
625
+ );
626
+ };
627
+
628
+ const useCopyToClipboard = ({
629
+ copiedDuration = 3000,
630
+ }: {
631
+ copiedDuration?: number;
632
+ } = {}) => {
633
+ const [isCopied, setIsCopied] = useState<boolean>(false);
634
+
635
+ const copyToClipboard = (value: string) => {
636
+ if (!value) return;
637
+
638
+ navigator.clipboard.writeText(value).then(() => {
639
+ setIsCopied(true);
640
+ setTimeout(() => setIsCopied(false), copiedDuration);
641
+ });
642
+ };
643
+
644
+ return { isCopied, copyToClipboard };
645
+ };
646
+
647
+ const defaultComponents = memoizeMarkdownComponents({
648
+ h1: ({ className, ...props }) => (
649
+ <h1
650
+ className={cn(
651
+ "aui-md-h1 mb-8 scroll-m-20 font-extrabold text-4xl tracking-tight last:mb-0",
652
+ className,
653
+ )}
654
+ {...props}
655
+ />
656
+ ),
657
+ h2: ({ className, ...props }) => (
658
+ <h2
659
+ className={cn(
660
+ "aui-md-h2 mt-8 mb-4 scroll-m-20 font-semibold text-3xl tracking-tight first:mt-0 last:mb-0",
661
+ className,
662
+ )}
663
+ {...props}
664
+ />
665
+ ),
666
+ h3: ({ className, ...props }) => (
667
+ <h3
668
+ className={cn(
669
+ "aui-md-h3 mt-6 mb-4 scroll-m-20 font-semibold text-2xl tracking-tight first:mt-0 last:mb-0",
670
+ className,
671
+ )}
672
+ {...props}
673
+ />
674
+ ),
675
+ h4: ({ className, ...props }) => (
676
+ <h4
677
+ className={cn(
678
+ "aui-md-h4 mt-6 mb-4 scroll-m-20 font-semibold text-xl tracking-tight first:mt-0 last:mb-0",
679
+ className,
680
+ )}
681
+ {...props}
682
+ />
683
+ ),
684
+ h5: ({ className, ...props }) => (
685
+ <h5
686
+ className={cn(
687
+ "aui-md-h5 my-4 font-semibold text-lg first:mt-0 last:mb-0",
688
+ className,
689
+ )}
690
+ {...props}
691
+ />
692
+ ),
693
+ h6: ({ className, ...props }) => (
694
+ <h6
695
+ className={cn(
696
+ "aui-md-h6 my-4 font-semibold first:mt-0 last:mb-0",
697
+ className,
698
+ )}
699
+ {...props}
700
+ />
701
+ ),
702
+ p: ({ className, ...props }) => (
703
+ <p
704
+ className={cn(
705
+ "aui-md-p mt-5 mb-5 leading-7 first:mt-0 last:mb-0",
706
+ className,
707
+ )}
708
+ {...props}
709
+ />
710
+ ),
711
+ a: ({ className, ...props }) => (
712
+ <a
713
+ className={cn(
714
+ "aui-md-a font-medium text-primary underline underline-offset-4",
715
+ className,
716
+ )}
717
+ {...props}
718
+ />
719
+ ),
720
+ blockquote: ({ className, ...props }) => (
721
+ <blockquote
722
+ className={cn("aui-md-blockquote border-l-2 pl-6 italic", className)}
723
+ {...props}
724
+ />
725
+ ),
726
+ ul: ({ className, ...props }) => (
727
+ <ul
728
+ className={cn("aui-md-ul my-5 ml-6 list-disc [&>li]:mt-2", className)}
729
+ {...props}
730
+ />
731
+ ),
732
+ ol: ({ className, ...props }) => (
733
+ <ol
734
+ className={cn("aui-md-ol my-5 ml-6 list-decimal [&>li]:mt-2", className)}
735
+ {...props}
736
+ />
737
+ ),
738
+ hr: ({ className, ...props }) => (
739
+ <hr className={cn("aui-md-hr my-5 border-b", className)} {...props} />
740
+ ),
741
+ table: ({ className, ...props }) => (
742
+ <table
743
+ className={cn(
744
+ "aui-md-table my-5 w-full border-separate border-spacing-0 overflow-y-auto",
745
+ className,
746
+ )}
747
+ {...props}
748
+ />
749
+ ),
750
+ th: ({ className, ...props }) => (
751
+ <th
752
+ className={cn(
753
+ "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",
754
+ className,
755
+ )}
756
+ {...props}
757
+ />
758
+ ),
759
+ td: ({ className, ...props }) => (
760
+ <td
761
+ className={cn(
762
+ "aui-md-td border-b border-l px-4 py-2 text-left last:border-r [[align=center]]:text-center [[align=right]]:text-right",
763
+ className,
764
+ )}
765
+ {...props}
766
+ />
767
+ ),
768
+ tr: ({ className, ...props }) => (
769
+ <tr
770
+ className={cn(
771
+ "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",
772
+ className,
773
+ )}
774
+ {...props}
775
+ />
776
+ ),
777
+ sup: ({ className, ...props }) => (
778
+ <sup
779
+ className={cn("aui-md-sup [&>a]:text-xs [&>a]:no-underline", className)}
780
+ {...props}
781
+ />
782
+ ),
783
+ pre: ({ className, ...props }) => (
784
+ <pre
785
+ className={cn(
786
+ "aui-md-pre overflow-x-auto rounded-t-none! rounded-b-lg bg-black p-4 text-white",
787
+ className,
788
+ )}
789
+ {...props}
790
+ />
791
+ ),
792
+ code: function Code({ className, ...props }) {
793
+ const isCodeBlock = useIsMarkdownCodeBlock();
794
+ return (
795
+ <code
796
+ className={cn(
797
+ !isCodeBlock &&
798
+ "aui-md-inline-code rounded border bg-muted font-semibold",
799
+ className,
800
+ )}
801
+ {...props}
802
+ />
803
+ );
804
+ },
805
+ CodeHeader,
806
+ });
807
+
808
+ ```
809
+
810
+ ## components/assistant-ui/thread-list.tsx
811
+
812
+ ```tsx
813
+ import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
814
+ import { Button } from "@/components/ui/button";
815
+ import { Skeleton } from "@/components/ui/skeleton";
816
+ import {
817
+ AssistantIf,
818
+ ThreadListItemPrimitive,
819
+ ThreadListPrimitive,
820
+ } from "@assistant-ui/react";
821
+ import { ArchiveIcon, PlusIcon } from "lucide-react";
822
+ import type { FC } from "react";
823
+
824
+ export const ThreadList: FC = () => {
825
+ return (
826
+ <ThreadListPrimitive.Root className="aui-root aui-thread-list-root flex flex-col gap-1">
827
+ <ThreadListNew />
828
+ <AssistantIf condition={({ threads }) => threads.isLoading}>
829
+ <ThreadListSkeleton />
830
+ </AssistantIf>
831
+ <AssistantIf condition={({ threads }) => !threads.isLoading}>
832
+ <ThreadListPrimitive.Items components={{ ThreadListItem }} />
833
+ </AssistantIf>
834
+ </ThreadListPrimitive.Root>
835
+ );
836
+ };
837
+
838
+ const ThreadListNew: FC = () => {
839
+ return (
840
+ <ThreadListPrimitive.New asChild>
841
+ <Button
842
+ variant="outline"
843
+ className="aui-thread-list-new h-9 justify-start gap-2 rounded-lg px-3 text-sm hover:bg-muted data-active:bg-muted"
844
+ >
845
+ <PlusIcon className="size-4" />
846
+ New Thread
847
+ </Button>
848
+ </ThreadListPrimitive.New>
849
+ );
850
+ };
851
+
852
+ const ThreadListSkeleton: FC = () => {
853
+ return (
854
+ <div className="flex flex-col gap-1">
855
+ {Array.from({ length: 5 }, (_, i) => (
856
+ <div
857
+ key={i}
858
+ role="status"
859
+ aria-label="Loading threads"
860
+ className="aui-thread-list-skeleton-wrapper flex h-9 items-center px-3"
861
+ >
862
+ <Skeleton className="aui-thread-list-skeleton h-4 w-full" />
863
+ </div>
864
+ ))}
865
+ </div>
866
+ );
867
+ };
868
+
869
+ const ThreadListItem: FC = () => {
870
+ return (
871
+ <ThreadListItemPrimitive.Root className="aui-thread-list-item group flex h-9 items-center rounded-lg transition-colors hover:bg-muted focus-visible:bg-muted focus-visible:outline-none data-active:bg-muted">
872
+ <ThreadListItemPrimitive.Trigger className="aui-thread-list-item-trigger flex h-full flex-1 items-center truncate px-3 text-start text-sm">
873
+ <ThreadListItemPrimitive.Title fallback="New Chat" />
874
+ </ThreadListItemPrimitive.Trigger>
875
+ <ThreadListItemArchive />
876
+ </ThreadListItemPrimitive.Root>
877
+ );
878
+ };
879
+
880
+ const ThreadListItemArchive: FC = () => {
881
+ return (
882
+ <ThreadListItemPrimitive.Archive asChild>
883
+ <TooltipIconButton
884
+ variant="ghost"
885
+ tooltip="Archive thread"
886
+ className="aui-thread-list-item-archive mr-2 size-7 p-0 opacity-0 transition-opacity group-hover:opacity-100"
887
+ >
888
+ <ArchiveIcon className="size-4" />
889
+ </TooltipIconButton>
890
+ </ThreadListItemPrimitive.Archive>
891
+ );
892
+ };
893
+
894
+ ```
895
+
896
+ ## components/assistant-ui/thread.tsx
897
+
898
+ ```tsx
899
+ import {
900
+ ComposerAddAttachment,
901
+ ComposerAttachments,
902
+ UserMessageAttachments,
903
+ } from "@/components/assistant-ui/attachment";
904
+ import { MarkdownText } from "@/components/assistant-ui/markdown-text";
905
+ import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
906
+ import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
907
+ import { Button } from "@/components/ui/button";
908
+ import { cn } from "@/lib/utils";
909
+ import {
910
+ ActionBarPrimitive,
911
+ AssistantIf,
912
+ BranchPickerPrimitive,
913
+ ComposerPrimitive,
914
+ ErrorPrimitive,
915
+ MessagePrimitive,
916
+ ThreadPrimitive,
917
+ } from "@assistant-ui/react";
918
+ import {
919
+ ArrowDownIcon,
920
+ ArrowUpIcon,
921
+ CheckIcon,
922
+ ChevronLeftIcon,
923
+ ChevronRightIcon,
924
+ CopyIcon,
925
+ DownloadIcon,
926
+ PencilIcon,
927
+ RefreshCwIcon,
928
+ SquareIcon,
929
+ } from "lucide-react";
930
+ import type { FC } from "react";
931
+
932
+ export const Thread: FC = () => {
933
+ return (
934
+ <ThreadPrimitive.Root
935
+ className="aui-root aui-thread-root @container flex h-full flex-col bg-background"
936
+ style={{
937
+ ["--thread-max-width" as string]: "44rem",
938
+ }}
939
+ >
940
+ <ThreadPrimitive.Viewport
941
+ turnAnchor="top"
942
+ className="aui-thread-viewport relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll scroll-smooth px-4 pt-4"
943
+ >
944
+ <AssistantIf condition={({ thread }) => thread.isEmpty}>
945
+ <ThreadWelcome />
946
+ </AssistantIf>
947
+
948
+ <ThreadPrimitive.Messages
949
+ components={{
950
+ UserMessage,
951
+ EditComposer,
952
+ AssistantMessage,
953
+ }}
954
+ />
955
+
956
+ <ThreadPrimitive.ViewportFooter className="aui-thread-viewport-footer sticky bottom-0 mx-auto mt-auto flex w-full max-w-(--thread-max-width) flex-col gap-4 overflow-visible rounded-t-3xl bg-background pb-4 md:pb-6">
957
+ <ThreadScrollToBottom />
958
+ <Composer />
959
+ </ThreadPrimitive.ViewportFooter>
960
+ </ThreadPrimitive.Viewport>
961
+ </ThreadPrimitive.Root>
962
+ );
963
+ };
964
+
965
+ const ThreadScrollToBottom: FC = () => {
966
+ return (
967
+ <ThreadPrimitive.ScrollToBottom asChild>
968
+ <TooltipIconButton
969
+ tooltip="Scroll to bottom"
970
+ variant="outline"
971
+ className="aui-thread-scroll-to-bottom -top-12 absolute z-10 self-center rounded-full p-4 disabled:invisible dark:bg-background dark:hover:bg-accent"
972
+ >
973
+ <ArrowDownIcon />
974
+ </TooltipIconButton>
975
+ </ThreadPrimitive.ScrollToBottom>
976
+ );
977
+ };
978
+
979
+ const ThreadWelcome: FC = () => {
980
+ return (
981
+ <div className="aui-thread-welcome-root mx-auto my-auto flex w-full max-w-(--thread-max-width) grow flex-col">
982
+ <div className="aui-thread-welcome-center flex w-full grow flex-col items-center justify-center">
983
+ <div className="aui-thread-welcome-message flex size-full flex-col justify-center px-4">
984
+ <h1 className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in font-semibold text-2xl duration-200">
985
+ Hello there!
986
+ </h1>
987
+ <p className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in text-muted-foreground text-xl delay-75 duration-200">
988
+ How can I help you today?
989
+ </p>
990
+ </div>
991
+ </div>
992
+ <ThreadSuggestions />
993
+ </div>
994
+ );
995
+ };
996
+
997
+ const SUGGESTIONS = [
998
+ {
999
+ title: "What's the weather",
1000
+ label: "in San Francisco?",
1001
+ prompt: "What's the weather in San Francisco?",
1002
+ },
1003
+ {
1004
+ title: "Explain React hooks",
1005
+ label: "like useState and useEffect",
1006
+ prompt: "Explain React hooks like useState and useEffect",
1007
+ },
1008
+ ] as const;
1009
+
1010
+ const ThreadSuggestions: FC = () => {
1011
+ return (
1012
+ <div className="aui-thread-welcome-suggestions grid w-full @md:grid-cols-2 gap-2 pb-4">
1013
+ {SUGGESTIONS.map((suggestion, index) => (
1014
+ <div
1015
+ key={suggestion.prompt}
1016
+ className="aui-thread-welcome-suggestion-display fade-in slide-in-from-bottom-2 @md:nth-[n+3]:block nth-[n+3]:hidden animate-in fill-mode-both duration-200"
1017
+ style={{ animationDelay: `${100 + index * 50}ms` }}
1018
+ >
1019
+ <ThreadPrimitive.Suggestion prompt={suggestion.prompt} send asChild>
1020
+ <Button
1021
+ variant="ghost"
1022
+ className="aui-thread-welcome-suggestion h-auto w-full @md:flex-col flex-wrap items-start justify-start gap-1 rounded-2xl border px-4 py-3 text-left text-sm transition-colors hover:bg-muted"
1023
+ aria-label={suggestion.prompt}
1024
+ >
1025
+ <span className="aui-thread-welcome-suggestion-text-1 font-medium">
1026
+ {suggestion.title}
1027
+ </span>
1028
+ <span className="aui-thread-welcome-suggestion-text-2 text-muted-foreground">
1029
+ {suggestion.label}
1030
+ </span>
1031
+ </Button>
1032
+ </ThreadPrimitive.Suggestion>
1033
+ </div>
1034
+ ))}
1035
+ </div>
1036
+ );
1037
+ };
1038
+
1039
+ const Composer: FC = () => {
1040
+ return (
1041
+ <ComposerPrimitive.Root className="aui-composer-root relative flex w-full flex-col">
1042
+ <ComposerPrimitive.AttachmentDropzone className="aui-composer-attachment-dropzone flex w-full flex-col rounded-2xl border border-input bg-background px-1 pt-2 outline-none transition-shadow has-[textarea:focus-visible]:border-ring has-[textarea:focus-visible]:ring-2 has-[textarea:focus-visible]:ring-ring/20 data-[dragging=true]:border-ring data-[dragging=true]:border-dashed data-[dragging=true]:bg-accent/50">
1043
+ <ComposerAttachments />
1044
+ <ComposerPrimitive.Input
1045
+ placeholder="Send a message..."
1046
+ className="aui-composer-input mb-1 max-h-32 min-h-14 w-full resize-none bg-transparent px-4 pt-2 pb-3 text-sm outline-none placeholder:text-muted-foreground focus-visible:ring-0"
1047
+ rows={1}
1048
+ autoFocus
1049
+ aria-label="Message input"
1050
+ />
1051
+ <ComposerAction />
1052
+ </ComposerPrimitive.AttachmentDropzone>
1053
+ </ComposerPrimitive.Root>
1054
+ );
1055
+ };
1056
+
1057
+ const ComposerAction: FC = () => {
1058
+ return (
1059
+ <div className="aui-composer-action-wrapper relative mx-2 mb-2 flex items-center justify-between">
1060
+ <ComposerAddAttachment />
1061
+
1062
+ <AssistantIf condition={({ thread }) => !thread.isRunning}>
1063
+ <ComposerPrimitive.Send asChild>
1064
+ <TooltipIconButton
1065
+ tooltip="Send message"
1066
+ side="bottom"
1067
+ type="submit"
1068
+ variant="default"
1069
+ size="icon"
1070
+ className="aui-composer-send size-8 rounded-full"
1071
+ aria-label="Send message"
1072
+ >
1073
+ <ArrowUpIcon className="aui-composer-send-icon size-4" />
1074
+ </TooltipIconButton>
1075
+ </ComposerPrimitive.Send>
1076
+ </AssistantIf>
1077
+
1078
+ <AssistantIf condition={({ thread }) => thread.isRunning}>
1079
+ <ComposerPrimitive.Cancel asChild>
1080
+ <Button
1081
+ type="button"
1082
+ variant="default"
1083
+ size="icon"
1084
+ className="aui-composer-cancel size-8 rounded-full"
1085
+ aria-label="Stop generating"
1086
+ >
1087
+ <SquareIcon className="aui-composer-cancel-icon size-3 fill-current" />
1088
+ </Button>
1089
+ </ComposerPrimitive.Cancel>
1090
+ </AssistantIf>
1091
+ </div>
1092
+ );
1093
+ };
1094
+
1095
+ const MessageError: FC = () => {
1096
+ return (
1097
+ <MessagePrimitive.Error>
1098
+ <ErrorPrimitive.Root className="aui-message-error-root mt-2 rounded-md border border-destructive bg-destructive/10 p-3 text-destructive text-sm dark:bg-destructive/5 dark:text-red-200">
1099
+ <ErrorPrimitive.Message className="aui-message-error-message line-clamp-2" />
1100
+ </ErrorPrimitive.Root>
1101
+ </MessagePrimitive.Error>
1102
+ );
1103
+ };
1104
+
1105
+ const AssistantMessage: FC = () => {
1106
+ return (
1107
+ <MessagePrimitive.Root
1108
+ className="aui-assistant-message-root fade-in slide-in-from-bottom-1 relative mx-auto w-full max-w-(--thread-max-width) animate-in py-3 duration-150"
1109
+ data-role="assistant"
1110
+ >
1111
+ <div className="aui-assistant-message-content wrap-break-word px-2 text-foreground leading-relaxed">
1112
+ <MessagePrimitive.Parts
1113
+ components={{
1114
+ Text: MarkdownText,
1115
+ tools: { Fallback: ToolFallback },
1116
+ }}
1117
+ />
1118
+ <MessageError />
1119
+ </div>
1120
+
1121
+ <div className="aui-assistant-message-footer mt-1 ml-2 flex">
1122
+ <BranchPicker />
1123
+ <AssistantActionBar />
1124
+ </div>
1125
+ </MessagePrimitive.Root>
1126
+ );
1127
+ };
1128
+
1129
+ const AssistantActionBar: FC = () => {
1130
+ return (
1131
+ <ActionBarPrimitive.Root
1132
+ hideWhenRunning
1133
+ autohide="not-last"
1134
+ autohideFloat="single-branch"
1135
+ className="aui-assistant-action-bar-root -ml-1 col-start-3 row-start-2 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"
1136
+ >
1137
+ <ActionBarPrimitive.Copy asChild>
1138
+ <TooltipIconButton tooltip="Copy">
1139
+ <AssistantIf condition={({ message }) => message.isCopied}>
1140
+ <CheckIcon />
1141
+ </AssistantIf>
1142
+ <AssistantIf condition={({ message }) => !message.isCopied}>
1143
+ <CopyIcon />
1144
+ </AssistantIf>
1145
+ </TooltipIconButton>
1146
+ </ActionBarPrimitive.Copy>
1147
+ <ActionBarPrimitive.ExportMarkdown asChild>
1148
+ <TooltipIconButton tooltip="Export as Markdown">
1149
+ <DownloadIcon />
1150
+ </TooltipIconButton>
1151
+ </ActionBarPrimitive.ExportMarkdown>
1152
+ <ActionBarPrimitive.Reload asChild>
1153
+ <TooltipIconButton tooltip="Refresh">
1154
+ <RefreshCwIcon />
1155
+ </TooltipIconButton>
1156
+ </ActionBarPrimitive.Reload>
1157
+ </ActionBarPrimitive.Root>
1158
+ );
1159
+ };
1160
+
1161
+ const UserMessage: FC = () => {
1162
+ return (
1163
+ <MessagePrimitive.Root
1164
+ className="aui-user-message-root fade-in slide-in-from-bottom-1 mx-auto grid w-full max-w-(--thread-max-width) animate-in auto-rows-auto grid-cols-[minmax(72px,1fr)_auto] content-start gap-y-2 px-2 py-3 duration-150 [&:where(>*)]:col-start-2"
1165
+ data-role="user"
1166
+ >
1167
+ <UserMessageAttachments />
1168
+
1169
+ <div className="aui-user-message-content-wrapper relative col-start-2 min-w-0">
1170
+ <div className="aui-user-message-content wrap-break-word rounded-2xl bg-muted px-4 py-2.5 text-foreground">
1171
+ <MessagePrimitive.Parts />
1172
+ </div>
1173
+ <div className="aui-user-action-bar-wrapper -translate-x-full -translate-y-1/2 absolute top-1/2 left-0 pr-2">
1174
+ <UserActionBar />
1175
+ </div>
1176
+ </div>
1177
+
1178
+ <BranchPicker className="aui-user-branch-picker -mr-1 col-span-full col-start-1 row-start-3 justify-end" />
1179
+ </MessagePrimitive.Root>
1180
+ );
1181
+ };
1182
+
1183
+ const UserActionBar: FC = () => {
1184
+ return (
1185
+ <ActionBarPrimitive.Root
1186
+ hideWhenRunning
1187
+ autohide="not-last"
1188
+ className="aui-user-action-bar-root flex flex-col items-end"
1189
+ >
1190
+ <ActionBarPrimitive.Edit asChild>
1191
+ <TooltipIconButton tooltip="Edit" className="aui-user-action-edit p-4">
1192
+ <PencilIcon />
1193
+ </TooltipIconButton>
1194
+ </ActionBarPrimitive.Edit>
1195
+ </ActionBarPrimitive.Root>
1196
+ );
1197
+ };
1198
+
1199
+ const EditComposer: FC = () => {
1200
+ return (
1201
+ <MessagePrimitive.Root className="aui-edit-composer-wrapper mx-auto flex w-full max-w-(--thread-max-width) flex-col px-2 py-3">
1202
+ <ComposerPrimitive.Root className="aui-edit-composer-root ml-auto flex w-full max-w-[85%] flex-col rounded-2xl bg-muted">
1203
+ <ComposerPrimitive.Input
1204
+ className="aui-edit-composer-input min-h-14 w-full resize-none bg-transparent p-4 text-foreground text-sm outline-none"
1205
+ autoFocus
1206
+ />
1207
+ <div className="aui-edit-composer-footer mx-3 mb-3 flex items-center gap-2 self-end">
1208
+ <ComposerPrimitive.Cancel asChild>
1209
+ <Button variant="ghost" size="sm">
1210
+ Cancel
1211
+ </Button>
1212
+ </ComposerPrimitive.Cancel>
1213
+ <ComposerPrimitive.Send asChild>
1214
+ <Button size="sm">Update</Button>
1215
+ </ComposerPrimitive.Send>
1216
+ </div>
1217
+ </ComposerPrimitive.Root>
1218
+ </MessagePrimitive.Root>
1219
+ );
1220
+ };
1221
+
1222
+ const BranchPicker: FC<BranchPickerPrimitive.Root.Props> = ({
1223
+ className,
1224
+ ...rest
1225
+ }) => {
1226
+ return (
1227
+ <BranchPickerPrimitive.Root
1228
+ hideWhenSingleBranch
1229
+ className={cn(
1230
+ "aui-branch-picker-root -ml-2 mr-2 inline-flex items-center text-muted-foreground text-xs",
1231
+ className,
1232
+ )}
1233
+ {...rest}
1234
+ >
1235
+ <BranchPickerPrimitive.Previous asChild>
1236
+ <TooltipIconButton tooltip="Previous">
1237
+ <ChevronLeftIcon />
1238
+ </TooltipIconButton>
1239
+ </BranchPickerPrimitive.Previous>
1240
+ <span className="aui-branch-picker-state font-medium">
1241
+ <BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />
1242
+ </span>
1243
+ <BranchPickerPrimitive.Next asChild>
1244
+ <TooltipIconButton tooltip="Next">
1245
+ <ChevronRightIcon />
1246
+ </TooltipIconButton>
1247
+ </BranchPickerPrimitive.Next>
1248
+ </BranchPickerPrimitive.Root>
1249
+ );
1250
+ };
1251
+
1252
+ ```
1253
+
1254
+ ## components/assistant-ui/tool-fallback.tsx
1255
+
1256
+ ```tsx
1257
+ import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
1258
+ import {
1259
+ CheckIcon,
1260
+ ChevronDownIcon,
1261
+ ChevronUpIcon,
1262
+ XCircleIcon,
1263
+ } from "lucide-react";
1264
+ import { useState } from "react";
1265
+ import { Button } from "@/components/ui/button";
1266
+ import { cn } from "@/lib/utils";
1267
+
1268
+ export const ToolFallback: ToolCallMessagePartComponent = ({
1269
+ toolName,
1270
+ argsText,
1271
+ result,
1272
+ status,
1273
+ }) => {
1274
+ const [isCollapsed, setIsCollapsed] = useState(true);
1275
+
1276
+ const isCancelled =
1277
+ status?.type === "incomplete" && status.reason === "cancelled";
1278
+ const cancelledReason =
1279
+ isCancelled && status.error
1280
+ ? typeof status.error === "string"
1281
+ ? status.error
1282
+ : JSON.stringify(status.error)
1283
+ : null;
1284
+
1285
+ return (
1286
+ <div
1287
+ className={cn(
1288
+ "aui-tool-fallback-root mb-4 flex w-full flex-col gap-3 rounded-lg border py-3",
1289
+ isCancelled && "border-muted-foreground/30 bg-muted/30",
1290
+ )}
1291
+ >
1292
+ <div className="aui-tool-fallback-header flex items-center gap-2 px-4">
1293
+ {isCancelled ? (
1294
+ <XCircleIcon className="aui-tool-fallback-icon size-4 text-muted-foreground" />
1295
+ ) : (
1296
+ <CheckIcon className="aui-tool-fallback-icon size-4" />
1297
+ )}
1298
+ <p
1299
+ className={cn(
1300
+ "aui-tool-fallback-title grow",
1301
+ isCancelled && "text-muted-foreground line-through",
1302
+ )}
1303
+ >
1304
+ {isCancelled ? "Cancelled tool: " : "Used tool: "}
1305
+ <b>{toolName}</b>
1306
+ </p>
1307
+ <Button onClick={() => setIsCollapsed(!isCollapsed)}>
1308
+ {isCollapsed ? <ChevronUpIcon /> : <ChevronDownIcon />}
1309
+ </Button>
1310
+ </div>
1311
+ {!isCollapsed && (
1312
+ <div className="aui-tool-fallback-content flex flex-col gap-2 border-t pt-2">
1313
+ {cancelledReason && (
1314
+ <div className="aui-tool-fallback-cancelled-root px-4">
1315
+ <p className="aui-tool-fallback-cancelled-header font-semibold text-muted-foreground">
1316
+ Cancelled reason:
1317
+ </p>
1318
+ <p className="aui-tool-fallback-cancelled-reason text-muted-foreground">
1319
+ {cancelledReason}
1320
+ </p>
1321
+ </div>
1322
+ )}
1323
+ <div
1324
+ className={cn(
1325
+ "aui-tool-fallback-args-root px-4",
1326
+ isCancelled && "opacity-60",
1327
+ )}
1328
+ >
1329
+ <pre className="aui-tool-fallback-args-value whitespace-pre-wrap">
1330
+ {argsText}
1331
+ </pre>
1332
+ </div>
1333
+ {!isCancelled && result !== undefined && (
1334
+ <div className="aui-tool-fallback-result-root border-t border-dashed px-4 pt-2">
1335
+ <p className="aui-tool-fallback-result-header font-semibold">
1336
+ Result:
1337
+ </p>
1338
+ <pre className="aui-tool-fallback-result-content whitespace-pre-wrap">
1339
+ {typeof result === "string"
1340
+ ? result
1341
+ : JSON.stringify(result, null, 2)}
1342
+ </pre>
1343
+ </div>
1344
+ )}
1345
+ </div>
1346
+ )}
1347
+ </div>
1348
+ );
1349
+ };
1350
+
1351
+ ```
1352
+
1353
+ ## components/assistant-ui/tooltip-icon-button.tsx
1354
+
1355
+ ```tsx
1356
+ "use client";
1357
+
1358
+ import { ComponentPropsWithRef, forwardRef } from "react";
1359
+ import { Slottable } from "@radix-ui/react-slot";
1360
+
1361
+ import {
1362
+ Tooltip,
1363
+ TooltipContent,
1364
+ TooltipTrigger,
1365
+ } from "@/components/ui/tooltip";
1366
+ import { Button } from "@/components/ui/button";
1367
+ import { cn } from "@/lib/utils";
1368
+
1369
+ export type TooltipIconButtonProps = ComponentPropsWithRef<typeof Button> & {
1370
+ tooltip: string;
1371
+ side?: "top" | "bottom" | "left" | "right";
1372
+ };
1373
+
1374
+ export const TooltipIconButton = forwardRef<
1375
+ HTMLButtonElement,
1376
+ TooltipIconButtonProps
1377
+ >(({ children, tooltip, side = "bottom", className, ...rest }, ref) => {
1378
+ return (
1379
+ <Tooltip>
1380
+ <TooltipTrigger asChild>
1381
+ <Button
1382
+ variant="ghost"
1383
+ size="icon"
1384
+ {...rest}
1385
+ className={cn("aui-button-icon size-6 p-1", className)}
1386
+ ref={ref}
1387
+ >
1388
+ <Slottable>{children}</Slottable>
1389
+ <span className="aui-sr-only sr-only">{tooltip}</span>
1390
+ </Button>
1391
+ </TooltipTrigger>
1392
+ <TooltipContent side={side}>{tooltip}</TooltipContent>
1393
+ </Tooltip>
1394
+ );
1395
+ });
1396
+
1397
+ TooltipIconButton.displayName = "TooltipIconButton";
1398
+
1399
+ ```
1400
+
1401
+ ## components/ui/avatar.tsx
1402
+
1403
+ ```tsx
1404
+ "use client";
1405
+
1406
+ import * as React from "react";
1407
+ import * as AvatarPrimitive from "@radix-ui/react-avatar";
1408
+
1409
+ import { cn } from "@/lib/utils";
1410
+
1411
+ const Avatar = React.forwardRef<
1412
+ React.ElementRef<typeof AvatarPrimitive.Root>,
1413
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
1414
+ >(({ className, ...props }, ref) => (
1415
+ <AvatarPrimitive.Root
1416
+ ref={ref}
1417
+ className={cn(
1418
+ "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
1419
+ className,
1420
+ )}
1421
+ {...props}
1422
+ />
1423
+ ));
1424
+ Avatar.displayName = AvatarPrimitive.Root.displayName;
1425
+
1426
+ const AvatarImage = React.forwardRef<
1427
+ React.ElementRef<typeof AvatarPrimitive.Image>,
1428
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
1429
+ >(({ className, ...props }, ref) => (
1430
+ <AvatarPrimitive.Image
1431
+ ref={ref}
1432
+ className={cn("aspect-square h-full w-full", className)}
1433
+ {...props}
1434
+ />
1435
+ ));
1436
+ AvatarImage.displayName = AvatarPrimitive.Image.displayName;
1437
+
1438
+ const AvatarFallback = React.forwardRef<
1439
+ React.ElementRef<typeof AvatarPrimitive.Fallback>,
1440
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
1441
+ >(({ className, ...props }, ref) => (
1442
+ <AvatarPrimitive.Fallback
1443
+ ref={ref}
1444
+ className={cn(
1445
+ "flex h-full w-full items-center justify-center rounded-full bg-muted",
1446
+ className,
1447
+ )}
1448
+ {...props}
1449
+ />
1450
+ ));
1451
+ AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
1452
+
1453
+ export { Avatar, AvatarImage, AvatarFallback };
1454
+
1455
+ ```
1456
+
1457
+ ## components/ui/button.tsx
1458
+
1459
+ ```tsx
1460
+ import * as React from "react";
1461
+ import { Slot } from "@radix-ui/react-slot";
1462
+ import { cva, type VariantProps } from "class-variance-authority";
1463
+
1464
+ import { cn } from "@/lib/utils";
1465
+
1466
+ const buttonVariants = cva(
1467
+ "inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
1468
+ {
1469
+ variants: {
1470
+ variant: {
1471
+ default:
1472
+ "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
1473
+ destructive:
1474
+ "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
1475
+ outline:
1476
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
1477
+ secondary:
1478
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
1479
+ ghost:
1480
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
1481
+ link: "text-primary underline-offset-4 hover:underline",
1482
+ },
1483
+ size: {
1484
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
1485
+ sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
1486
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
1487
+ icon: "size-9",
1488
+ },
1489
+ },
1490
+ defaultVariants: {
1491
+ variant: "default",
1492
+ size: "default",
1493
+ },
1494
+ },
1495
+ );
1496
+
1497
+ function Button({
1498
+ className,
1499
+ variant,
1500
+ size,
1501
+ asChild = false,
1502
+ ...props
1503
+ }: React.ComponentProps<"button"> &
1504
+ VariantProps<typeof buttonVariants> & {
1505
+ asChild?: boolean;
1506
+ }) {
1507
+ const Comp = asChild ? Slot : "button";
1508
+
1509
+ return (
1510
+ <Comp
1511
+ data-slot="button"
1512
+ className={cn(buttonVariants({ variant, size, className }))}
1513
+ {...props}
1514
+ />
1515
+ );
1516
+ }
1517
+
1518
+ export { Button, buttonVariants };
1519
+
1520
+ ```
1521
+
1522
+ ## components/ui/dialog.tsx
1523
+
1524
+ ```tsx
1525
+ "use client";
1526
+
1527
+ import * as React from "react";
1528
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
1529
+ import { XIcon } from "lucide-react";
1530
+
1531
+ import { cn } from "@/lib/utils";
1532
+
1533
+ function Dialog({
1534
+ ...props
1535
+ }: React.ComponentProps<typeof DialogPrimitive.Root>) {
1536
+ return <DialogPrimitive.Root data-slot="dialog" {...props} />;
1537
+ }
1538
+
1539
+ function DialogTrigger({
1540
+ ...props
1541
+ }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
1542
+ return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
1543
+ }
1544
+
1545
+ function DialogPortal({
1546
+ ...props
1547
+ }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
1548
+ return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
1549
+ }
1550
+
1551
+ function DialogClose({
1552
+ ...props
1553
+ }: React.ComponentProps<typeof DialogPrimitive.Close>) {
1554
+ return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
1555
+ }
1556
+
1557
+ function DialogOverlay({
1558
+ className,
1559
+ ...props
1560
+ }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
1561
+ return (
1562
+ <DialogPrimitive.Overlay
1563
+ data-slot="dialog-overlay"
1564
+ className={cn(
1565
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80 data-[state=closed]:animate-out data-[state=open]:animate-in",
1566
+ className,
1567
+ )}
1568
+ {...props}
1569
+ />
1570
+ );
1571
+ }
1572
+
1573
+ function DialogContent({
1574
+ className,
1575
+ children,
1576
+ ...props
1577
+ }: React.ComponentProps<typeof DialogPrimitive.Content>) {
1578
+ return (
1579
+ <DialogPortal data-slot="dialog-portal">
1580
+ <DialogOverlay />
1581
+ <DialogPrimitive.Content
1582
+ data-slot="dialog-content"
1583
+ className={cn(
1584
+ "data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 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=open]:animate-in sm:max-w-lg",
1585
+ className,
1586
+ )}
1587
+ {...props}
1588
+ >
1589
+ {children}
1590
+ <DialogPrimitive.Close className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0">
1591
+ <XIcon />
1592
+ <span className="sr-only">Close</span>
1593
+ </DialogPrimitive.Close>
1594
+ </DialogPrimitive.Content>
1595
+ </DialogPortal>
1596
+ );
1597
+ }
1598
+
1599
+ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
1600
+ return (
1601
+ <div
1602
+ data-slot="dialog-header"
1603
+ className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
1604
+ {...props}
1605
+ />
1606
+ );
1607
+ }
1608
+
1609
+ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
1610
+ return (
1611
+ <div
1612
+ data-slot="dialog-footer"
1613
+ className={cn(
1614
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
1615
+ className,
1616
+ )}
1617
+ {...props}
1618
+ />
1619
+ );
1620
+ }
1621
+
1622
+ function DialogTitle({
1623
+ className,
1624
+ ...props
1625
+ }: React.ComponentProps<typeof DialogPrimitive.Title>) {
1626
+ return (
1627
+ <DialogPrimitive.Title
1628
+ data-slot="dialog-title"
1629
+ className={cn("font-semibold text-lg leading-none", className)}
1630
+ {...props}
1631
+ />
1632
+ );
1633
+ }
1634
+
1635
+ function DialogDescription({
1636
+ className,
1637
+ ...props
1638
+ }: React.ComponentProps<typeof DialogPrimitive.Description>) {
1639
+ return (
1640
+ <DialogPrimitive.Description
1641
+ data-slot="dialog-description"
1642
+ className={cn("text-muted-foreground text-sm", className)}
1643
+ {...props}
1644
+ />
1645
+ );
1646
+ }
1647
+
1648
+ export {
1649
+ Dialog,
1650
+ DialogClose,
1651
+ DialogContent,
1652
+ DialogDescription,
1653
+ DialogFooter,
1654
+ DialogHeader,
1655
+ DialogOverlay,
1656
+ DialogPortal,
1657
+ DialogTitle,
1658
+ DialogTrigger,
1659
+ };
1660
+
1661
+ ```
1662
+
1663
+ ## components/ui/skeleton.tsx
1664
+
1665
+ ```tsx
1666
+ import { cn } from "@/lib/utils";
1667
+
1668
+ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
1669
+ return (
1670
+ <div
1671
+ data-slot="skeleton"
1672
+ className={cn("animate-pulse rounded-md bg-accent", className)}
1673
+ {...props}
1674
+ />
1675
+ );
1676
+ }
1677
+
1678
+ export { Skeleton };
1679
+
1680
+ ```
1681
+
1682
+ ## components/ui/tooltip.tsx
1683
+
1684
+ ```tsx
1685
+ "use client";
1686
+
1687
+ import * as React from "react";
1688
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
1689
+
1690
+ import { cn } from "@/lib/utils";
1691
+
1692
+ function TooltipProvider({
1693
+ delayDuration = 0,
1694
+ ...props
1695
+ }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
1696
+ return (
1697
+ <TooltipPrimitive.Provider
1698
+ data-slot="tooltip-provider"
1699
+ delayDuration={delayDuration}
1700
+ {...props}
1701
+ />
1702
+ );
1703
+ }
1704
+
1705
+ function Tooltip({
1706
+ ...props
1707
+ }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
1708
+ return (
1709
+ <TooltipProvider>
1710
+ <TooltipPrimitive.Root data-slot="tooltip" {...props} />
1711
+ </TooltipProvider>
1712
+ );
1713
+ }
1714
+
1715
+ function TooltipTrigger({
1716
+ ...props
1717
+ }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
1718
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
1719
+ }
1720
+
1721
+ function TooltipContent({
1722
+ className,
1723
+ sideOffset = 0,
1724
+ children,
1725
+ ...props
1726
+ }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
1727
+ return (
1728
+ <TooltipPrimitive.Portal>
1729
+ <TooltipPrimitive.Content
1730
+ data-slot="tooltip-content"
1731
+ sideOffset={sideOffset}
1732
+ className={cn(
1733
+ "fade-in-0 zoom-in-95 data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in text-balance rounded-md bg-primary px-3 py-1.5 text-primary-foreground text-xs data-[state=closed]:animate-out",
1734
+ className,
1735
+ )}
1736
+ {...props}
1737
+ >
1738
+ {children}
1739
+ <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-primary fill-primary" />
1740
+ </TooltipPrimitive.Content>
1741
+ </TooltipPrimitive.Portal>
1742
+ );
1743
+ }
1744
+
1745
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
1746
+
1747
+ ```
1748
+
1749
+ ## lib/utils.ts
1750
+
1751
+ ```typescript
1752
+ import { clsx, type ClassValue } from "clsx";
1753
+ import { twMerge } from "tailwind-merge";
1754
+
1755
+ export function cn(...inputs: ClassValue[]) {
1756
+ return twMerge(clsx(inputs));
1757
+ }
1758
+
1759
+ ```
1760
+
1761
+ ## next.config.ts
1762
+
1763
+ ```typescript
1764
+ import type { NextConfig } from "next";
1765
+
1766
+ const nextConfig: NextConfig = {};
1767
+
1768
+ export default nextConfig;
1769
+
1770
+ ```
1771
+
1772
+ ## package.json
1773
+
1774
+ ```json
1775
+ {
1776
+ "name": "example-with-custom-thread-list",
1777
+ "private": true,
1778
+ "version": "0.0.0",
1779
+ "type": "module",
1780
+ "dependencies": {
1781
+ "@ai-sdk/openai": "^2.0.77",
1782
+ "@ai-sdk/react": "^2.0.107",
1783
+ "@assistant-ui/react": "workspace:^",
1784
+ "@assistant-ui/react-ai-sdk": "workspace:*",
1785
+ "@assistant-ui/react-markdown": "workspace:^",
1786
+ "@radix-ui/react-avatar": "^1.1.11",
1787
+ "@radix-ui/react-dialog": "^1.1.15",
1788
+ "@radix-ui/react-slot": "^1.2.4",
1789
+ "@radix-ui/react-tooltip": "^1.2.8",
1790
+ "@tailwindcss/postcss": "^4.1.17",
1791
+ "ai": "^5.0.107",
1792
+ "assistant-stream": "workspace:*",
1793
+ "class-variance-authority": "^0.7.1",
1794
+ "clsx": "^2.1.1",
1795
+ "lucide-react": "^0.556.0",
1796
+ "next": "16.0.7",
1797
+ "postcss": "^8.5.6",
1798
+ "react": "19.2.1",
1799
+ "react-dom": "19.2.1",
1800
+ "remark-gfm": "^4.0.1",
1801
+ "tailwind-merge": "^3.4.0",
1802
+ "tailwindcss": "^4.1.17",
1803
+ "zod": "^4.1.13",
1804
+ "zustand": "^5.0.9"
1805
+ },
1806
+ "devDependencies": {
1807
+ "@assistant-ui/x-buildutils": "workspace:*",
1808
+ "@types/node": "^24.10.1",
1809
+ "@types/react": "^19.2.7",
1810
+ "@types/react-dom": "^19.2.3",
1811
+ "tw-animate-css": "^1.4.0",
1812
+ "typescript": "^5.9.3"
1813
+ },
1814
+ "scripts": {
1815
+ "dev": "next dev",
1816
+ "build": "next build",
1817
+ "start": "next start",
1818
+ "lint": "eslint ."
1819
+ }
1820
+ }
1821
+
1822
+ ```
1823
+
1824
+ ## tsconfig.json
1825
+
1826
+ ```json
1827
+ {
1828
+ "extends": "@assistant-ui/x-buildutils/ts/base",
1829
+ "compilerOptions": {
1830
+ "target": "ES6",
1831
+ "module": "ESNext",
1832
+ "incremental": true,
1833
+ "plugins": [
1834
+ {
1835
+ "name": "next"
1836
+ }
1837
+ ],
1838
+ "allowJs": true,
1839
+ "strictNullChecks": true,
1840
+ "jsx": "preserve",
1841
+ "paths": {
1842
+ "@/*": ["./*"],
1843
+ "@assistant-ui/*": ["../../packages/*/src"],
1844
+ "@assistant-ui/react/*": ["../../packages/react/src/*"],
1845
+ "@assistant-ui/tap/*": ["../../packages/tap/src/*"],
1846
+ "assistant-stream": ["../../packages/assistant-stream/src"],
1847
+ "assistant-stream/*": ["../../packages/assistant-stream/src/*"]
1848
+ }
1849
+ },
1850
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
1851
+ "exclude": ["node_modules"]
1852
+ }
1853
+
1854
+ ```
1855
+