@assistant-ui/mcp-docs-server 0.1.15 → 0.1.17
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.
- package/.docs/organized/code-examples/store-example.md +249 -147
- package/.docs/organized/code-examples/with-ag-ui.md +250 -186
- package/.docs/organized/code-examples/with-ai-sdk-v5.md +250 -199
- package/.docs/organized/code-examples/with-assistant-transport.md +195 -243
- package/.docs/organized/code-examples/with-cloud.md +277 -226
- package/.docs/organized/code-examples/with-custom-thread-list.md +1855 -0
- package/.docs/organized/code-examples/with-external-store.md +246 -180
- package/.docs/organized/code-examples/with-ffmpeg.md +255 -189
- package/.docs/organized/code-examples/with-langgraph.md +293 -242
- package/.docs/organized/code-examples/with-parent-id-grouping.md +243 -263
- package/.docs/organized/code-examples/with-react-hook-form.md +262 -196
- package/.docs/organized/code-examples/with-tanstack.md +1578 -0
- package/.docs/raw/blog/2024-07-29-hello/index.mdx +2 -2
- package/.docs/raw/docs/api-reference/overview.mdx +6 -6
- package/.docs/raw/docs/api-reference/primitives/ActionBar.mdx +85 -4
- package/.docs/raw/docs/api-reference/primitives/AssistantIf.mdx +200 -0
- package/.docs/raw/docs/api-reference/primitives/Composer.mdx +0 -20
- package/.docs/raw/docs/api-reference/primitives/Message.mdx +0 -45
- package/.docs/raw/docs/api-reference/primitives/Thread.mdx +0 -50
- package/.docs/raw/docs/cloud/persistence/ai-sdk.mdx +2 -3
- package/.docs/raw/docs/cloud/persistence/langgraph.mdx +2 -3
- package/.docs/raw/docs/devtools.mdx +2 -3
- package/.docs/raw/docs/getting-started.mdx +36 -1102
- package/.docs/raw/docs/guides/Attachments.mdx +3 -25
- package/.docs/raw/docs/guides/Branching.mdx +1 -1
- package/.docs/raw/docs/guides/Speech.mdx +1 -1
- package/.docs/raw/docs/guides/ToolUI.mdx +1 -1
- package/.docs/raw/docs/legacy/styled/AssistantModal.mdx +2 -3
- package/.docs/raw/docs/legacy/styled/Decomposition.mdx +6 -5
- package/.docs/raw/docs/legacy/styled/Markdown.mdx +2 -3
- package/.docs/raw/docs/legacy/styled/Thread.mdx +2 -3
- package/.docs/raw/docs/react-compatibility.mdx +2 -5
- package/.docs/raw/docs/runtimes/ai-sdk/use-chat.mdx +3 -4
- package/.docs/raw/docs/runtimes/ai-sdk/v4-legacy.mdx +3 -6
- package/.docs/raw/docs/runtimes/custom/external-store.mdx +2 -3
- package/.docs/raw/docs/runtimes/custom/local.mdx +11 -41
- package/.docs/raw/docs/runtimes/data-stream.mdx +15 -11
- package/.docs/raw/docs/runtimes/langgraph/index.mdx +3 -3
- package/.docs/raw/docs/runtimes/langgraph/tutorial/part-2.mdx +1 -1
- package/.docs/raw/docs/runtimes/langgraph/tutorial/part-3.mdx +2 -3
- package/.docs/raw/docs/runtimes/langserve.mdx +2 -3
- package/.docs/raw/docs/runtimes/mastra/full-stack-integration.mdx +2 -3
- package/.docs/raw/docs/runtimes/mastra/separate-server-integration.mdx +2 -3
- package/.docs/raw/docs/ui/AssistantModal.mdx +3 -25
- package/.docs/raw/docs/ui/AssistantSidebar.mdx +2 -24
- package/.docs/raw/docs/ui/Attachment.mdx +3 -25
- package/.docs/raw/docs/ui/Markdown.mdx +2 -24
- package/.docs/raw/docs/ui/Mermaid.mdx +2 -24
- package/.docs/raw/docs/ui/Reasoning.mdx +2 -24
- package/.docs/raw/docs/ui/Scrollbar.mdx +4 -6
- package/.docs/raw/docs/ui/SyntaxHighlighting.mdx +3 -47
- package/.docs/raw/docs/ui/Thread.mdx +38 -53
- package/.docs/raw/docs/ui/ThreadList.mdx +4 -47
- package/.docs/raw/docs/ui/ToolFallback.mdx +2 -24
- package/package.json +6 -7
|
@@ -0,0 +1,1578 @@
|
|
|
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.18",
|
|
39
|
+
"@tanstack/react-router": "^1.141.1",
|
|
40
|
+
"@tanstack/react-start": "^1.141.1",
|
|
41
|
+
"@tanstack/router-plugin": "^1.141.1",
|
|
42
|
+
"class-variance-authority": "^0.7.1",
|
|
43
|
+
"clsx": "^2.1.1",
|
|
44
|
+
"lucide-react": "^0.560.0",
|
|
45
|
+
"nitro": "latest",
|
|
46
|
+
"openai": "^6.10.0",
|
|
47
|
+
"react": "^19.2.3",
|
|
48
|
+
"react-dom": "^19.2.3",
|
|
49
|
+
"remark-gfm": "^4.0.1",
|
|
50
|
+
"tailwind-merge": "^3.4.0",
|
|
51
|
+
"tailwindcss": "^4.1.18",
|
|
52
|
+
"vite-tsconfig-paths": "^5.1.4"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/node": "^25.0.0",
|
|
56
|
+
"@types/react": "^19.2.7",
|
|
57
|
+
"@types/react-dom": "^19.2.3",
|
|
58
|
+
"@vitejs/plugin-react": "^5.1.2",
|
|
59
|
+
"typescript": "^5.9.3",
|
|
60
|
+
"vite": "^7.2.7"
|
|
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
|
+
output: {
|
|
1563
|
+
dir: "dist",
|
|
1564
|
+
},
|
|
1565
|
+
}),
|
|
1566
|
+
viteTsConfigPaths({
|
|
1567
|
+
projects: ["./tsconfig.json"],
|
|
1568
|
+
}),
|
|
1569
|
+
tailwindcss(),
|
|
1570
|
+
tanstackStart(),
|
|
1571
|
+
viteReact(),
|
|
1572
|
+
],
|
|
1573
|
+
});
|
|
1574
|
+
|
|
1575
|
+
export default config;
|
|
1576
|
+
|
|
1577
|
+
```
|
|
1578
|
+
|