@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,1574 @@
1
+ # Example: with-tanstack
2
+
3
+ ## .cta.json
4
+
5
+ ```json
6
+ {
7
+ "projectName": "assistant-ui-with-tanstack",
8
+ "mode": "file-router",
9
+ "typescript": true,
10
+ "tailwind": true,
11
+ "packageManager": "pnpm",
12
+ "addOnOptions": {},
13
+ "git": true,
14
+ "version": 1,
15
+ "framework": "react-cra",
16
+ "chosenAddOns": ["nitro", "start"]
17
+ }
18
+
19
+ ```
20
+
21
+ ## package.json
22
+
23
+ ```json
24
+ {
25
+ "name": "assistant-ui-with-tanstack",
26
+ "private": true,
27
+ "type": "module",
28
+ "scripts": {
29
+ "dev": "vite dev --port 3000",
30
+ "build": "vite build",
31
+ "serve": "vite preview"
32
+ },
33
+ "dependencies": {
34
+ "@assistant-ui/react": "workspace:*",
35
+ "@assistant-ui/react-markdown": "workspace:*",
36
+ "@radix-ui/react-slot": "^1.2.4",
37
+ "@radix-ui/react-tooltip": "^1.2.8",
38
+ "@tailwindcss/vite": "^4.1.17",
39
+ "@tanstack/react-router": "^1.139.14",
40
+ "@tanstack/react-start": "^1.139.14",
41
+ "@tanstack/router-plugin": "^1.139.14",
42
+ "class-variance-authority": "^0.7.1",
43
+ "clsx": "^2.1.1",
44
+ "lucide-react": "^0.555.0",
45
+ "nitro": "latest",
46
+ "openai": "^6.10.0",
47
+ "react": "^19.2.1",
48
+ "react-dom": "^19.2.1",
49
+ "remark-gfm": "^4.0.1",
50
+ "tailwind-merge": "^3.4.0",
51
+ "tailwindcss": "^4.1.17",
52
+ "vite-tsconfig-paths": "^5.1.4"
53
+ },
54
+ "devDependencies": {
55
+ "@types/node": "^22.19.1",
56
+ "@types/react": "^19.2.7",
57
+ "@types/react-dom": "^19.2.3",
58
+ "@vitejs/plugin-react": "^5.1.1",
59
+ "typescript": "^5.9.3",
60
+ "vite": "^7.2.6"
61
+ }
62
+ }
63
+
64
+ ```
65
+
66
+ ## src/components/assistant-ui/markdown-text.tsx
67
+
68
+ ```tsx
69
+ import "@assistant-ui/react-markdown/styles/dot.css";
70
+
71
+ import {
72
+ type CodeHeaderProps,
73
+ MarkdownTextPrimitive,
74
+ unstable_memoizeMarkdownComponents as memoizeMarkdownComponents,
75
+ useIsMarkdownCodeBlock,
76
+ } from "@assistant-ui/react-markdown";
77
+ import remarkGfm from "remark-gfm";
78
+ import { type FC, memo, useState } from "react";
79
+ import { CheckIcon, CopyIcon } from "lucide-react";
80
+
81
+ import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
82
+ import { cn } from "@/lib/utils";
83
+
84
+ const MarkdownTextImpl = () => {
85
+ return (
86
+ <MarkdownTextPrimitive
87
+ remarkPlugins={[remarkGfm]}
88
+ className="aui-md"
89
+ components={defaultComponents}
90
+ />
91
+ );
92
+ };
93
+
94
+ export const MarkdownText = memo(MarkdownTextImpl);
95
+
96
+ const CodeHeader: FC<CodeHeaderProps> = ({ language, code }) => {
97
+ const { isCopied, copyToClipboard } = useCopyToClipboard();
98
+ const onCopy = () => {
99
+ if (!code || isCopied) return;
100
+ copyToClipboard(code);
101
+ };
102
+
103
+ return (
104
+ <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">
105
+ <span className="aui-code-header-language lowercase [&>span]:text-xs">
106
+ {language}
107
+ </span>
108
+ <TooltipIconButton tooltip="Copy" onClick={onCopy}>
109
+ {!isCopied && <CopyIcon />}
110
+ {isCopied && <CheckIcon />}
111
+ </TooltipIconButton>
112
+ </div>
113
+ );
114
+ };
115
+
116
+ const useCopyToClipboard = ({
117
+ copiedDuration = 3000,
118
+ }: {
119
+ copiedDuration?: number;
120
+ } = {}) => {
121
+ const [isCopied, setIsCopied] = useState<boolean>(false);
122
+
123
+ const copyToClipboard = (value: string) => {
124
+ if (!value) return;
125
+
126
+ navigator.clipboard.writeText(value).then(() => {
127
+ setIsCopied(true);
128
+ setTimeout(() => setIsCopied(false), copiedDuration);
129
+ });
130
+ };
131
+
132
+ return { isCopied, copyToClipboard };
133
+ };
134
+
135
+ const defaultComponents = memoizeMarkdownComponents({
136
+ h1: ({ className, ...props }) => (
137
+ <h1
138
+ className={cn(
139
+ "aui-md-h1 mb-8 scroll-m-20 font-extrabold text-4xl tracking-tight last:mb-0",
140
+ className,
141
+ )}
142
+ {...props}
143
+ />
144
+ ),
145
+ h2: ({ className, ...props }) => (
146
+ <h2
147
+ className={cn(
148
+ "aui-md-h2 mt-8 mb-4 scroll-m-20 font-semibold text-3xl tracking-tight first:mt-0 last:mb-0",
149
+ className,
150
+ )}
151
+ {...props}
152
+ />
153
+ ),
154
+ h3: ({ className, ...props }) => (
155
+ <h3
156
+ className={cn(
157
+ "aui-md-h3 mt-6 mb-4 scroll-m-20 font-semibold text-2xl tracking-tight first:mt-0 last:mb-0",
158
+ className,
159
+ )}
160
+ {...props}
161
+ />
162
+ ),
163
+ h4: ({ className, ...props }) => (
164
+ <h4
165
+ className={cn(
166
+ "aui-md-h4 mt-6 mb-4 scroll-m-20 font-semibold text-xl tracking-tight first:mt-0 last:mb-0",
167
+ className,
168
+ )}
169
+ {...props}
170
+ />
171
+ ),
172
+ h5: ({ className, ...props }) => (
173
+ <h5
174
+ className={cn(
175
+ "aui-md-h5 my-4 font-semibold text-lg first:mt-0 last:mb-0",
176
+ className,
177
+ )}
178
+ {...props}
179
+ />
180
+ ),
181
+ h6: ({ className, ...props }) => (
182
+ <h6
183
+ className={cn(
184
+ "aui-md-h6 my-4 font-semibold first:mt-0 last:mb-0",
185
+ className,
186
+ )}
187
+ {...props}
188
+ />
189
+ ),
190
+ p: ({ className, ...props }) => (
191
+ <p
192
+ className={cn(
193
+ "aui-md-p mt-5 mb-5 leading-7 first:mt-0 last:mb-0",
194
+ className,
195
+ )}
196
+ {...props}
197
+ />
198
+ ),
199
+ a: ({ className, ...props }) => (
200
+ <a
201
+ className={cn(
202
+ "aui-md-a font-medium text-primary underline underline-offset-4",
203
+ className,
204
+ )}
205
+ {...props}
206
+ />
207
+ ),
208
+ blockquote: ({ className, ...props }) => (
209
+ <blockquote
210
+ className={cn("aui-md-blockquote border-l-2 pl-6 italic", className)}
211
+ {...props}
212
+ />
213
+ ),
214
+ ul: ({ className, ...props }) => (
215
+ <ul
216
+ className={cn("aui-md-ul my-5 ml-6 list-disc [&>li]:mt-2", className)}
217
+ {...props}
218
+ />
219
+ ),
220
+ ol: ({ className, ...props }) => (
221
+ <ol
222
+ className={cn("aui-md-ol my-5 ml-6 list-decimal [&>li]:mt-2", className)}
223
+ {...props}
224
+ />
225
+ ),
226
+ hr: ({ className, ...props }) => (
227
+ <hr className={cn("aui-md-hr my-5 border-b", className)} {...props} />
228
+ ),
229
+ table: ({ className, ...props }) => (
230
+ <table
231
+ className={cn(
232
+ "aui-md-table my-5 w-full border-separate border-spacing-0 overflow-y-auto",
233
+ className,
234
+ )}
235
+ {...props}
236
+ />
237
+ ),
238
+ th: ({ className, ...props }) => (
239
+ <th
240
+ className={cn(
241
+ "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",
242
+ className,
243
+ )}
244
+ {...props}
245
+ />
246
+ ),
247
+ td: ({ className, ...props }) => (
248
+ <td
249
+ className={cn(
250
+ "aui-md-td border-b border-l px-4 py-2 text-left last:border-r [[align=center]]:text-center [[align=right]]:text-right",
251
+ className,
252
+ )}
253
+ {...props}
254
+ />
255
+ ),
256
+ tr: ({ className, ...props }) => (
257
+ <tr
258
+ className={cn(
259
+ "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",
260
+ className,
261
+ )}
262
+ {...props}
263
+ />
264
+ ),
265
+ sup: ({ className, ...props }) => (
266
+ <sup
267
+ className={cn("aui-md-sup [&>a]:text-xs [&>a]:no-underline", className)}
268
+ {...props}
269
+ />
270
+ ),
271
+ pre: ({ className, ...props }) => (
272
+ <pre
273
+ className={cn(
274
+ "aui-md-pre overflow-x-auto rounded-t-none! rounded-b-lg bg-black p-4 text-white",
275
+ className,
276
+ )}
277
+ {...props}
278
+ />
279
+ ),
280
+ code: function Code({ className, ...props }) {
281
+ const isCodeBlock = useIsMarkdownCodeBlock();
282
+ return (
283
+ <code
284
+ className={cn(
285
+ !isCodeBlock &&
286
+ "aui-md-inline-code rounded border bg-muted font-semibold",
287
+ className,
288
+ )}
289
+ {...props}
290
+ />
291
+ );
292
+ },
293
+ CodeHeader,
294
+ });
295
+
296
+ ```
297
+
298
+ ## src/components/assistant-ui/thread.tsx
299
+
300
+ ```tsx
301
+ import {
302
+ ArrowDownIcon,
303
+ ArrowUpIcon,
304
+ CheckIcon,
305
+ ChevronLeftIcon,
306
+ ChevronRightIcon,
307
+ CopyIcon,
308
+ PencilIcon,
309
+ RefreshCwIcon,
310
+ Square,
311
+ } from "lucide-react";
312
+
313
+ import {
314
+ ActionBarPrimitive,
315
+ AssistantIf,
316
+ BranchPickerPrimitive,
317
+ ComposerPrimitive,
318
+ ErrorPrimitive,
319
+ MessagePrimitive,
320
+ ThreadPrimitive,
321
+ } from "@assistant-ui/react";
322
+
323
+ import type { FC } from "react";
324
+
325
+ import { Button } from "@/components/ui/button";
326
+ import { MarkdownText } from "@/components/assistant-ui/markdown-text";
327
+ import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
328
+ import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
329
+
330
+ import { cn } from "@/lib/utils";
331
+
332
+ export const Thread: FC = () => {
333
+ return (
334
+ <ThreadPrimitive.Root
335
+ className="aui-root aui-thread-root @container flex h-full flex-col bg-background"
336
+ style={{
337
+ ["--thread-max-width" as string]: "44rem",
338
+ }}
339
+ >
340
+ <ThreadPrimitive.Viewport
341
+ turnAnchor="top"
342
+ className="aui-thread-viewport relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll scroll-smooth px-4 pt-4"
343
+ >
344
+ <AssistantIf condition={({ thread }) => thread.isEmpty}>
345
+ <ThreadWelcome />
346
+ </AssistantIf>
347
+
348
+ <ThreadPrimitive.Messages
349
+ components={{
350
+ UserMessage,
351
+ EditComposer,
352
+ AssistantMessage,
353
+ }}
354
+ />
355
+
356
+ <ThreadPrimitive.ViewportFooter className="aui-thread-viewport-footer sticky bottom-0 mx-auto mt-4 flex w-full max-w-(--thread-max-width) flex-col gap-4 overflow-visible rounded-t-3xl bg-background pb-4 md:pb-6">
357
+ <ThreadScrollToBottom />
358
+ <Composer />
359
+ </ThreadPrimitive.ViewportFooter>
360
+ </ThreadPrimitive.Viewport>
361
+ </ThreadPrimitive.Root>
362
+ );
363
+ };
364
+
365
+ const ThreadScrollToBottom: FC = () => {
366
+ return (
367
+ <ThreadPrimitive.ScrollToBottom asChild>
368
+ <TooltipIconButton
369
+ tooltip="Scroll to bottom"
370
+ variant="outline"
371
+ 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"
372
+ >
373
+ <ArrowDownIcon />
374
+ </TooltipIconButton>
375
+ </ThreadPrimitive.ScrollToBottom>
376
+ );
377
+ };
378
+
379
+ const ThreadWelcome: FC = () => {
380
+ return (
381
+ <div className="aui-thread-welcome-root mx-auto my-auto flex w-full max-w-(--thread-max-width) grow flex-col">
382
+ <div className="aui-thread-welcome-center flex w-full grow flex-col items-center justify-center">
383
+ <div className="aui-thread-welcome-message flex size-full flex-col justify-center px-8">
384
+ <div className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-2 animate-in font-semibold text-2xl duration-300 ease-out">
385
+ Hello there!
386
+ </div>
387
+ <div className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-2 animate-in text-2xl text-muted-foreground/65 delay-100 duration-300 ease-out">
388
+ How can I help you today?
389
+ </div>
390
+ </div>
391
+ </div>
392
+ <ThreadSuggestions />
393
+ </div>
394
+ );
395
+ };
396
+
397
+ const ThreadSuggestions: FC = () => {
398
+ return (
399
+ <div className="aui-thread-welcome-suggestions grid w-full @md:grid-cols-2 gap-2 pb-4">
400
+ {[
401
+ {
402
+ title: "What's the weather",
403
+ label: "in San Francisco?",
404
+ action: "What's the weather in San Francisco?",
405
+ },
406
+ {
407
+ title: "Explain React hooks",
408
+ label: "like useState and useEffect",
409
+ action: "Explain React hooks like useState and useEffect",
410
+ },
411
+ {
412
+ title: "Write a SQL query",
413
+ label: "to find top customers",
414
+ action: "Write a SQL query to find top customers",
415
+ },
416
+ {
417
+ title: "Create a meal plan",
418
+ label: "for healthy weight loss",
419
+ action: "Create a meal plan for healthy weight loss",
420
+ },
421
+ ].map((suggestedAction, index) => (
422
+ <div
423
+ key={`suggested-action-${suggestedAction.title}-${index}`}
424
+ className="aui-thread-welcome-suggestion-display fade-in slide-in-from-bottom-4 @md:nth-[n+3]:block nth-[n+3]:hidden animate-in fill-mode-both duration-300 ease-out"
425
+ style={{ animationDelay: `${index * 50}ms` }}
426
+ >
427
+ <ThreadPrimitive.Suggestion
428
+ prompt={suggestedAction.action}
429
+ send
430
+ asChild
431
+ >
432
+ <Button
433
+ variant="ghost"
434
+ className="aui-thread-welcome-suggestion h-auto w-full flex-1 @md:flex-col flex-wrap items-start justify-start gap-1 rounded-3xl border px-5 py-4 text-left text-sm dark:hover:bg-accent/60"
435
+ aria-label={suggestedAction.action}
436
+ >
437
+ <span className="aui-thread-welcome-suggestion-text-1 font-medium">
438
+ {suggestedAction.title}
439
+ </span>
440
+ <span className="aui-thread-welcome-suggestion-text-2 text-muted-foreground">
441
+ {suggestedAction.label}
442
+ </span>
443
+ </Button>
444
+ </ThreadPrimitive.Suggestion>
445
+ </div>
446
+ ))}
447
+ </div>
448
+ );
449
+ };
450
+
451
+ const Composer: FC = () => {
452
+ return (
453
+ <ComposerPrimitive.Root className="aui-composer-root relative flex w-full flex-col">
454
+ <div className="flex w-full flex-col rounded-3xl border border-input bg-background px-1 pt-2 shadow-xs outline-none transition-[color,box-shadow] has-[textarea:focus-visible]:border-ring has-[textarea:focus-visible]:ring-[3px] has-[textarea:focus-visible]:ring-ring/50 dark:bg-background">
455
+ <ComposerPrimitive.Input
456
+ placeholder="Send a message..."
457
+ 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"
458
+ rows={1}
459
+ autoFocus
460
+ aria-label="Message input"
461
+ />
462
+ <ComposerAction />
463
+ </div>
464
+ </ComposerPrimitive.Root>
465
+ );
466
+ };
467
+
468
+ const ComposerAction: FC = () => {
469
+ return (
470
+ <div className="aui-composer-action-wrapper relative mx-1 mt-2 mb-2 flex items-center justify-end">
471
+ <AssistantIf condition={({ thread }) => !thread.isRunning}>
472
+ <ComposerPrimitive.Send asChild>
473
+ <TooltipIconButton
474
+ tooltip="Send message"
475
+ side="bottom"
476
+ type="submit"
477
+ variant="default"
478
+ size="icon"
479
+ className="aui-composer-send size-[34px] rounded-full p-1"
480
+ aria-label="Send message"
481
+ >
482
+ <ArrowUpIcon className="aui-composer-send-icon size-5" />
483
+ </TooltipIconButton>
484
+ </ComposerPrimitive.Send>
485
+ </AssistantIf>
486
+
487
+ <AssistantIf condition={({ thread }) => thread.isRunning}>
488
+ <ComposerPrimitive.Cancel asChild>
489
+ <Button
490
+ type="button"
491
+ variant="default"
492
+ size="icon"
493
+ className="aui-composer-cancel size-[34px] rounded-full border border-muted-foreground/60 hover:bg-primary/75 dark:border-muted-foreground/90"
494
+ aria-label="Stop generating"
495
+ >
496
+ <Square className="aui-composer-cancel-icon size-3.5 fill-white dark:fill-black" />
497
+ </Button>
498
+ </ComposerPrimitive.Cancel>
499
+ </AssistantIf>
500
+ </div>
501
+ );
502
+ };
503
+
504
+ const MessageError: FC = () => {
505
+ return (
506
+ <MessagePrimitive.Error>
507
+ <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">
508
+ <ErrorPrimitive.Message className="aui-message-error-message line-clamp-2" />
509
+ </ErrorPrimitive.Root>
510
+ </MessagePrimitive.Error>
511
+ );
512
+ };
513
+
514
+ const AssistantMessage: FC = () => {
515
+ return (
516
+ <MessagePrimitive.Root
517
+ 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-4 duration-150 ease-out"
518
+ data-role="assistant"
519
+ >
520
+ <div className="aui-assistant-message-content wrap-break-word mx-2 text-foreground leading-7">
521
+ <MessagePrimitive.Parts
522
+ components={{
523
+ Text: MarkdownText,
524
+ tools: { Fallback: ToolFallback },
525
+ }}
526
+ />
527
+ <MessageError />
528
+ </div>
529
+
530
+ <div className="aui-assistant-message-footer mt-2 ml-2 flex">
531
+ <BranchPicker />
532
+ <AssistantActionBar />
533
+ </div>
534
+ </MessagePrimitive.Root>
535
+ );
536
+ };
537
+
538
+ const AssistantActionBar: FC = () => {
539
+ return (
540
+ <ActionBarPrimitive.Root
541
+ hideWhenRunning
542
+ autohide="not-last"
543
+ autohideFloat="single-branch"
544
+ 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"
545
+ >
546
+ <ActionBarPrimitive.Copy asChild>
547
+ <TooltipIconButton tooltip="Copy">
548
+ <AssistantIf condition={({ message }) => message.isCopied}>
549
+ <CheckIcon />
550
+ </AssistantIf>
551
+ <AssistantIf condition={({ message }) => !message.isCopied}>
552
+ <CopyIcon />
553
+ </AssistantIf>
554
+ </TooltipIconButton>
555
+ </ActionBarPrimitive.Copy>
556
+ <ActionBarPrimitive.Reload asChild>
557
+ <TooltipIconButton tooltip="Refresh">
558
+ <RefreshCwIcon />
559
+ </TooltipIconButton>
560
+ </ActionBarPrimitive.Reload>
561
+ </ActionBarPrimitive.Root>
562
+ );
563
+ };
564
+
565
+ const UserMessage: FC = () => {
566
+ return (
567
+ <MessagePrimitive.Root
568
+ 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-4 duration-150 ease-out [&:where(>*)]:col-start-2"
569
+ data-role="user"
570
+ >
571
+ <div className="aui-user-message-content-wrapper relative col-start-2 min-w-0">
572
+ <div className="aui-user-message-content wrap-break-word rounded-3xl bg-muted px-5 py-2.5 text-foreground">
573
+ <MessagePrimitive.Parts />
574
+ </div>
575
+ <div className="aui-user-action-bar-wrapper -translate-x-full -translate-y-1/2 absolute top-1/2 left-0 pr-2">
576
+ <UserActionBar />
577
+ </div>
578
+ </div>
579
+
580
+ <BranchPicker className="aui-user-branch-picker -mr-1 col-span-full col-start-1 row-start-3 justify-end" />
581
+ </MessagePrimitive.Root>
582
+ );
583
+ };
584
+
585
+ const UserActionBar: FC = () => {
586
+ return (
587
+ <ActionBarPrimitive.Root
588
+ hideWhenRunning
589
+ autohide="not-last"
590
+ className="aui-user-action-bar-root flex flex-col items-end"
591
+ >
592
+ <ActionBarPrimitive.Edit asChild>
593
+ <TooltipIconButton tooltip="Edit" className="aui-user-action-edit p-4">
594
+ <PencilIcon />
595
+ </TooltipIconButton>
596
+ </ActionBarPrimitive.Edit>
597
+ </ActionBarPrimitive.Root>
598
+ );
599
+ };
600
+
601
+ const EditComposer: FC = () => {
602
+ return (
603
+ <MessagePrimitive.Root className="aui-edit-composer-wrapper mx-auto flex w-full max-w-(--thread-max-width) flex-col gap-4 px-2">
604
+ <ComposerPrimitive.Root className="aui-edit-composer-root ml-auto flex w-full max-w-7/8 flex-col rounded-xl bg-muted">
605
+ <ComposerPrimitive.Input
606
+ className="aui-edit-composer-input flex min-h-[60px] w-full resize-none bg-transparent p-4 text-foreground outline-none"
607
+ autoFocus
608
+ />
609
+
610
+ <div className="aui-edit-composer-footer mx-3 mb-3 flex items-center justify-center gap-2 self-end">
611
+ <ComposerPrimitive.Cancel asChild>
612
+ <Button variant="ghost" size="sm" aria-label="Cancel edit">
613
+ Cancel
614
+ </Button>
615
+ </ComposerPrimitive.Cancel>
616
+ <ComposerPrimitive.Send asChild>
617
+ <Button size="sm" aria-label="Update message">
618
+ Update
619
+ </Button>
620
+ </ComposerPrimitive.Send>
621
+ </div>
622
+ </ComposerPrimitive.Root>
623
+ </MessagePrimitive.Root>
624
+ );
625
+ };
626
+
627
+ const BranchPicker: FC<BranchPickerPrimitive.Root.Props> = ({
628
+ className,
629
+ ...rest
630
+ }) => {
631
+ return (
632
+ <BranchPickerPrimitive.Root
633
+ hideWhenSingleBranch
634
+ className={cn(
635
+ "aui-branch-picker-root -ml-2 mr-2 inline-flex items-center text-muted-foreground text-xs",
636
+ className,
637
+ )}
638
+ {...rest}
639
+ >
640
+ <BranchPickerPrimitive.Previous asChild>
641
+ <TooltipIconButton tooltip="Previous">
642
+ <ChevronLeftIcon />
643
+ </TooltipIconButton>
644
+ </BranchPickerPrimitive.Previous>
645
+ <span className="aui-branch-picker-state font-medium">
646
+ <BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />
647
+ </span>
648
+ <BranchPickerPrimitive.Next asChild>
649
+ <TooltipIconButton tooltip="Next">
650
+ <ChevronRightIcon />
651
+ </TooltipIconButton>
652
+ </BranchPickerPrimitive.Next>
653
+ </BranchPickerPrimitive.Root>
654
+ );
655
+ };
656
+
657
+ ```
658
+
659
+ ## src/components/assistant-ui/tool-fallback.tsx
660
+
661
+ ```tsx
662
+ import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
663
+ import {
664
+ CheckIcon,
665
+ ChevronDownIcon,
666
+ ChevronUpIcon,
667
+ XCircleIcon,
668
+ } from "lucide-react";
669
+ import { useState } from "react";
670
+ import { Button } from "@/components/ui/button";
671
+ import { cn } from "@/lib/utils";
672
+
673
+ export const ToolFallback: ToolCallMessagePartComponent = ({
674
+ toolName,
675
+ argsText,
676
+ result,
677
+ status,
678
+ }) => {
679
+ const [isCollapsed, setIsCollapsed] = useState(true);
680
+
681
+ const isCancelled =
682
+ status?.type === "incomplete" && status.reason === "cancelled";
683
+ const cancelledReason =
684
+ isCancelled && status.error
685
+ ? typeof status.error === "string"
686
+ ? status.error
687
+ : JSON.stringify(status.error)
688
+ : null;
689
+
690
+ return (
691
+ <div
692
+ className={cn(
693
+ "aui-tool-fallback-root mb-4 flex w-full flex-col gap-3 rounded-lg border py-3",
694
+ isCancelled && "border-muted-foreground/30 bg-muted/30",
695
+ )}
696
+ >
697
+ <div className="aui-tool-fallback-header flex items-center gap-2 px-4">
698
+ {isCancelled ? (
699
+ <XCircleIcon className="aui-tool-fallback-icon size-4 text-muted-foreground" />
700
+ ) : (
701
+ <CheckIcon className="aui-tool-fallback-icon size-4" />
702
+ )}
703
+ <p
704
+ className={cn(
705
+ "aui-tool-fallback-title grow",
706
+ isCancelled && "text-muted-foreground line-through",
707
+ )}
708
+ >
709
+ {isCancelled ? "Cancelled tool: " : "Used tool: "}
710
+ <b>{toolName}</b>
711
+ </p>
712
+ <Button onClick={() => setIsCollapsed(!isCollapsed)}>
713
+ {isCollapsed ? <ChevronUpIcon /> : <ChevronDownIcon />}
714
+ </Button>
715
+ </div>
716
+ {!isCollapsed && (
717
+ <div className="aui-tool-fallback-content flex flex-col gap-2 border-t pt-2">
718
+ {cancelledReason && (
719
+ <div className="aui-tool-fallback-cancelled-root px-4">
720
+ <p className="aui-tool-fallback-cancelled-header font-semibold text-muted-foreground">
721
+ Cancelled reason:
722
+ </p>
723
+ <p className="aui-tool-fallback-cancelled-reason text-muted-foreground">
724
+ {cancelledReason}
725
+ </p>
726
+ </div>
727
+ )}
728
+ <div
729
+ className={cn(
730
+ "aui-tool-fallback-args-root px-4",
731
+ isCancelled && "opacity-60",
732
+ )}
733
+ >
734
+ <pre className="aui-tool-fallback-args-value whitespace-pre-wrap">
735
+ {argsText}
736
+ </pre>
737
+ </div>
738
+ {!isCancelled && result !== undefined && (
739
+ <div className="aui-tool-fallback-result-root border-t border-dashed px-4 pt-2">
740
+ <p className="aui-tool-fallback-result-header font-semibold">
741
+ Result:
742
+ </p>
743
+ <pre className="aui-tool-fallback-result-content whitespace-pre-wrap">
744
+ {typeof result === "string"
745
+ ? result
746
+ : JSON.stringify(result, null, 2)}
747
+ </pre>
748
+ </div>
749
+ )}
750
+ </div>
751
+ )}
752
+ </div>
753
+ );
754
+ };
755
+
756
+ ```
757
+
758
+ ## src/components/assistant-ui/tooltip-icon-button.tsx
759
+
760
+ ```tsx
761
+ import { ComponentPropsWithRef, forwardRef } from "react";
762
+ import { Slottable } from "@radix-ui/react-slot";
763
+
764
+ import {
765
+ Tooltip,
766
+ TooltipContent,
767
+ TooltipTrigger,
768
+ } from "@/components/ui/tooltip";
769
+ import { Button } from "@/components/ui/button";
770
+ import { cn } from "@/lib/utils";
771
+
772
+ export type TooltipIconButtonProps = ComponentPropsWithRef<typeof Button> & {
773
+ tooltip: string;
774
+ side?: "top" | "bottom" | "left" | "right";
775
+ };
776
+
777
+ export const TooltipIconButton = forwardRef<
778
+ HTMLButtonElement,
779
+ TooltipIconButtonProps
780
+ >(({ children, tooltip, side = "bottom", className, ...rest }, ref) => {
781
+ return (
782
+ <Tooltip>
783
+ <TooltipTrigger asChild>
784
+ <Button
785
+ variant="ghost"
786
+ size="icon"
787
+ {...rest}
788
+ className={cn("aui-button-icon size-6 p-1", className)}
789
+ ref={ref}
790
+ >
791
+ <Slottable>{children}</Slottable>
792
+ <span className="aui-sr-only sr-only">{tooltip}</span>
793
+ </Button>
794
+ </TooltipTrigger>
795
+ <TooltipContent side={side}>{tooltip}</TooltipContent>
796
+ </Tooltip>
797
+ );
798
+ });
799
+
800
+ TooltipIconButton.displayName = "TooltipIconButton";
801
+
802
+ ```
803
+
804
+ ## src/components/MyRuntimeProvider.tsx
805
+
806
+ ```tsx
807
+ import { useState, type ReactNode } from "react";
808
+ import {
809
+ useExternalStoreRuntime,
810
+ ThreadMessageLike,
811
+ AppendMessage,
812
+ AssistantRuntimeProvider,
813
+ } from "@assistant-ui/react";
814
+ import { chatStream } from "@/server/chat";
815
+
816
+ type MyMessage = {
817
+ id: string;
818
+ role: "user" | "assistant";
819
+ content: string;
820
+ };
821
+
822
+ const generateId = () => Math.random().toString(36).substring(2, 9);
823
+
824
+ const convertMessage = (message: MyMessage): ThreadMessageLike => {
825
+ return {
826
+ id: message.id,
827
+ role: message.role,
828
+ content: [{ type: "text", text: message.content }],
829
+ };
830
+ };
831
+
832
+ export function MyRuntimeProvider({
833
+ children,
834
+ }: Readonly<{
835
+ children: ReactNode;
836
+ }>) {
837
+ const [isRunning, setIsRunning] = useState(false);
838
+ const [messages, setMessages] = useState<MyMessage[]>([]);
839
+
840
+ const onNew = async (message: AppendMessage) => {
841
+ if (message.content[0]?.type !== "text")
842
+ throw new Error("Only text messages are supported");
843
+
844
+ const input = message.content[0].text;
845
+ const userMessage: MyMessage = {
846
+ id: generateId(),
847
+ role: "user",
848
+ content: input,
849
+ };
850
+
851
+ // Add user message
852
+ const updatedMessages = [...messages, userMessage];
853
+ setMessages(updatedMessages);
854
+
855
+ // Create placeholder for assistant message
856
+ setIsRunning(true);
857
+ const assistantId = generateId();
858
+ const assistantMessage: MyMessage = {
859
+ id: assistantId,
860
+ role: "assistant",
861
+ content: "",
862
+ };
863
+ setMessages((prev) => [...prev, assistantMessage]);
864
+
865
+ try {
866
+ // Stream response using async generator
867
+ const stream = await chatStream({
868
+ data: {
869
+ messages: updatedMessages.map((m) => ({
870
+ role: m.role,
871
+ content: m.content,
872
+ })),
873
+ },
874
+ });
875
+
876
+ // Handle streaming chunks
877
+ for await (const chunk of stream) {
878
+ setMessages((prev) =>
879
+ prev.map((m) =>
880
+ m.id === assistantId ? { ...m, content: m.content + chunk } : m,
881
+ ),
882
+ );
883
+ }
884
+ } catch (error) {
885
+ console.error("Chat error:", error);
886
+ setMessages((prev) =>
887
+ prev.map((m) =>
888
+ m.id === assistantId
889
+ ? { ...m, content: "Sorry, an error occurred. Please try again." }
890
+ : m,
891
+ ),
892
+ );
893
+ } finally {
894
+ setIsRunning(false);
895
+ }
896
+ };
897
+
898
+ const runtime = useExternalStoreRuntime({
899
+ isRunning,
900
+ messages,
901
+ convertMessage,
902
+ onNew,
903
+ });
904
+
905
+ return (
906
+ <AssistantRuntimeProvider runtime={runtime}>
907
+ {children}
908
+ </AssistantRuntimeProvider>
909
+ );
910
+ }
911
+
912
+ ```
913
+
914
+ ## src/components/ui/button.tsx
915
+
916
+ ```tsx
917
+ import * as React from "react";
918
+ import { Slot } from "@radix-ui/react-slot";
919
+ import { cva, type VariantProps } from "class-variance-authority";
920
+
921
+ import { cn } from "@/lib/utils";
922
+
923
+ const buttonVariants = cva(
924
+ "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",
925
+ {
926
+ variants: {
927
+ variant: {
928
+ default:
929
+ "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
930
+ destructive:
931
+ "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",
932
+ outline:
933
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
934
+ secondary:
935
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
936
+ ghost:
937
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
938
+ link: "text-primary underline-offset-4 hover:underline",
939
+ },
940
+ size: {
941
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
942
+ sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
943
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
944
+ icon: "size-9",
945
+ },
946
+ },
947
+ defaultVariants: {
948
+ variant: "default",
949
+ size: "default",
950
+ },
951
+ },
952
+ );
953
+
954
+ function Button({
955
+ className,
956
+ variant,
957
+ size,
958
+ asChild = false,
959
+ ...props
960
+ }: React.ComponentProps<"button"> &
961
+ VariantProps<typeof buttonVariants> & {
962
+ asChild?: boolean;
963
+ }) {
964
+ const Comp = asChild ? Slot : "button";
965
+
966
+ return (
967
+ <Comp
968
+ data-slot="button"
969
+ className={cn(buttonVariants({ variant, size, className }))}
970
+ {...props}
971
+ />
972
+ );
973
+ }
974
+
975
+ export { Button, buttonVariants };
976
+
977
+ ```
978
+
979
+ ## src/components/ui/tooltip.tsx
980
+
981
+ ```tsx
982
+ import * as React from "react";
983
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
984
+
985
+ import { cn } from "@/lib/utils";
986
+
987
+ function TooltipProvider({
988
+ delayDuration = 0,
989
+ ...props
990
+ }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
991
+ return (
992
+ <TooltipPrimitive.Provider
993
+ data-slot="tooltip-provider"
994
+ delayDuration={delayDuration}
995
+ {...props}
996
+ />
997
+ );
998
+ }
999
+
1000
+ function Tooltip({
1001
+ ...props
1002
+ }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
1003
+ return (
1004
+ <TooltipProvider>
1005
+ <TooltipPrimitive.Root data-slot="tooltip" {...props} />
1006
+ </TooltipProvider>
1007
+ );
1008
+ }
1009
+
1010
+ function TooltipTrigger({
1011
+ ...props
1012
+ }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
1013
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
1014
+ }
1015
+
1016
+ function TooltipContent({
1017
+ className,
1018
+ sideOffset = 0,
1019
+ children,
1020
+ ...props
1021
+ }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
1022
+ return (
1023
+ <TooltipPrimitive.Portal>
1024
+ <TooltipPrimitive.Content
1025
+ data-slot="tooltip-content"
1026
+ sideOffset={sideOffset}
1027
+ className={cn(
1028
+ "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",
1029
+ className,
1030
+ )}
1031
+ {...props}
1032
+ >
1033
+ {children}
1034
+ <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-primary fill-primary" />
1035
+ </TooltipPrimitive.Content>
1036
+ </TooltipPrimitive.Portal>
1037
+ );
1038
+ }
1039
+
1040
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
1041
+
1042
+ ```
1043
+
1044
+ ## src/lib/utils.ts
1045
+
1046
+ ```typescript
1047
+ import { clsx, type ClassValue } from "clsx";
1048
+ import { twMerge } from "tailwind-merge";
1049
+
1050
+ export function cn(...inputs: ClassValue[]) {
1051
+ return twMerge(clsx(inputs));
1052
+ }
1053
+
1054
+ ```
1055
+
1056
+ ## src/router.tsx
1057
+
1058
+ ```tsx
1059
+ import { createRouter } from "@tanstack/react-router";
1060
+
1061
+ // Import the generated route tree
1062
+ import { routeTree } from "./routeTree.gen";
1063
+
1064
+ // Create a new router instance
1065
+ export const getRouter = () => {
1066
+ const router = createRouter({
1067
+ routeTree,
1068
+ scrollRestoration: true,
1069
+ defaultPreloadStaleTime: 0,
1070
+ });
1071
+
1072
+ return router;
1073
+ };
1074
+
1075
+ ```
1076
+
1077
+ ## src/routes/__root.tsx
1078
+
1079
+ ```tsx
1080
+ import { HeadContent, Scripts, createRootRoute } from "@tanstack/react-router";
1081
+
1082
+ import appCss from "../styles.css?url";
1083
+
1084
+ export const Route = createRootRoute({
1085
+ head: () => ({
1086
+ meta: [
1087
+ {
1088
+ charSet: "utf-8",
1089
+ },
1090
+ {
1091
+ name: "viewport",
1092
+ content: "width=device-width, initial-scale=1",
1093
+ },
1094
+ {
1095
+ title: "assistant-ui + TanStack Start",
1096
+ },
1097
+ ],
1098
+ links: [
1099
+ {
1100
+ rel: "stylesheet",
1101
+ href: appCss,
1102
+ },
1103
+ ],
1104
+ }),
1105
+
1106
+ shellComponent: RootDocument,
1107
+ });
1108
+
1109
+ function RootDocument({ children }: { children: React.ReactNode }) {
1110
+ return (
1111
+ <html lang="en">
1112
+ {/* biome-ignore lint/style/noHeadElement: TanStack Start uses standard HTML head */}
1113
+ <head>
1114
+ <HeadContent />
1115
+ </head>
1116
+ <body className="bg-background text-foreground">
1117
+ {children}
1118
+ <Scripts />
1119
+ </body>
1120
+ </html>
1121
+ );
1122
+ }
1123
+
1124
+ ```
1125
+
1126
+ ## src/routes/index.tsx
1127
+
1128
+ ```tsx
1129
+ import { createFileRoute } from "@tanstack/react-router";
1130
+ import { Thread } from "@/components/assistant-ui/thread";
1131
+ import { MyRuntimeProvider } from "@/components/MyRuntimeProvider";
1132
+
1133
+ export const Route = createFileRoute("/")({ component: App });
1134
+
1135
+ function App() {
1136
+ return (
1137
+ <MyRuntimeProvider>
1138
+ <main className="h-dvh">
1139
+ <Thread />
1140
+ </main>
1141
+ </MyRuntimeProvider>
1142
+ );
1143
+ }
1144
+
1145
+ ```
1146
+
1147
+ ## src/routeTree.gen.ts
1148
+
1149
+ ```typescript
1150
+ /* eslint-disable */
1151
+
1152
+ // @ts-nocheck
1153
+
1154
+ // noinspection JSUnusedGlobalSymbols
1155
+
1156
+ // This file was automatically generated by TanStack Router.
1157
+ // You should NOT make any changes in this file as it will be overwritten.
1158
+ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
1159
+
1160
+ import { Route as rootRouteImport } from './routes/__root'
1161
+ import { Route as IndexRouteImport } from './routes/index'
1162
+
1163
+ const IndexRoute = IndexRouteImport.update({
1164
+ id: '/',
1165
+ path: '/',
1166
+ getParentRoute: () => rootRouteImport,
1167
+ } as any)
1168
+
1169
+ export interface FileRoutesByFullPath {
1170
+ '/': typeof IndexRoute
1171
+ }
1172
+ export interface FileRoutesByTo {
1173
+ '/': typeof IndexRoute
1174
+ }
1175
+ export interface FileRoutesById {
1176
+ __root__: typeof rootRouteImport
1177
+ '/': typeof IndexRoute
1178
+ }
1179
+ export interface FileRouteTypes {
1180
+ fileRoutesByFullPath: FileRoutesByFullPath
1181
+ fullPaths: '/'
1182
+ fileRoutesByTo: FileRoutesByTo
1183
+ to: '/'
1184
+ id: '__root__' | '/'
1185
+ fileRoutesById: FileRoutesById
1186
+ }
1187
+ export interface RootRouteChildren {
1188
+ IndexRoute: typeof IndexRoute
1189
+ }
1190
+
1191
+ declare module '@tanstack/react-router' {
1192
+ interface FileRoutesByPath {
1193
+ '/': {
1194
+ id: '/'
1195
+ path: '/'
1196
+ fullPath: '/'
1197
+ preLoaderRoute: typeof IndexRouteImport
1198
+ parentRoute: typeof rootRouteImport
1199
+ }
1200
+ }
1201
+ }
1202
+
1203
+ const rootRouteChildren: RootRouteChildren = {
1204
+ IndexRoute: IndexRoute,
1205
+ }
1206
+ export const routeTree = rootRouteImport
1207
+ ._addFileChildren(rootRouteChildren)
1208
+ ._addFileTypes<FileRouteTypes>()
1209
+
1210
+ import type { getRouter } from './router.tsx'
1211
+ import type { createStart } from '@tanstack/react-start'
1212
+ declare module '@tanstack/react-start' {
1213
+ interface Register {
1214
+ ssr: true
1215
+ router: Awaited<ReturnType<typeof getRouter>>
1216
+ }
1217
+ }
1218
+
1219
+ ```
1220
+
1221
+ ## src/server/chat.ts
1222
+
1223
+ ```typescript
1224
+ import { createServerFn } from "@tanstack/react-start";
1225
+ import OpenAI from "openai";
1226
+
1227
+ type ChatMessage = {
1228
+ role: "user" | "assistant" | "system";
1229
+ content: string;
1230
+ };
1231
+
1232
+ type ChatInput = {
1233
+ messages: ChatMessage[];
1234
+ };
1235
+
1236
+ export const chatStream = createServerFn({ method: "POST" })
1237
+ .inputValidator((data: ChatInput) => data)
1238
+ .handler(async function* ({ data }) {
1239
+ const openai = new OpenAI({
1240
+ apiKey: process.env.OPENAI_API_KEY,
1241
+ });
1242
+
1243
+ const stream = await openai.chat.completions.create({
1244
+ model: "gpt-4o-mini",
1245
+ messages: data.messages,
1246
+ stream: true,
1247
+ });
1248
+
1249
+ for await (const chunk of stream) {
1250
+ const content = chunk.choices[0]?.delta?.content;
1251
+ if (content) {
1252
+ yield content;
1253
+ }
1254
+ }
1255
+ });
1256
+
1257
+ ```
1258
+
1259
+ ## src/styles.css
1260
+
1261
+ ```css
1262
+ @import "tailwindcss";
1263
+
1264
+ @custom-variant dark (&:is(.dark *));
1265
+
1266
+ @theme inline {
1267
+ --radius-sm: calc(var(--radius) - 4px);
1268
+ --radius-md: calc(var(--radius) - 2px);
1269
+ --radius-lg: var(--radius);
1270
+ --radius-xl: calc(var(--radius) + 4px);
1271
+ --color-background: var(--background);
1272
+ --color-foreground: var(--foreground);
1273
+ --color-card: var(--card);
1274
+ --color-card-foreground: var(--card-foreground);
1275
+ --color-popover: var(--popover);
1276
+ --color-popover-foreground: var(--popover-foreground);
1277
+ --color-primary: var(--primary);
1278
+ --color-primary-foreground: var(--primary-foreground);
1279
+ --color-secondary: var(--secondary);
1280
+ --color-secondary-foreground: var(--secondary-foreground);
1281
+ --color-muted: var(--muted);
1282
+ --color-muted-foreground: var(--muted-foreground);
1283
+ --color-accent: var(--accent);
1284
+ --color-accent-foreground: var(--accent-foreground);
1285
+ --color-destructive: var(--destructive);
1286
+ --color-border: var(--border);
1287
+ --color-input: var(--input);
1288
+ --color-ring: var(--ring);
1289
+ }
1290
+
1291
+ :root {
1292
+ --radius: 0.625rem;
1293
+ --background: oklch(1 0 0);
1294
+ --foreground: oklch(0.141 0.005 285.823);
1295
+ --card: oklch(1 0 0);
1296
+ --card-foreground: oklch(0.141 0.005 285.823);
1297
+ --popover: oklch(1 0 0);
1298
+ --popover-foreground: oklch(0.141 0.005 285.823);
1299
+ --primary: oklch(0.21 0.006 285.885);
1300
+ --primary-foreground: oklch(0.985 0 0);
1301
+ --secondary: oklch(0.967 0.001 286.375);
1302
+ --secondary-foreground: oklch(0.21 0.006 285.885);
1303
+ --muted: oklch(0.967 0.001 286.375);
1304
+ --muted-foreground: oklch(0.552 0.016 285.938);
1305
+ --accent: oklch(0.967 0.001 286.375);
1306
+ --accent-foreground: oklch(0.21 0.006 285.885);
1307
+ --destructive: oklch(0.577 0.245 27.325);
1308
+ --border: oklch(0.92 0.004 286.32);
1309
+ --input: oklch(0.92 0.004 286.32);
1310
+ --ring: oklch(0.705 0.015 286.067);
1311
+ }
1312
+
1313
+ .dark {
1314
+ --background: oklch(0.141 0.005 285.823);
1315
+ --foreground: oklch(0.985 0 0);
1316
+ --card: oklch(0.21 0.006 285.885);
1317
+ --card-foreground: oklch(0.985 0 0);
1318
+ --popover: oklch(0.21 0.006 285.885);
1319
+ --popover-foreground: oklch(0.985 0 0);
1320
+ --primary: oklch(0.92 0.004 286.32);
1321
+ --primary-foreground: oklch(0.21 0.006 285.885);
1322
+ --secondary: oklch(0.274 0.006 286.033);
1323
+ --secondary-foreground: oklch(0.985 0 0);
1324
+ --muted: oklch(0.274 0.006 286.033);
1325
+ --muted-foreground: oklch(0.705 0.015 286.067);
1326
+ --accent: oklch(0.274 0.006 286.033);
1327
+ --accent-foreground: oklch(0.985 0 0);
1328
+ --destructive: oklch(0.704 0.191 22.216);
1329
+ --border: oklch(1 0 0 / 10%);
1330
+ --input: oklch(1 0 0 / 15%);
1331
+ --ring: oklch(0.552 0.016 285.938);
1332
+ }
1333
+
1334
+ @layer base {
1335
+ * {
1336
+ @apply border-border outline-ring/50;
1337
+ }
1338
+ body {
1339
+ @apply bg-background text-foreground m-0;
1340
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
1341
+ "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
1342
+ sans-serif;
1343
+ -webkit-font-smoothing: antialiased;
1344
+ -moz-osx-font-smoothing: grayscale;
1345
+ }
1346
+ code {
1347
+ font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
1348
+ monospace;
1349
+ }
1350
+ }
1351
+
1352
+ /* Animation utilities for assistant-ui */
1353
+ @keyframes fade-in {
1354
+ from {
1355
+ opacity: 0;
1356
+ }
1357
+ to {
1358
+ opacity: 1;
1359
+ }
1360
+ }
1361
+
1362
+ @keyframes slide-in-from-bottom-1 {
1363
+ from {
1364
+ transform: translateY(0.25rem);
1365
+ }
1366
+ to {
1367
+ transform: translateY(0);
1368
+ }
1369
+ }
1370
+
1371
+ @keyframes slide-in-from-bottom-2 {
1372
+ from {
1373
+ transform: translateY(0.5rem);
1374
+ }
1375
+ to {
1376
+ transform: translateY(0);
1377
+ }
1378
+ }
1379
+
1380
+ @keyframes slide-in-from-bottom-4 {
1381
+ from {
1382
+ transform: translateY(1rem);
1383
+ }
1384
+ to {
1385
+ transform: translateY(0);
1386
+ }
1387
+ }
1388
+
1389
+ @keyframes zoom-in-95 {
1390
+ from {
1391
+ transform: scale(0.95);
1392
+ }
1393
+ to {
1394
+ transform: scale(1);
1395
+ }
1396
+ }
1397
+
1398
+ @keyframes zoom-out-95 {
1399
+ to {
1400
+ transform: scale(0.95);
1401
+ }
1402
+ }
1403
+
1404
+ @keyframes fade-out {
1405
+ to {
1406
+ opacity: 0;
1407
+ }
1408
+ }
1409
+
1410
+ @keyframes slide-in-from-top-2 {
1411
+ from {
1412
+ transform: translateY(-0.5rem);
1413
+ }
1414
+ to {
1415
+ transform: translateY(0);
1416
+ }
1417
+ }
1418
+
1419
+ @keyframes slide-in-from-left-2 {
1420
+ from {
1421
+ transform: translateX(-0.5rem);
1422
+ }
1423
+ to {
1424
+ transform: translateX(0);
1425
+ }
1426
+ }
1427
+
1428
+ @keyframes slide-in-from-right-2 {
1429
+ from {
1430
+ transform: translateX(0.5rem);
1431
+ }
1432
+ to {
1433
+ transform: translateX(0);
1434
+ }
1435
+ }
1436
+
1437
+ .animate-in {
1438
+ animation-duration: 150ms;
1439
+ animation-timing-function: ease-out;
1440
+ animation-fill-mode: both;
1441
+ }
1442
+
1443
+ .fade-in {
1444
+ animation-name: fade-in;
1445
+ }
1446
+
1447
+ .fade-in-0 {
1448
+ animation-name: fade-in;
1449
+ }
1450
+
1451
+ .zoom-in-95 {
1452
+ animation-name: zoom-in-95;
1453
+ }
1454
+
1455
+ .slide-in-from-bottom-1 {
1456
+ animation-name: fade-in, slide-in-from-bottom-1;
1457
+ }
1458
+
1459
+ .slide-in-from-bottom-2 {
1460
+ animation-name: fade-in, slide-in-from-bottom-2;
1461
+ }
1462
+
1463
+ .slide-in-from-bottom-4 {
1464
+ animation-name: fade-in, slide-in-from-bottom-4;
1465
+ }
1466
+
1467
+ .slide-in-from-top-2 {
1468
+ animation-name: fade-in, slide-in-from-top-2;
1469
+ }
1470
+
1471
+ .slide-in-from-left-2 {
1472
+ animation-name: fade-in, slide-in-from-left-2;
1473
+ }
1474
+
1475
+ .slide-in-from-right-2 {
1476
+ animation-name: fade-in, slide-in-from-right-2;
1477
+ }
1478
+
1479
+ [data-state="closed"].animate-out {
1480
+ animation-duration: 150ms;
1481
+ animation-timing-function: ease-in;
1482
+ animation-fill-mode: both;
1483
+ }
1484
+
1485
+ [data-state="closed"].fade-out-0 {
1486
+ animation-name: fade-out;
1487
+ }
1488
+
1489
+ [data-state="closed"].zoom-out-95 {
1490
+ animation-name: zoom-out-95;
1491
+ }
1492
+
1493
+ .delay-100 {
1494
+ animation-delay: 100ms;
1495
+ }
1496
+
1497
+ .fill-mode-both {
1498
+ animation-fill-mode: both;
1499
+ }
1500
+
1501
+ .duration-150 {
1502
+ animation-duration: 150ms;
1503
+ }
1504
+
1505
+ .duration-300 {
1506
+ animation-duration: 300ms;
1507
+ }
1508
+
1509
+ .ease-out {
1510
+ animation-timing-function: ease-out;
1511
+ }
1512
+
1513
+ ```
1514
+
1515
+ ## tsconfig.json
1516
+
1517
+ ```json
1518
+ {
1519
+ "include": ["**/*.ts", "**/*.tsx"],
1520
+ "compilerOptions": {
1521
+ "target": "ES2022",
1522
+ "jsx": "react-jsx",
1523
+ "module": "ESNext",
1524
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
1525
+ "types": ["vite/client"],
1526
+
1527
+ /* Bundler mode */
1528
+ "moduleResolution": "bundler",
1529
+ "allowImportingTsExtensions": true,
1530
+ "verbatimModuleSyntax": false,
1531
+ "noEmit": true,
1532
+
1533
+ /* Linting */
1534
+ "skipLibCheck": true,
1535
+ "strict": true,
1536
+ "noUnusedLocals": true,
1537
+ "noUnusedParameters": true,
1538
+ "noFallthroughCasesInSwitch": true,
1539
+ "noUncheckedSideEffectImports": true,
1540
+ "baseUrl": ".",
1541
+ "paths": {
1542
+ "@/*": ["./src/*"]
1543
+ }
1544
+ }
1545
+ }
1546
+
1547
+ ```
1548
+
1549
+ ## vite.config.ts
1550
+
1551
+ ```typescript
1552
+ import { defineConfig } from "vite";
1553
+ import { tanstackStart } from "@tanstack/react-start/plugin/vite";
1554
+ import viteReact from "@vitejs/plugin-react";
1555
+ import viteTsConfigPaths from "vite-tsconfig-paths";
1556
+ import tailwindcss from "@tailwindcss/vite";
1557
+ import { nitro } from "nitro/vite";
1558
+
1559
+ const config = defineConfig({
1560
+ plugins: [
1561
+ nitro(),
1562
+ viteTsConfigPaths({
1563
+ projects: ["./tsconfig.json"],
1564
+ }),
1565
+ tailwindcss(),
1566
+ tanstackStart(),
1567
+ viteReact(),
1568
+ ],
1569
+ });
1570
+
1571
+ export default config;
1572
+
1573
+ ```
1574
+