@assistant-ui/mcp-docs-server 0.1.7 → 0.1.9
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/with-ai-sdk-v5.md +24 -15
- package/.docs/organized/code-examples/with-assistant-transport.md +1599 -0
- package/.docs/organized/code-examples/with-cloud.md +12 -10
- package/.docs/organized/code-examples/with-external-store.md +10 -8
- package/.docs/organized/code-examples/with-ffmpeg.md +17 -14
- package/.docs/organized/code-examples/with-langgraph.md +83 -47
- package/.docs/organized/code-examples/with-parent-id-grouping.md +10 -8
- package/.docs/organized/code-examples/with-react-hook-form.md +17 -14
- package/.docs/raw/docs/api-reference/integrations/react-data-stream.mdx +194 -0
- package/.docs/raw/docs/api-reference/overview.mdx +6 -0
- package/.docs/raw/docs/api-reference/primitives/Composer.mdx +31 -0
- package/.docs/raw/docs/api-reference/primitives/Message.mdx +108 -3
- package/.docs/raw/docs/api-reference/primitives/Thread.mdx +59 -0
- package/.docs/raw/docs/api-reference/primitives/ThreadList.mdx +128 -0
- package/.docs/raw/docs/api-reference/primitives/ThreadListItem.mdx +160 -0
- package/.docs/raw/docs/api-reference/runtimes/AssistantRuntime.mdx +0 -11
- package/.docs/raw/docs/api-reference/runtimes/ComposerRuntime.mdx +3 -3
- package/.docs/raw/docs/copilots/assistant-frame.mdx +399 -0
- package/.docs/raw/docs/devtools.mdx +51 -0
- package/.docs/raw/docs/getting-started.mdx +20 -19
- package/.docs/raw/docs/guides/Attachments.mdx +6 -13
- package/.docs/raw/docs/guides/Tools.mdx +56 -13
- package/.docs/raw/docs/guides/context-api.mdx +574 -0
- package/.docs/raw/docs/migrations/v0-12.mdx +125 -0
- package/.docs/raw/docs/runtimes/ai-sdk/use-chat.mdx +2 -2
- package/.docs/raw/docs/runtimes/custom/local.mdx +17 -4
- package/.docs/raw/docs/runtimes/data-stream.mdx +287 -0
- package/.docs/raw/docs/runtimes/mastra/full-stack-integration.mdx +6 -5
- package/.docs/raw/docs/runtimes/mastra/overview.mdx +3 -3
- package/.docs/raw/docs/runtimes/mastra/separate-server-integration.mdx +13 -13
- package/.docs/raw/docs/runtimes/pick-a-runtime.mdx +5 -0
- package/.docs/raw/docs/ui/ThreadList.mdx +54 -16
- package/dist/{chunk-L4K23SWI.js → chunk-NVNFQ5ZO.js} +4 -1
- package/dist/index.js +1 -1
- package/dist/prepare-docs/prepare.js +1 -1
- package/dist/stdio.js +1 -1
- package/package.json +7 -7
- package/.docs/raw/docs/concepts/architecture.mdx +0 -19
- package/.docs/raw/docs/concepts/runtime-layer.mdx +0 -163
- package/.docs/raw/docs/concepts/why.mdx +0 -9
|
@@ -0,0 +1,1599 @@
|
|
|
1
|
+
# Example: with-assistant-transport
|
|
2
|
+
|
|
3
|
+
## app/globals.css
|
|
4
|
+
|
|
5
|
+
```css
|
|
6
|
+
@import "tailwindcss";
|
|
7
|
+
|
|
8
|
+
@plugin "tailwindcss-animate";
|
|
9
|
+
@import "tw-animate-css";
|
|
10
|
+
|
|
11
|
+
@custom-variant dark (&:is(.dark *));
|
|
12
|
+
|
|
13
|
+
@theme inline {
|
|
14
|
+
--color-background: var(--background);
|
|
15
|
+
--color-foreground: var(--foreground);
|
|
16
|
+
--font-sans: var(--font-geist-sans);
|
|
17
|
+
--font-mono: var(--font-geist-mono);
|
|
18
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
19
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
20
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
21
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
22
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
23
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
24
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
25
|
+
--color-sidebar: var(--sidebar);
|
|
26
|
+
--color-chart-5: var(--chart-5);
|
|
27
|
+
--color-chart-4: var(--chart-4);
|
|
28
|
+
--color-chart-3: var(--chart-3);
|
|
29
|
+
--color-chart-2: var(--chart-2);
|
|
30
|
+
--color-chart-1: var(--chart-1);
|
|
31
|
+
--color-ring: var(--ring);
|
|
32
|
+
--color-input: var(--input);
|
|
33
|
+
--color-border: var(--border);
|
|
34
|
+
--color-destructive: var(--destructive);
|
|
35
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
36
|
+
--color-accent: var(--accent);
|
|
37
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
38
|
+
--color-muted: var(--muted);
|
|
39
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
40
|
+
--color-secondary: var(--secondary);
|
|
41
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
42
|
+
--color-primary: var(--primary);
|
|
43
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
44
|
+
--color-popover: var(--popover);
|
|
45
|
+
--color-card-foreground: var(--card-foreground);
|
|
46
|
+
--color-card: var(--card);
|
|
47
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
48
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
49
|
+
--radius-lg: var(--radius);
|
|
50
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
:root {
|
|
54
|
+
--radius: 0.625rem;
|
|
55
|
+
--card: oklch(1 0 0);
|
|
56
|
+
--card-foreground: oklch(0.141 0.005 285.823);
|
|
57
|
+
--popover: oklch(1 0 0);
|
|
58
|
+
--popover-foreground: oklch(0.141 0.005 285.823);
|
|
59
|
+
--primary: oklch(0.21 0.006 285.885);
|
|
60
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
61
|
+
--secondary: oklch(0.967 0.001 286.375);
|
|
62
|
+
--secondary-foreground: oklch(0.21 0.006 285.885);
|
|
63
|
+
--muted: oklch(0.967 0.001 286.375);
|
|
64
|
+
--muted-foreground: oklch(0.552 0.016 285.938);
|
|
65
|
+
--accent: oklch(0.967 0.001 286.375);
|
|
66
|
+
--accent-foreground: oklch(0.21 0.006 285.885);
|
|
67
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
68
|
+
--border: oklch(0.92 0.004 286.32);
|
|
69
|
+
--input: oklch(0.92 0.004 286.32);
|
|
70
|
+
--ring: oklch(0.705 0.015 286.067);
|
|
71
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
72
|
+
--chart-2: oklch(0.6 0.118 184.704);
|
|
73
|
+
--chart-3: oklch(0.398 0.07 227.392);
|
|
74
|
+
--chart-4: oklch(0.828 0.189 84.429);
|
|
75
|
+
--chart-5: oklch(0.769 0.188 70.08);
|
|
76
|
+
--sidebar: oklch(0.985 0 0);
|
|
77
|
+
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
|
78
|
+
--sidebar-primary: oklch(0.21 0.006 285.885);
|
|
79
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
80
|
+
--sidebar-accent: oklch(0.967 0.001 286.375);
|
|
81
|
+
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
|
82
|
+
--sidebar-border: oklch(0.92 0.004 286.32);
|
|
83
|
+
--sidebar-ring: oklch(0.705 0.015 286.067);
|
|
84
|
+
--background: oklch(1 0 0);
|
|
85
|
+
--foreground: oklch(0.141 0.005 285.823);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.dark {
|
|
89
|
+
--background: oklch(0.141 0.005 285.823);
|
|
90
|
+
--foreground: oklch(0.985 0 0);
|
|
91
|
+
--card: oklch(0.21 0.006 285.885);
|
|
92
|
+
--card-foreground: oklch(0.985 0 0);
|
|
93
|
+
--popover: oklch(0.21 0.006 285.885);
|
|
94
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
95
|
+
--primary: oklch(0.92 0.004 286.32);
|
|
96
|
+
--primary-foreground: oklch(0.21 0.006 285.885);
|
|
97
|
+
--secondary: oklch(0.274 0.006 286.033);
|
|
98
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
99
|
+
--muted: oklch(0.274 0.006 286.033);
|
|
100
|
+
--muted-foreground: oklch(0.705 0.015 286.067);
|
|
101
|
+
--accent: oklch(0.274 0.006 286.033);
|
|
102
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
103
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
104
|
+
--border: oklch(1 0 0 / 10%);
|
|
105
|
+
--input: oklch(1 0 0 / 15%);
|
|
106
|
+
--ring: oklch(0.552 0.016 285.938);
|
|
107
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
108
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
109
|
+
--chart-3: oklch(0.769 0.188 70.08);
|
|
110
|
+
--chart-4: oklch(0.627 0.265 303.9);
|
|
111
|
+
--chart-5: oklch(0.645 0.246 16.439);
|
|
112
|
+
--sidebar: oklch(0.21 0.006 285.885);
|
|
113
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
114
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
115
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
116
|
+
--sidebar-accent: oklch(0.274 0.006 286.033);
|
|
117
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
118
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
119
|
+
--sidebar-ring: oklch(0.552 0.016 285.938);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@layer base {
|
|
123
|
+
* {
|
|
124
|
+
@apply border-border outline-ring/50;
|
|
125
|
+
}
|
|
126
|
+
body {
|
|
127
|
+
@apply bg-background text-foreground;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## app/layout.tsx
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
import type { Metadata } from "next";
|
|
137
|
+
import "./globals.css";
|
|
138
|
+
|
|
139
|
+
export const metadata: Metadata = {
|
|
140
|
+
title: "assistant-ui with Assistant Transport",
|
|
141
|
+
description:
|
|
142
|
+
"An example of using assistant-ui with the assistant-transport runtime",
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export default function RootLayout({
|
|
146
|
+
children,
|
|
147
|
+
}: {
|
|
148
|
+
children: React.ReactNode;
|
|
149
|
+
}) {
|
|
150
|
+
return (
|
|
151
|
+
<html lang="en">
|
|
152
|
+
<body className="font-sans antialiased">
|
|
153
|
+
<div className="flex h-screen flex-col">
|
|
154
|
+
<header className="bg-background border-b px-4 py-2">
|
|
155
|
+
<h1 className="text-lg font-semibold">
|
|
156
|
+
Assistant Transport Example
|
|
157
|
+
</h1>
|
|
158
|
+
</header>
|
|
159
|
+
<main className="flex-1 overflow-hidden">{children}</main>
|
|
160
|
+
</div>
|
|
161
|
+
</body>
|
|
162
|
+
</html>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## app/MyRuntimeProvider.tsx
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
"use client";
|
|
172
|
+
|
|
173
|
+
import {
|
|
174
|
+
AssistantRuntimeProvider,
|
|
175
|
+
AssistantTransportConnectionMetadata,
|
|
176
|
+
makeAssistantTool,
|
|
177
|
+
} from "@assistant-ui/react";
|
|
178
|
+
import {
|
|
179
|
+
convertLangChainMessages,
|
|
180
|
+
LangChainMessage,
|
|
181
|
+
} from "@assistant-ui/react-langgraph";
|
|
182
|
+
import { useAssistantTransportRuntime } from "@assistant-ui/react";
|
|
183
|
+
import React, { ReactNode } from "react";
|
|
184
|
+
import { z } from "zod";
|
|
185
|
+
import { createMessageConverter } from "../../../packages/react/dist/legacy-runtime/runtime-cores/external-store/createMessageConverter";
|
|
186
|
+
|
|
187
|
+
// Frontend tool with execute function
|
|
188
|
+
const WeatherTool = makeAssistantTool({
|
|
189
|
+
type: "frontend",
|
|
190
|
+
toolName: "get_weather",
|
|
191
|
+
description: "Get the current weather for a city",
|
|
192
|
+
parameters: z.object({
|
|
193
|
+
location: z.string().describe("The city to get weather for"),
|
|
194
|
+
unit: z
|
|
195
|
+
.enum(["celsius", "fahrenheit"])
|
|
196
|
+
.optional()
|
|
197
|
+
.describe("Temperature unit"),
|
|
198
|
+
}),
|
|
199
|
+
execute: async ({ location, unit = "celsius" }) => {
|
|
200
|
+
console.log(`Getting weather for ${location} in ${unit}`);
|
|
201
|
+
// Simulate API call
|
|
202
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
203
|
+
|
|
204
|
+
const temp = Math.floor(Math.random() * 30) + 10;
|
|
205
|
+
const conditions = ["sunny", "cloudy", "rainy", "partly cloudy"];
|
|
206
|
+
const condition = conditions[Math.floor(Math.random() * conditions.length)];
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
location,
|
|
210
|
+
temperature: temp,
|
|
211
|
+
unit,
|
|
212
|
+
condition,
|
|
213
|
+
humidity: Math.floor(Math.random() * 40) + 40,
|
|
214
|
+
windSpeed: Math.floor(Math.random() * 20) + 5,
|
|
215
|
+
};
|
|
216
|
+
},
|
|
217
|
+
streamCall: async (reader) => {
|
|
218
|
+
console.log("streamCall", reader);
|
|
219
|
+
const city = await reader.args.get("location");
|
|
220
|
+
console.log("location", city);
|
|
221
|
+
|
|
222
|
+
const args = await reader.args.get();
|
|
223
|
+
console.log("args", args);
|
|
224
|
+
|
|
225
|
+
const result = await reader.response.get();
|
|
226
|
+
console.log("result", result);
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
type MyRuntimeProviderProps = {
|
|
231
|
+
children: ReactNode;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
type State = {
|
|
235
|
+
messages: LangChainMessage[];
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const LangChainMessageConverter = createMessageConverter(
|
|
239
|
+
convertLangChainMessages,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const converter = (
|
|
243
|
+
state: State,
|
|
244
|
+
connectionMetadata: AssistantTransportConnectionMetadata,
|
|
245
|
+
) => {
|
|
246
|
+
const optimisticStateMessages = connectionMetadata.pendingCommands.map(
|
|
247
|
+
(c): LangChainMessage[] => {
|
|
248
|
+
if (c.type === "add-message") {
|
|
249
|
+
return [
|
|
250
|
+
{
|
|
251
|
+
type: "human" as const,
|
|
252
|
+
content: [
|
|
253
|
+
{
|
|
254
|
+
type: "text" as const,
|
|
255
|
+
text: c.message.parts
|
|
256
|
+
.map((p) => (p.type === "text" ? p.text : ""))
|
|
257
|
+
.join("\n"),
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
},
|
|
261
|
+
];
|
|
262
|
+
}
|
|
263
|
+
return [];
|
|
264
|
+
},
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const messages = [...state.messages, ...optimisticStateMessages.flat()];
|
|
268
|
+
console.log({ state, messages });
|
|
269
|
+
return {
|
|
270
|
+
messages: LangChainMessageConverter.toThreadMessages(messages),
|
|
271
|
+
isRunning: connectionMetadata.isSending || false,
|
|
272
|
+
};
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
export function MyRuntimeProvider({ children }: MyRuntimeProviderProps) {
|
|
276
|
+
const runtime = useAssistantTransportRuntime({
|
|
277
|
+
initialState: {
|
|
278
|
+
messages: [],
|
|
279
|
+
},
|
|
280
|
+
api:
|
|
281
|
+
process.env["NEXT_PUBLIC_API_URL"] || "http://localhost:8010/assistant",
|
|
282
|
+
converter,
|
|
283
|
+
headers: async () => ({
|
|
284
|
+
"Test-Header": "test-value",
|
|
285
|
+
}),
|
|
286
|
+
body: {
|
|
287
|
+
"Test-Body": "test-value",
|
|
288
|
+
},
|
|
289
|
+
onResponse: () => {
|
|
290
|
+
console.log("Response received from server");
|
|
291
|
+
},
|
|
292
|
+
onFinish: () => {
|
|
293
|
+
console.log("Conversation completed");
|
|
294
|
+
},
|
|
295
|
+
onError: (error: Error) => {
|
|
296
|
+
console.error("Assistant transport error:", error);
|
|
297
|
+
},
|
|
298
|
+
onCancel: () => {
|
|
299
|
+
console.log("Request cancelled");
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
return (
|
|
304
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
305
|
+
<WeatherTool />
|
|
306
|
+
|
|
307
|
+
{children}
|
|
308
|
+
</AssistantRuntimeProvider>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## app/page.tsx
|
|
315
|
+
|
|
316
|
+
```tsx
|
|
317
|
+
"use client";
|
|
318
|
+
|
|
319
|
+
import { Thread } from "@/components/assistant-ui/thread";
|
|
320
|
+
import { MyRuntimeProvider } from "./MyRuntimeProvider";
|
|
321
|
+
|
|
322
|
+
export default function Home() {
|
|
323
|
+
return (
|
|
324
|
+
<MyRuntimeProvider>
|
|
325
|
+
<div className="h-full">
|
|
326
|
+
<Thread />
|
|
327
|
+
</div>
|
|
328
|
+
</MyRuntimeProvider>
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
## components.json
|
|
335
|
+
|
|
336
|
+
```json
|
|
337
|
+
{
|
|
338
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
339
|
+
"style": "new-york",
|
|
340
|
+
"rsc": true,
|
|
341
|
+
"tsx": true,
|
|
342
|
+
"tailwind": {
|
|
343
|
+
"config": "tailwind.config.js",
|
|
344
|
+
"css": "app/globals.css",
|
|
345
|
+
"baseColor": "zinc",
|
|
346
|
+
"cssVariables": true,
|
|
347
|
+
"prefix": ""
|
|
348
|
+
},
|
|
349
|
+
"iconLibrary": "lucide",
|
|
350
|
+
"aliases": {
|
|
351
|
+
"components": "@/components",
|
|
352
|
+
"utils": "@/lib/utils",
|
|
353
|
+
"ui": "@/components/ui",
|
|
354
|
+
"lib": "@/lib",
|
|
355
|
+
"hooks": "@/hooks"
|
|
356
|
+
},
|
|
357
|
+
"registries": {}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## components/assistant-ui/markdown-text.tsx
|
|
363
|
+
|
|
364
|
+
```tsx
|
|
365
|
+
"use client";
|
|
366
|
+
|
|
367
|
+
import "@assistant-ui/react-markdown/styles/dot.css";
|
|
368
|
+
|
|
369
|
+
import {
|
|
370
|
+
type CodeHeaderProps,
|
|
371
|
+
MarkdownTextPrimitive,
|
|
372
|
+
unstable_memoizeMarkdownComponents as memoizeMarkdownComponents,
|
|
373
|
+
useIsMarkdownCodeBlock,
|
|
374
|
+
} from "@assistant-ui/react-markdown";
|
|
375
|
+
import remarkGfm from "remark-gfm";
|
|
376
|
+
import { type FC, memo, useState } from "react";
|
|
377
|
+
import { CheckIcon, CopyIcon } from "lucide-react";
|
|
378
|
+
|
|
379
|
+
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
|
380
|
+
import { cn } from "@/lib/utils";
|
|
381
|
+
|
|
382
|
+
const MarkdownTextImpl = () => {
|
|
383
|
+
return (
|
|
384
|
+
<MarkdownTextPrimitive
|
|
385
|
+
remarkPlugins={[remarkGfm]}
|
|
386
|
+
className="aui-md"
|
|
387
|
+
components={defaultComponents}
|
|
388
|
+
/>
|
|
389
|
+
);
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
export const MarkdownText = memo(MarkdownTextImpl);
|
|
393
|
+
|
|
394
|
+
const CodeHeader: FC<CodeHeaderProps> = ({ language, code }) => {
|
|
395
|
+
const { isCopied, copyToClipboard } = useCopyToClipboard();
|
|
396
|
+
const onCopy = () => {
|
|
397
|
+
if (!code || isCopied) return;
|
|
398
|
+
copyToClipboard(code);
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
return (
|
|
402
|
+
<div className="mt-4 flex items-center justify-between gap-4 rounded-t-lg bg-zinc-900 px-4 py-2 text-sm font-semibold text-white">
|
|
403
|
+
<span className="lowercase [&>span]:text-xs">{language}</span>
|
|
404
|
+
<TooltipIconButton tooltip="Copy" onClick={onCopy}>
|
|
405
|
+
{!isCopied && <CopyIcon />}
|
|
406
|
+
{isCopied && <CheckIcon />}
|
|
407
|
+
</TooltipIconButton>
|
|
408
|
+
</div>
|
|
409
|
+
);
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const useCopyToClipboard = ({
|
|
413
|
+
copiedDuration = 3000,
|
|
414
|
+
}: {
|
|
415
|
+
copiedDuration?: number;
|
|
416
|
+
} = {}) => {
|
|
417
|
+
const [isCopied, setIsCopied] = useState<boolean>(false);
|
|
418
|
+
|
|
419
|
+
const copyToClipboard = (value: string) => {
|
|
420
|
+
if (!value) return;
|
|
421
|
+
|
|
422
|
+
navigator.clipboard.writeText(value).then(() => {
|
|
423
|
+
setIsCopied(true);
|
|
424
|
+
setTimeout(() => setIsCopied(false), copiedDuration);
|
|
425
|
+
});
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
return { isCopied, copyToClipboard };
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const defaultComponents = memoizeMarkdownComponents({
|
|
432
|
+
h1: ({ className, ...props }) => (
|
|
433
|
+
<h1
|
|
434
|
+
className={cn(
|
|
435
|
+
"mb-8 scroll-m-20 text-4xl font-extrabold tracking-tight last:mb-0",
|
|
436
|
+
className,
|
|
437
|
+
)}
|
|
438
|
+
{...props}
|
|
439
|
+
/>
|
|
440
|
+
),
|
|
441
|
+
h2: ({ className, ...props }) => (
|
|
442
|
+
<h2
|
|
443
|
+
className={cn(
|
|
444
|
+
"mb-4 mt-8 scroll-m-20 text-3xl font-semibold tracking-tight first:mt-0 last:mb-0",
|
|
445
|
+
className,
|
|
446
|
+
)}
|
|
447
|
+
{...props}
|
|
448
|
+
/>
|
|
449
|
+
),
|
|
450
|
+
h3: ({ className, ...props }) => (
|
|
451
|
+
<h3
|
|
452
|
+
className={cn(
|
|
453
|
+
"mb-4 mt-6 scroll-m-20 text-2xl font-semibold tracking-tight first:mt-0 last:mb-0",
|
|
454
|
+
className,
|
|
455
|
+
)}
|
|
456
|
+
{...props}
|
|
457
|
+
/>
|
|
458
|
+
),
|
|
459
|
+
h4: ({ className, ...props }) => (
|
|
460
|
+
<h4
|
|
461
|
+
className={cn(
|
|
462
|
+
"mb-4 mt-6 scroll-m-20 text-xl font-semibold tracking-tight first:mt-0 last:mb-0",
|
|
463
|
+
className,
|
|
464
|
+
)}
|
|
465
|
+
{...props}
|
|
466
|
+
/>
|
|
467
|
+
),
|
|
468
|
+
h5: ({ className, ...props }) => (
|
|
469
|
+
<h5
|
|
470
|
+
className={cn(
|
|
471
|
+
"my-4 text-lg font-semibold first:mt-0 last:mb-0",
|
|
472
|
+
className,
|
|
473
|
+
)}
|
|
474
|
+
{...props}
|
|
475
|
+
/>
|
|
476
|
+
),
|
|
477
|
+
h6: ({ className, ...props }) => (
|
|
478
|
+
<h6
|
|
479
|
+
className={cn("my-4 font-semibold first:mt-0 last:mb-0", className)}
|
|
480
|
+
{...props}
|
|
481
|
+
/>
|
|
482
|
+
),
|
|
483
|
+
p: ({ className, ...props }) => (
|
|
484
|
+
<p
|
|
485
|
+
className={cn("mb-5 mt-5 leading-7 first:mt-0 last:mb-0", className)}
|
|
486
|
+
{...props}
|
|
487
|
+
/>
|
|
488
|
+
),
|
|
489
|
+
a: ({ className, ...props }) => (
|
|
490
|
+
<a
|
|
491
|
+
className={cn(
|
|
492
|
+
"text-primary font-medium underline underline-offset-4",
|
|
493
|
+
className,
|
|
494
|
+
)}
|
|
495
|
+
{...props}
|
|
496
|
+
/>
|
|
497
|
+
),
|
|
498
|
+
blockquote: ({ className, ...props }) => (
|
|
499
|
+
<blockquote
|
|
500
|
+
className={cn("border-l-2 pl-6 italic", className)}
|
|
501
|
+
{...props}
|
|
502
|
+
/>
|
|
503
|
+
),
|
|
504
|
+
ul: ({ className, ...props }) => (
|
|
505
|
+
<ul
|
|
506
|
+
className={cn("my-5 ml-6 list-disc [&>li]:mt-2", className)}
|
|
507
|
+
{...props}
|
|
508
|
+
/>
|
|
509
|
+
),
|
|
510
|
+
ol: ({ className, ...props }) => (
|
|
511
|
+
<ol
|
|
512
|
+
className={cn("my-5 ml-6 list-decimal [&>li]:mt-2", className)}
|
|
513
|
+
{...props}
|
|
514
|
+
/>
|
|
515
|
+
),
|
|
516
|
+
hr: ({ className, ...props }) => (
|
|
517
|
+
<hr className={cn("my-5 border-b", className)} {...props} />
|
|
518
|
+
),
|
|
519
|
+
table: ({ className, ...props }) => (
|
|
520
|
+
<table
|
|
521
|
+
className={cn(
|
|
522
|
+
"my-5 w-full border-separate border-spacing-0 overflow-y-auto",
|
|
523
|
+
className,
|
|
524
|
+
)}
|
|
525
|
+
{...props}
|
|
526
|
+
/>
|
|
527
|
+
),
|
|
528
|
+
th: ({ className, ...props }) => (
|
|
529
|
+
<th
|
|
530
|
+
className={cn(
|
|
531
|
+
"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",
|
|
532
|
+
className,
|
|
533
|
+
)}
|
|
534
|
+
{...props}
|
|
535
|
+
/>
|
|
536
|
+
),
|
|
537
|
+
td: ({ className, ...props }) => (
|
|
538
|
+
<td
|
|
539
|
+
className={cn(
|
|
540
|
+
"border-b border-l px-4 py-2 text-left last:border-r [&[align=center]]:text-center [&[align=right]]:text-right",
|
|
541
|
+
className,
|
|
542
|
+
)}
|
|
543
|
+
{...props}
|
|
544
|
+
/>
|
|
545
|
+
),
|
|
546
|
+
tr: ({ className, ...props }) => (
|
|
547
|
+
<tr
|
|
548
|
+
className={cn(
|
|
549
|
+
"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",
|
|
550
|
+
className,
|
|
551
|
+
)}
|
|
552
|
+
{...props}
|
|
553
|
+
/>
|
|
554
|
+
),
|
|
555
|
+
sup: ({ className, ...props }) => (
|
|
556
|
+
<sup
|
|
557
|
+
className={cn("[&>a]:text-xs [&>a]:no-underline", className)}
|
|
558
|
+
{...props}
|
|
559
|
+
/>
|
|
560
|
+
),
|
|
561
|
+
pre: ({ className, ...props }) => (
|
|
562
|
+
<pre
|
|
563
|
+
className={cn(
|
|
564
|
+
"overflow-x-auto !rounded-t-none rounded-b-lg bg-black p-4 text-white",
|
|
565
|
+
className,
|
|
566
|
+
)}
|
|
567
|
+
{...props}
|
|
568
|
+
/>
|
|
569
|
+
),
|
|
570
|
+
code: function Code({ className, ...props }) {
|
|
571
|
+
const isCodeBlock = useIsMarkdownCodeBlock();
|
|
572
|
+
return (
|
|
573
|
+
<code
|
|
574
|
+
className={cn(
|
|
575
|
+
!isCodeBlock && "bg-muted rounded border font-semibold",
|
|
576
|
+
className,
|
|
577
|
+
)}
|
|
578
|
+
{...props}
|
|
579
|
+
/>
|
|
580
|
+
);
|
|
581
|
+
},
|
|
582
|
+
CodeHeader,
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
## components/assistant-ui/thread.tsx
|
|
588
|
+
|
|
589
|
+
```tsx
|
|
590
|
+
import {
|
|
591
|
+
ThreadPrimitive,
|
|
592
|
+
ComposerPrimitive,
|
|
593
|
+
MessagePrimitive,
|
|
594
|
+
ActionBarPrimitive,
|
|
595
|
+
BranchPickerPrimitive,
|
|
596
|
+
ErrorPrimitive,
|
|
597
|
+
} from "@assistant-ui/react";
|
|
598
|
+
import type { FC } from "react";
|
|
599
|
+
import {
|
|
600
|
+
ArrowDownIcon,
|
|
601
|
+
ArrowUpIcon,
|
|
602
|
+
PlusIcon,
|
|
603
|
+
CopyIcon,
|
|
604
|
+
CheckIcon,
|
|
605
|
+
PencilIcon,
|
|
606
|
+
RefreshCwIcon,
|
|
607
|
+
ChevronLeftIcon,
|
|
608
|
+
ChevronRightIcon,
|
|
609
|
+
Square,
|
|
610
|
+
} from "lucide-react";
|
|
611
|
+
|
|
612
|
+
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
|
613
|
+
import { motion } from "framer-motion";
|
|
614
|
+
import { Button } from "@/components/ui/button";
|
|
615
|
+
import { cn } from "@/lib/utils";
|
|
616
|
+
import { MarkdownText } from "./markdown-text";
|
|
617
|
+
import { ToolFallback } from "./tool-fallback";
|
|
618
|
+
|
|
619
|
+
export const Thread: FC = () => {
|
|
620
|
+
return (
|
|
621
|
+
<ThreadPrimitive.Root
|
|
622
|
+
className="bg-background flex h-full flex-col"
|
|
623
|
+
style={{
|
|
624
|
+
["--thread-max-width" as string]: "48rem",
|
|
625
|
+
["--thread-padding-x" as string]: "1rem",
|
|
626
|
+
}}
|
|
627
|
+
>
|
|
628
|
+
<ThreadPrimitive.Viewport className="relative flex min-w-0 flex-1 flex-col gap-6 overflow-y-scroll">
|
|
629
|
+
<ThreadWelcome />
|
|
630
|
+
|
|
631
|
+
<ThreadPrimitive.Messages
|
|
632
|
+
components={{
|
|
633
|
+
UserMessage,
|
|
634
|
+
EditComposer,
|
|
635
|
+
AssistantMessage,
|
|
636
|
+
}}
|
|
637
|
+
/>
|
|
638
|
+
|
|
639
|
+
<ThreadPrimitive.If empty={false}>
|
|
640
|
+
<motion.div className="min-h-6 min-w-6 shrink-0" />
|
|
641
|
+
</ThreadPrimitive.If>
|
|
642
|
+
</ThreadPrimitive.Viewport>
|
|
643
|
+
|
|
644
|
+
<Composer />
|
|
645
|
+
</ThreadPrimitive.Root>
|
|
646
|
+
);
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
const ThreadScrollToBottom: FC = () => {
|
|
650
|
+
return (
|
|
651
|
+
<ThreadPrimitive.ScrollToBottom asChild>
|
|
652
|
+
<TooltipIconButton
|
|
653
|
+
tooltip="Scroll to bottom"
|
|
654
|
+
variant="outline"
|
|
655
|
+
className="dark:bg-background dark:hover:bg-accent absolute -top-12 z-10 self-center rounded-full p-4 disabled:invisible"
|
|
656
|
+
>
|
|
657
|
+
<ArrowDownIcon />
|
|
658
|
+
</TooltipIconButton>
|
|
659
|
+
</ThreadPrimitive.ScrollToBottom>
|
|
660
|
+
);
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
const ThreadWelcome: FC = () => {
|
|
664
|
+
return (
|
|
665
|
+
<ThreadPrimitive.Empty>
|
|
666
|
+
<div className="mx-auto flex w-full max-w-[var(--thread-max-width)] flex-grow flex-col px-[var(--thread-padding-x)]">
|
|
667
|
+
<div className="flex w-full flex-grow flex-col items-center justify-center">
|
|
668
|
+
<div className="flex size-full flex-col justify-center px-8 md:mt-20">
|
|
669
|
+
<motion.div
|
|
670
|
+
initial={{ opacity: 0, y: 10 }}
|
|
671
|
+
animate={{ opacity: 1, y: 0 }}
|
|
672
|
+
exit={{ opacity: 0, y: 10 }}
|
|
673
|
+
transition={{ delay: 0.5 }}
|
|
674
|
+
className="text-2xl font-semibold"
|
|
675
|
+
>
|
|
676
|
+
Hello there!
|
|
677
|
+
</motion.div>
|
|
678
|
+
<motion.div
|
|
679
|
+
initial={{ opacity: 0, y: 10 }}
|
|
680
|
+
animate={{ opacity: 1, y: 0 }}
|
|
681
|
+
exit={{ opacity: 0, y: 10 }}
|
|
682
|
+
transition={{ delay: 0.6 }}
|
|
683
|
+
className="text-muted-foreground/65 text-2xl"
|
|
684
|
+
>
|
|
685
|
+
How can I help you today?
|
|
686
|
+
</motion.div>
|
|
687
|
+
</div>
|
|
688
|
+
</div>
|
|
689
|
+
</div>
|
|
690
|
+
</ThreadPrimitive.Empty>
|
|
691
|
+
);
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
const ThreadWelcomeSuggestions: FC = () => {
|
|
695
|
+
return (
|
|
696
|
+
<div className="grid w-full gap-2 sm:grid-cols-2">
|
|
697
|
+
{[
|
|
698
|
+
{
|
|
699
|
+
title: "What are the advantages",
|
|
700
|
+
label: "of using Assistant Cloud?",
|
|
701
|
+
action: "What are the advantages of using Assistant Cloud?",
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
title: "Write code to",
|
|
705
|
+
label: `demonstrate topological sorting`,
|
|
706
|
+
action: `Write code to demonstrate topological sorting`,
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
title: "Help me write an essay",
|
|
710
|
+
label: `about AI chat applications`,
|
|
711
|
+
action: `Help me write an essay about AI chat applications`,
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
title: "What is the weather",
|
|
715
|
+
label: "in San Francisco?",
|
|
716
|
+
action: "What is the weather in San Francisco?",
|
|
717
|
+
},
|
|
718
|
+
].map((suggestedAction, index) => (
|
|
719
|
+
<motion.div
|
|
720
|
+
initial={{ opacity: 0, y: 20 }}
|
|
721
|
+
animate={{ opacity: 1, y: 0 }}
|
|
722
|
+
exit={{ opacity: 0, y: 20 }}
|
|
723
|
+
transition={{ delay: 0.05 * index }}
|
|
724
|
+
key={`suggested-action-${suggestedAction.title}-${index}`}
|
|
725
|
+
className="[&:nth-child(n+3)]:hidden sm:[&:nth-child(n+3)]:block"
|
|
726
|
+
>
|
|
727
|
+
<ThreadPrimitive.Suggestion
|
|
728
|
+
prompt={suggestedAction.action}
|
|
729
|
+
method="replace"
|
|
730
|
+
autoSend
|
|
731
|
+
asChild
|
|
732
|
+
>
|
|
733
|
+
<Button
|
|
734
|
+
variant="ghost"
|
|
735
|
+
className="dark:hover:bg-accent/60 h-auto w-full flex-1 flex-wrap items-start justify-start gap-1 rounded-xl border px-4 py-3.5 text-left text-sm sm:flex-col"
|
|
736
|
+
aria-label={suggestedAction.action}
|
|
737
|
+
>
|
|
738
|
+
<span className="font-medium">{suggestedAction.title}</span>
|
|
739
|
+
<p className="text-muted-foreground">{suggestedAction.label}</p>
|
|
740
|
+
</Button>
|
|
741
|
+
</ThreadPrimitive.Suggestion>
|
|
742
|
+
</motion.div>
|
|
743
|
+
))}
|
|
744
|
+
</div>
|
|
745
|
+
);
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
const Composer: FC = () => {
|
|
749
|
+
return (
|
|
750
|
+
<div className="bg-background relative mx-auto flex w-full max-w-[var(--thread-max-width)] flex-col gap-4 px-[var(--thread-padding-x)] pb-4 md:pb-6">
|
|
751
|
+
<ThreadScrollToBottom />
|
|
752
|
+
<ThreadPrimitive.Empty>
|
|
753
|
+
<ThreadWelcomeSuggestions />
|
|
754
|
+
</ThreadPrimitive.Empty>
|
|
755
|
+
<ComposerPrimitive.Root className="relative flex w-full flex-col rounded-2xl focus-within:ring-2 focus-within:ring-black focus-within:ring-offset-2 dark:focus-within:ring-white">
|
|
756
|
+
<ComposerPrimitive.Input
|
|
757
|
+
placeholder="Send a message..."
|
|
758
|
+
className="bg-muted border-border dark:border-muted-foreground/15 focus:outline-primary placeholder:text-muted-foreground max-h-[calc(50dvh)] min-h-16 w-full resize-none rounded-t-2xl border-x border-t px-4 pb-3 pt-2 text-base outline-none"
|
|
759
|
+
rows={1}
|
|
760
|
+
autoFocus
|
|
761
|
+
aria-label="Message input"
|
|
762
|
+
/>
|
|
763
|
+
<ComposerAction />
|
|
764
|
+
</ComposerPrimitive.Root>
|
|
765
|
+
</div>
|
|
766
|
+
);
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
const ComposerAction: FC = () => {
|
|
770
|
+
return (
|
|
771
|
+
<div className="bg-muted border-border dark:border-muted-foreground/15 relative flex items-center justify-between rounded-b-2xl border-x border-b p-2">
|
|
772
|
+
<TooltipIconButton
|
|
773
|
+
tooltip="Attach file"
|
|
774
|
+
variant="ghost"
|
|
775
|
+
className="hover:bg-foreground/15 dark:hover:bg-background/50 scale-115 p-3.5"
|
|
776
|
+
onClick={() => {
|
|
777
|
+
console.log("Attachment clicked - not implemented");
|
|
778
|
+
}}
|
|
779
|
+
>
|
|
780
|
+
<PlusIcon />
|
|
781
|
+
</TooltipIconButton>
|
|
782
|
+
|
|
783
|
+
<ThreadPrimitive.If running={false}>
|
|
784
|
+
<ComposerPrimitive.Send asChild>
|
|
785
|
+
<Button
|
|
786
|
+
type="submit"
|
|
787
|
+
variant="default"
|
|
788
|
+
className="dark:border-muted-foreground/90 border-muted-foreground/60 hover:bg-primary/75 size-8 rounded-full border"
|
|
789
|
+
aria-label="Send message"
|
|
790
|
+
>
|
|
791
|
+
<ArrowUpIcon className="size-5" />
|
|
792
|
+
</Button>
|
|
793
|
+
</ComposerPrimitive.Send>
|
|
794
|
+
</ThreadPrimitive.If>
|
|
795
|
+
|
|
796
|
+
<ThreadPrimitive.If running>
|
|
797
|
+
<ComposerPrimitive.Cancel asChild>
|
|
798
|
+
<Button
|
|
799
|
+
type="button"
|
|
800
|
+
variant="default"
|
|
801
|
+
className="dark:border-muted-foreground/90 border-muted-foreground/60 hover:bg-primary/75 size-8 rounded-full border"
|
|
802
|
+
aria-label="Stop generating"
|
|
803
|
+
>
|
|
804
|
+
<Square className="size-3.5 fill-white dark:size-4 dark:fill-black" />
|
|
805
|
+
</Button>
|
|
806
|
+
</ComposerPrimitive.Cancel>
|
|
807
|
+
</ThreadPrimitive.If>
|
|
808
|
+
</div>
|
|
809
|
+
);
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
const MessageError: FC = () => {
|
|
813
|
+
return (
|
|
814
|
+
<MessagePrimitive.Error>
|
|
815
|
+
<ErrorPrimitive.Root className="border-destructive bg-destructive/10 dark:bg-destructive/5 text-destructive mt-2 rounded-md border p-3 text-sm dark:text-red-200">
|
|
816
|
+
<ErrorPrimitive.Message className="line-clamp-2" />
|
|
817
|
+
</ErrorPrimitive.Root>
|
|
818
|
+
</MessagePrimitive.Error>
|
|
819
|
+
);
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
const AssistantMessage: FC = () => {
|
|
823
|
+
return (
|
|
824
|
+
<MessagePrimitive.Root asChild>
|
|
825
|
+
<motion.div
|
|
826
|
+
className="relative mx-auto grid w-full max-w-[var(--thread-max-width)] grid-cols-[auto_auto_1fr] grid-rows-[auto_1fr] px-[var(--thread-padding-x)] py-4"
|
|
827
|
+
initial={{ y: 5, opacity: 0 }}
|
|
828
|
+
animate={{ y: 0, opacity: 1 }}
|
|
829
|
+
data-role="assistant"
|
|
830
|
+
>
|
|
831
|
+
<div className="ring-border bg-background col-start-1 row-start-1 flex size-8 shrink-0 items-center justify-center rounded-full ring-1">
|
|
832
|
+
<StarIcon size={14} />
|
|
833
|
+
</div>
|
|
834
|
+
|
|
835
|
+
<div className="text-foreground col-span-2 col-start-2 row-start-1 ml-4 break-words leading-7">
|
|
836
|
+
<MessagePrimitive.Content
|
|
837
|
+
components={{
|
|
838
|
+
Text: MarkdownText,
|
|
839
|
+
tools: { Fallback: ToolFallback },
|
|
840
|
+
}}
|
|
841
|
+
/>
|
|
842
|
+
<MessageError />
|
|
843
|
+
</div>
|
|
844
|
+
|
|
845
|
+
<AssistantActionBar />
|
|
846
|
+
|
|
847
|
+
<BranchPicker className="col-start-2 row-start-2 -ml-2 mr-2" />
|
|
848
|
+
</motion.div>
|
|
849
|
+
</MessagePrimitive.Root>
|
|
850
|
+
);
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
const AssistantActionBar: FC = () => {
|
|
854
|
+
return (
|
|
855
|
+
<ActionBarPrimitive.Root
|
|
856
|
+
hideWhenRunning
|
|
857
|
+
autohide="not-last"
|
|
858
|
+
autohideFloat="single-branch"
|
|
859
|
+
className="text-muted-foreground data-floating:bg-background data-floating:absolute data-floating:mt-2 data-floating:rounded-md data-floating:border data-floating:p-1 data-floating:shadow-sm col-start-3 row-start-2 ml-3 mt-3 flex gap-1"
|
|
860
|
+
>
|
|
861
|
+
<ActionBarPrimitive.Copy asChild>
|
|
862
|
+
<TooltipIconButton tooltip="Copy">
|
|
863
|
+
<MessagePrimitive.If copied>
|
|
864
|
+
<CheckIcon />
|
|
865
|
+
</MessagePrimitive.If>
|
|
866
|
+
<MessagePrimitive.If copied={false}>
|
|
867
|
+
<CopyIcon />
|
|
868
|
+
</MessagePrimitive.If>
|
|
869
|
+
</TooltipIconButton>
|
|
870
|
+
</ActionBarPrimitive.Copy>
|
|
871
|
+
<ActionBarPrimitive.Reload asChild>
|
|
872
|
+
<TooltipIconButton tooltip="Refresh">
|
|
873
|
+
<RefreshCwIcon />
|
|
874
|
+
</TooltipIconButton>
|
|
875
|
+
</ActionBarPrimitive.Reload>
|
|
876
|
+
</ActionBarPrimitive.Root>
|
|
877
|
+
);
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
const UserMessage: FC = () => {
|
|
881
|
+
return (
|
|
882
|
+
<MessagePrimitive.Root asChild>
|
|
883
|
+
<motion.div
|
|
884
|
+
className="mx-auto grid w-full max-w-[var(--thread-max-width)] auto-rows-auto grid-cols-[minmax(72px,1fr)_auto] gap-y-1 px-[var(--thread-padding-x)] py-4 [&:where(>*)]:col-start-2"
|
|
885
|
+
initial={{ y: 5, opacity: 0 }}
|
|
886
|
+
animate={{ y: 0, opacity: 1 }}
|
|
887
|
+
data-role="user"
|
|
888
|
+
>
|
|
889
|
+
<UserActionBar />
|
|
890
|
+
|
|
891
|
+
<div className="bg-muted text-foreground col-start-2 break-words rounded-3xl px-5 py-2.5">
|
|
892
|
+
<MessagePrimitive.Content components={{ Text: MarkdownText }} />
|
|
893
|
+
</div>
|
|
894
|
+
|
|
895
|
+
<BranchPicker className="col-span-full col-start-1 row-start-3 -mr-1 justify-end" />
|
|
896
|
+
</motion.div>
|
|
897
|
+
</MessagePrimitive.Root>
|
|
898
|
+
);
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
const UserActionBar: FC = () => {
|
|
902
|
+
return (
|
|
903
|
+
<ActionBarPrimitive.Root
|
|
904
|
+
hideWhenRunning
|
|
905
|
+
autohide="not-last"
|
|
906
|
+
className="col-start-1 mr-3 mt-2.5 flex flex-col items-end"
|
|
907
|
+
>
|
|
908
|
+
<ActionBarPrimitive.Edit asChild>
|
|
909
|
+
<TooltipIconButton tooltip="Edit">
|
|
910
|
+
<PencilIcon />
|
|
911
|
+
</TooltipIconButton>
|
|
912
|
+
</ActionBarPrimitive.Edit>
|
|
913
|
+
</ActionBarPrimitive.Root>
|
|
914
|
+
);
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
const EditComposer: FC = () => {
|
|
918
|
+
return (
|
|
919
|
+
<div className="mx-auto flex w-full max-w-[var(--thread-max-width)] flex-col gap-4 px-[var(--thread-padding-x)]">
|
|
920
|
+
<ComposerPrimitive.Root className="bg-muted max-w-7/8 ml-auto flex w-full flex-col rounded-xl">
|
|
921
|
+
<ComposerPrimitive.Input
|
|
922
|
+
className="text-foreground flex min-h-[60px] w-full resize-none bg-transparent p-4 outline-none"
|
|
923
|
+
autoFocus
|
|
924
|
+
/>
|
|
925
|
+
|
|
926
|
+
<div className="mx-3 mb-3 flex items-center justify-center gap-2 self-end">
|
|
927
|
+
<ComposerPrimitive.Cancel asChild>
|
|
928
|
+
<Button variant="ghost" size="sm" aria-label="Cancel edit">
|
|
929
|
+
Cancel
|
|
930
|
+
</Button>
|
|
931
|
+
</ComposerPrimitive.Cancel>
|
|
932
|
+
<ComposerPrimitive.Send asChild>
|
|
933
|
+
<Button size="sm" aria-label="Update message">
|
|
934
|
+
Update
|
|
935
|
+
</Button>
|
|
936
|
+
</ComposerPrimitive.Send>
|
|
937
|
+
</div>
|
|
938
|
+
</ComposerPrimitive.Root>
|
|
939
|
+
</div>
|
|
940
|
+
);
|
|
941
|
+
};
|
|
942
|
+
|
|
943
|
+
const BranchPicker: FC<BranchPickerPrimitive.Root.Props> = ({
|
|
944
|
+
className,
|
|
945
|
+
...rest
|
|
946
|
+
}) => {
|
|
947
|
+
return (
|
|
948
|
+
<BranchPickerPrimitive.Root
|
|
949
|
+
hideWhenSingleBranch
|
|
950
|
+
className={cn(
|
|
951
|
+
"text-muted-foreground inline-flex items-center text-xs",
|
|
952
|
+
className,
|
|
953
|
+
)}
|
|
954
|
+
{...rest}
|
|
955
|
+
>
|
|
956
|
+
<BranchPickerPrimitive.Previous asChild>
|
|
957
|
+
<TooltipIconButton tooltip="Previous">
|
|
958
|
+
<ChevronLeftIcon />
|
|
959
|
+
</TooltipIconButton>
|
|
960
|
+
</BranchPickerPrimitive.Previous>
|
|
961
|
+
<span className="font-medium">
|
|
962
|
+
<BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />
|
|
963
|
+
</span>
|
|
964
|
+
<BranchPickerPrimitive.Next asChild>
|
|
965
|
+
<TooltipIconButton tooltip="Next">
|
|
966
|
+
<ChevronRightIcon />
|
|
967
|
+
</TooltipIconButton>
|
|
968
|
+
</BranchPickerPrimitive.Next>
|
|
969
|
+
</BranchPickerPrimitive.Root>
|
|
970
|
+
);
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
const StarIcon = ({ size = 14 }: { size?: number }) => (
|
|
974
|
+
<svg
|
|
975
|
+
width={size}
|
|
976
|
+
height={size}
|
|
977
|
+
viewBox="0 0 16 16"
|
|
978
|
+
fill="none"
|
|
979
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
980
|
+
>
|
|
981
|
+
<path
|
|
982
|
+
d="M8 0L9.79611 6.20389L16 8L9.79611 9.79611L8 16L6.20389 9.79611L0 8L6.20389 6.20389L8 0Z"
|
|
983
|
+
fill="currentColor"
|
|
984
|
+
/>
|
|
985
|
+
</svg>
|
|
986
|
+
);
|
|
987
|
+
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
## components/assistant-ui/tool-fallback.tsx
|
|
991
|
+
|
|
992
|
+
```tsx
|
|
993
|
+
import { ToolCallMessagePartComponent } from "@assistant-ui/react";
|
|
994
|
+
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
|
995
|
+
import { useState } from "react";
|
|
996
|
+
import { Button } from "../ui/button";
|
|
997
|
+
|
|
998
|
+
export const ToolFallback: ToolCallMessagePartComponent = ({
|
|
999
|
+
toolName,
|
|
1000
|
+
argsText,
|
|
1001
|
+
result,
|
|
1002
|
+
}) => {
|
|
1003
|
+
const [isCollapsed, setIsCollapsed] = useState(true);
|
|
1004
|
+
return (
|
|
1005
|
+
<div className="mb-4 flex w-full flex-col gap-3 rounded-lg border py-3">
|
|
1006
|
+
<div className="flex items-center gap-2 px-4">
|
|
1007
|
+
<CheckIcon className="size-4" />
|
|
1008
|
+
<p className="flex-grow">
|
|
1009
|
+
Used tool: <b>{toolName}</b>
|
|
1010
|
+
</p>
|
|
1011
|
+
<Button onClick={() => setIsCollapsed(!isCollapsed)}>
|
|
1012
|
+
{isCollapsed ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
|
1013
|
+
</Button>
|
|
1014
|
+
</div>
|
|
1015
|
+
{!isCollapsed && (
|
|
1016
|
+
<div className="flex flex-col gap-2 border-t pt-2">
|
|
1017
|
+
<div className="px-4">
|
|
1018
|
+
<pre className="whitespace-pre-wrap">{argsText}</pre>
|
|
1019
|
+
</div>
|
|
1020
|
+
{result !== undefined && (
|
|
1021
|
+
<div className="border-t border-dashed px-4 pt-2">
|
|
1022
|
+
<p className="font-semibold">Result:</p>
|
|
1023
|
+
<pre className="whitespace-pre-wrap">
|
|
1024
|
+
{typeof result === "string"
|
|
1025
|
+
? result
|
|
1026
|
+
: JSON.stringify(result, null, 2)}
|
|
1027
|
+
</pre>
|
|
1028
|
+
</div>
|
|
1029
|
+
)}
|
|
1030
|
+
</div>
|
|
1031
|
+
)}
|
|
1032
|
+
</div>
|
|
1033
|
+
);
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
```
|
|
1037
|
+
|
|
1038
|
+
## components/assistant-ui/tooltip-icon-button.tsx
|
|
1039
|
+
|
|
1040
|
+
```tsx
|
|
1041
|
+
"use client";
|
|
1042
|
+
|
|
1043
|
+
import { ComponentPropsWithoutRef, forwardRef } from "react";
|
|
1044
|
+
import { Slottable } from "@radix-ui/react-slot";
|
|
1045
|
+
|
|
1046
|
+
import {
|
|
1047
|
+
Tooltip,
|
|
1048
|
+
TooltipContent,
|
|
1049
|
+
TooltipTrigger,
|
|
1050
|
+
} from "@/components/ui/tooltip";
|
|
1051
|
+
import { Button } from "@/components/ui/button";
|
|
1052
|
+
import { cn } from "@/lib/utils";
|
|
1053
|
+
|
|
1054
|
+
export type TooltipIconButtonProps = ComponentPropsWithoutRef<typeof Button> & {
|
|
1055
|
+
tooltip: string;
|
|
1056
|
+
side?: "top" | "bottom" | "left" | "right";
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
export const TooltipIconButton = forwardRef<
|
|
1060
|
+
HTMLButtonElement,
|
|
1061
|
+
TooltipIconButtonProps
|
|
1062
|
+
>(({ children, tooltip, side = "bottom", className, ...rest }, ref) => {
|
|
1063
|
+
return (
|
|
1064
|
+
<Tooltip>
|
|
1065
|
+
<TooltipTrigger asChild>
|
|
1066
|
+
<Button
|
|
1067
|
+
variant="ghost"
|
|
1068
|
+
size="icon"
|
|
1069
|
+
{...rest}
|
|
1070
|
+
className={cn("size-6 p-1", className)}
|
|
1071
|
+
ref={ref}
|
|
1072
|
+
>
|
|
1073
|
+
<Slottable>{children}</Slottable>
|
|
1074
|
+
<span className="sr-only">{tooltip}</span>
|
|
1075
|
+
</Button>
|
|
1076
|
+
</TooltipTrigger>
|
|
1077
|
+
<TooltipContent side={side}>{tooltip}</TooltipContent>
|
|
1078
|
+
</Tooltip>
|
|
1079
|
+
);
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
TooltipIconButton.displayName = "TooltipIconButton";
|
|
1083
|
+
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
## components/ui/button.tsx
|
|
1087
|
+
|
|
1088
|
+
```tsx
|
|
1089
|
+
import * as React from "react";
|
|
1090
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
1091
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
1092
|
+
|
|
1093
|
+
import { cn } from "@/lib/utils";
|
|
1094
|
+
|
|
1095
|
+
const buttonVariants = cva(
|
|
1096
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
1097
|
+
{
|
|
1098
|
+
variants: {
|
|
1099
|
+
variant: {
|
|
1100
|
+
default:
|
|
1101
|
+
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
|
1102
|
+
destructive:
|
|
1103
|
+
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
1104
|
+
outline:
|
|
1105
|
+
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
|
1106
|
+
secondary:
|
|
1107
|
+
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
|
1108
|
+
ghost:
|
|
1109
|
+
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
1110
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
1111
|
+
},
|
|
1112
|
+
size: {
|
|
1113
|
+
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
1114
|
+
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
1115
|
+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
1116
|
+
icon: "size-9",
|
|
1117
|
+
},
|
|
1118
|
+
},
|
|
1119
|
+
defaultVariants: {
|
|
1120
|
+
variant: "default",
|
|
1121
|
+
size: "default",
|
|
1122
|
+
},
|
|
1123
|
+
},
|
|
1124
|
+
);
|
|
1125
|
+
|
|
1126
|
+
function Button({
|
|
1127
|
+
className,
|
|
1128
|
+
variant,
|
|
1129
|
+
size,
|
|
1130
|
+
asChild = false,
|
|
1131
|
+
...props
|
|
1132
|
+
}: React.ComponentProps<"button"> &
|
|
1133
|
+
VariantProps<typeof buttonVariants> & {
|
|
1134
|
+
asChild?: boolean;
|
|
1135
|
+
}) {
|
|
1136
|
+
const Comp = asChild ? Slot : "button";
|
|
1137
|
+
|
|
1138
|
+
return (
|
|
1139
|
+
<Comp
|
|
1140
|
+
data-slot="button"
|
|
1141
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
1142
|
+
{...props}
|
|
1143
|
+
/>
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
export { Button, buttonVariants };
|
|
1148
|
+
|
|
1149
|
+
```
|
|
1150
|
+
|
|
1151
|
+
## components/ui/tooltip.tsx
|
|
1152
|
+
|
|
1153
|
+
```tsx
|
|
1154
|
+
"use client";
|
|
1155
|
+
|
|
1156
|
+
import * as React from "react";
|
|
1157
|
+
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
1158
|
+
|
|
1159
|
+
import { cn } from "@/lib/utils";
|
|
1160
|
+
|
|
1161
|
+
function TooltipProvider({
|
|
1162
|
+
delayDuration = 0,
|
|
1163
|
+
...props
|
|
1164
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
|
1165
|
+
return (
|
|
1166
|
+
<TooltipPrimitive.Provider
|
|
1167
|
+
data-slot="tooltip-provider"
|
|
1168
|
+
delayDuration={delayDuration}
|
|
1169
|
+
{...props}
|
|
1170
|
+
/>
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function Tooltip({
|
|
1175
|
+
...props
|
|
1176
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
|
1177
|
+
return (
|
|
1178
|
+
<TooltipProvider>
|
|
1179
|
+
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
|
1180
|
+
</TooltipProvider>
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function TooltipTrigger({
|
|
1185
|
+
...props
|
|
1186
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
|
1187
|
+
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function TooltipContent({
|
|
1191
|
+
className,
|
|
1192
|
+
sideOffset = 0,
|
|
1193
|
+
children,
|
|
1194
|
+
...props
|
|
1195
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
|
1196
|
+
return (
|
|
1197
|
+
<TooltipPrimitive.Portal>
|
|
1198
|
+
<TooltipPrimitive.Content
|
|
1199
|
+
data-slot="tooltip-content"
|
|
1200
|
+
sideOffset={sideOffset}
|
|
1201
|
+
className={cn(
|
|
1202
|
+
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out 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 origin-(--radix-tooltip-content-transform-origin) z-50 w-fit text-balance rounded-md px-3 py-1.5 text-xs",
|
|
1203
|
+
className,
|
|
1204
|
+
)}
|
|
1205
|
+
{...props}
|
|
1206
|
+
>
|
|
1207
|
+
{children}
|
|
1208
|
+
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
|
1209
|
+
</TooltipPrimitive.Content>
|
|
1210
|
+
</TooltipPrimitive.Portal>
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
|
1215
|
+
|
|
1216
|
+
```
|
|
1217
|
+
|
|
1218
|
+
## eslint.config.ts
|
|
1219
|
+
|
|
1220
|
+
```typescript
|
|
1221
|
+
export { default } from "@assistant-ui/x-buildutils/eslint";
|
|
1222
|
+
|
|
1223
|
+
```
|
|
1224
|
+
|
|
1225
|
+
## lib/utils.ts
|
|
1226
|
+
|
|
1227
|
+
```typescript
|
|
1228
|
+
import { clsx, type ClassValue } from "clsx";
|
|
1229
|
+
import { twMerge } from "tailwind-merge";
|
|
1230
|
+
|
|
1231
|
+
export function cn(...inputs: ClassValue[]) {
|
|
1232
|
+
return twMerge(clsx(inputs));
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
```
|
|
1236
|
+
|
|
1237
|
+
## next.config.js
|
|
1238
|
+
|
|
1239
|
+
```javascript
|
|
1240
|
+
/** @type {import('next').NextConfig} */
|
|
1241
|
+
const nextConfig = {
|
|
1242
|
+
experimental: {
|
|
1243
|
+
optimizePackageImports: ["@assistant-ui/react"],
|
|
1244
|
+
},
|
|
1245
|
+
async rewrites() {
|
|
1246
|
+
return [
|
|
1247
|
+
{
|
|
1248
|
+
source: "/assistant/:path*",
|
|
1249
|
+
destination: "http://localhost:8000/assistant/:path*",
|
|
1250
|
+
},
|
|
1251
|
+
];
|
|
1252
|
+
},
|
|
1253
|
+
};
|
|
1254
|
+
|
|
1255
|
+
export default nextConfig;
|
|
1256
|
+
|
|
1257
|
+
```
|
|
1258
|
+
|
|
1259
|
+
## package.json
|
|
1260
|
+
|
|
1261
|
+
```json
|
|
1262
|
+
{
|
|
1263
|
+
"name": "example-with-assistant-transport",
|
|
1264
|
+
"private": true,
|
|
1265
|
+
"version": "0.0.0",
|
|
1266
|
+
"type": "module",
|
|
1267
|
+
"dependencies": {
|
|
1268
|
+
"@assistant-ui/react": "workspace:^",
|
|
1269
|
+
"@assistant-ui/react-langgraph": "workspace:^",
|
|
1270
|
+
"@assistant-ui/react-markdown": "workspace:^",
|
|
1271
|
+
"@radix-ui/react-slot": "^1.2.3",
|
|
1272
|
+
"@radix-ui/react-tooltip": "^1.2.8",
|
|
1273
|
+
"@tailwindcss/postcss": "^4.1.13",
|
|
1274
|
+
"assistant-stream": "^0.2.30",
|
|
1275
|
+
"class-variance-authority": "^0.7.1",
|
|
1276
|
+
"clsx": "^2.1.1",
|
|
1277
|
+
"framer-motion": "^12.23.22",
|
|
1278
|
+
"lucide-react": "^0.544.0",
|
|
1279
|
+
"next": "15.5.4",
|
|
1280
|
+
"postcss": "^8.5.6",
|
|
1281
|
+
"react": "19.1.1",
|
|
1282
|
+
"react-dom": "19.1.1",
|
|
1283
|
+
"remark-gfm": "^4.0.1",
|
|
1284
|
+
"tailwind-merge": "^3.3.1",
|
|
1285
|
+
"tailwindcss": "^4.1.13",
|
|
1286
|
+
"tailwindcss-animate": "^1.0.7",
|
|
1287
|
+
"zod": "^4.1.11"
|
|
1288
|
+
},
|
|
1289
|
+
"devDependencies": {
|
|
1290
|
+
"@assistant-ui/x-buildutils": "workspace:*",
|
|
1291
|
+
"@types/node": "^24.5.2",
|
|
1292
|
+
"@types/react": "^19.1.15",
|
|
1293
|
+
"@types/react-dom": "^19.1.9",
|
|
1294
|
+
"eslint": "^9",
|
|
1295
|
+
"eslint-config-next": "15.5.4",
|
|
1296
|
+
"tw-animate-css": "^1.4.0",
|
|
1297
|
+
"typescript": "^5.9.2"
|
|
1298
|
+
},
|
|
1299
|
+
"scripts": {
|
|
1300
|
+
"dev": "next dev",
|
|
1301
|
+
"build": "next build",
|
|
1302
|
+
"start": "next start",
|
|
1303
|
+
"lint": "eslint ."
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
```
|
|
1308
|
+
|
|
1309
|
+
## postcss.config.js
|
|
1310
|
+
|
|
1311
|
+
```javascript
|
|
1312
|
+
const config = {
|
|
1313
|
+
plugins: {
|
|
1314
|
+
"@tailwindcss/postcss": {},
|
|
1315
|
+
},
|
|
1316
|
+
};
|
|
1317
|
+
|
|
1318
|
+
export default config;
|
|
1319
|
+
|
|
1320
|
+
```
|
|
1321
|
+
|
|
1322
|
+
## README.md
|
|
1323
|
+
|
|
1324
|
+
```markdown
|
|
1325
|
+
# Assistant Transport Example
|
|
1326
|
+
|
|
1327
|
+
This example demonstrates how to use assistant-ui with the `useAssistantTransportRuntime` hook to connect to a custom backend server that implements the assistant-transport protocol.
|
|
1328
|
+
|
|
1329
|
+
## Overview
|
|
1330
|
+
|
|
1331
|
+
The Assistant Transport runtime allows you to connect assistant-ui to any backend server that can handle:
|
|
1332
|
+
|
|
1333
|
+
- `AddMessageCommand` - for sending user messages
|
|
1334
|
+
- `AddToolResultCommand` - for sending tool execution results
|
|
1335
|
+
- Streaming responses using the `assistant-stream` format
|
|
1336
|
+
|
|
1337
|
+
## Prerequisites
|
|
1338
|
+
|
|
1339
|
+
Before running this example, you'll need:
|
|
1340
|
+
|
|
1341
|
+
1. A backend server that implements the assistant-transport protocol
|
|
1342
|
+
2. Node.js 18+ installed
|
|
1343
|
+
3. pnpm package manager
|
|
1344
|
+
|
|
1345
|
+
## Getting Started
|
|
1346
|
+
|
|
1347
|
+
### 1. Install Dependencies
|
|
1348
|
+
|
|
1349
|
+
```bash
|
|
1350
|
+
pnpm install
|
|
1351
|
+
```
|
|
1352
|
+
|
|
1353
|
+
### 2. Set Up Environment Variables
|
|
1354
|
+
|
|
1355
|
+
Copy the example environment file:
|
|
1356
|
+
|
|
1357
|
+
```bash
|
|
1358
|
+
cp .env.local.example .env.local
|
|
1359
|
+
```
|
|
1360
|
+
|
|
1361
|
+
Update the `NEXT_PUBLIC_API_URL` in `.env.local` to point to your backend server:
|
|
1362
|
+
|
|
1363
|
+
```env
|
|
1364
|
+
NEXT_PUBLIC_API_URL=http://localhost:8000/assistant
|
|
1365
|
+
```
|
|
1366
|
+
|
|
1367
|
+
### 3. Start the Development Server
|
|
1368
|
+
|
|
1369
|
+
```bash
|
|
1370
|
+
pnpm dev
|
|
1371
|
+
```
|
|
1372
|
+
|
|
1373
|
+
The application will be available at [http://localhost:3000](http://localhost:3000).
|
|
1374
|
+
|
|
1375
|
+
## Backend Server Requirements
|
|
1376
|
+
|
|
1377
|
+
Your backend server should:
|
|
1378
|
+
|
|
1379
|
+
1. Accept POST requests at the configured endpoint (e.g., `/assistant`)
|
|
1380
|
+
2. Handle the following command types in the request body:
|
|
1381
|
+
- `AddMessageCommand`: `{ type: "add-message", message: { role: "user", parts: [...] } }`
|
|
1382
|
+
- `AddToolResultCommand`: `{ type: "add-tool-result", toolCallId: string, result: object }`
|
|
1383
|
+
3. Return streaming responses using the `assistant-stream` format
|
|
1384
|
+
4. Include CORS headers to allow requests from the frontend
|
|
1385
|
+
|
|
1386
|
+
### Example Request Format
|
|
1387
|
+
|
|
1388
|
+
```json
|
|
1389
|
+
{
|
|
1390
|
+
"commands": [
|
|
1391
|
+
{
|
|
1392
|
+
"type": "add-message",
|
|
1393
|
+
"message": {
|
|
1394
|
+
"role": "user",
|
|
1395
|
+
"parts": [
|
|
1396
|
+
{
|
|
1397
|
+
"type": "text",
|
|
1398
|
+
"text": "Hello, how are you?"
|
|
1399
|
+
}
|
|
1400
|
+
]
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
],
|
|
1404
|
+
"system": "You are a helpful assistant",
|
|
1405
|
+
"tools": {
|
|
1406
|
+
"get_weather": {
|
|
1407
|
+
"description": "Get weather information",
|
|
1408
|
+
"parameters": {
|
|
1409
|
+
"type": "object",
|
|
1410
|
+
"properties": {
|
|
1411
|
+
"location": { "type": "string" }
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
```
|
|
1418
|
+
|
|
1419
|
+
## Project Structure
|
|
1420
|
+
|
|
1421
|
+
```
|
|
1422
|
+
examples/with-assistant-transport/
|
|
1423
|
+
├── app/
|
|
1424
|
+
│ ├── globals.css # Global styles with Tailwind CSS
|
|
1425
|
+
│ ├── layout.tsx # Root layout component
|
|
1426
|
+
│ ├── MyRuntimeProvider.tsx # Custom runtime provider using useAssistantTransportRuntime
|
|
1427
|
+
│ └── page.tsx # Main page component
|
|
1428
|
+
├── components/
|
|
1429
|
+
│ └── assistant-ui/
|
|
1430
|
+
│ └── thread.tsx # Thread component for the chat interface
|
|
1431
|
+
├── package.json # Project dependencies
|
|
1432
|
+
├── tailwind.config.js # Tailwind CSS configuration
|
|
1433
|
+
├── tsconfig.json # TypeScript configuration
|
|
1434
|
+
├── next.config.js # Next.js configuration
|
|
1435
|
+
└── README.md # This file
|
|
1436
|
+
```
|
|
1437
|
+
|
|
1438
|
+
## Key Features
|
|
1439
|
+
|
|
1440
|
+
- **Custom Runtime**: Uses `useAssistantTransportRuntime` to connect to any backend
|
|
1441
|
+
- **Streaming Support**: Handles real-time streaming responses from the server
|
|
1442
|
+
- **Tool Support**: Supports tool calling between frontend and backend
|
|
1443
|
+
- **Error Handling**: Includes proper error handling and loading states
|
|
1444
|
+
- **Modern UI**: Built with Tailwind CSS and Radix UI components
|
|
1445
|
+
|
|
1446
|
+
## Backend Examples
|
|
1447
|
+
|
|
1448
|
+
For a complete working backend example, check out:
|
|
1449
|
+
|
|
1450
|
+
- `examples/ultrathink` - Python FastAPI server with assistant-stream integration
|
|
1451
|
+
|
|
1452
|
+
## Customization
|
|
1453
|
+
|
|
1454
|
+
### Modifying the Runtime Configuration
|
|
1455
|
+
|
|
1456
|
+
Edit `app/MyRuntimeProvider.tsx` to customize:
|
|
1457
|
+
|
|
1458
|
+
- **API Endpoint**: Change the `api` URL
|
|
1459
|
+
- **Headers**: Add authentication or other headers
|
|
1460
|
+
- **Body Parameters**: Add additional request parameters
|
|
1461
|
+
- **Event Handlers**: Customize response, error, and completion handling
|
|
1462
|
+
- **State Converter**: Modify how backend state is converted to frontend state
|
|
1463
|
+
|
|
1464
|
+
### Styling
|
|
1465
|
+
|
|
1466
|
+
The project uses Tailwind CSS for styling. Modify `app/globals.css` and `tailwind.config.js` to customize the appearance.
|
|
1467
|
+
|
|
1468
|
+
## Troubleshooting
|
|
1469
|
+
|
|
1470
|
+
### Backend Connection Issues
|
|
1471
|
+
|
|
1472
|
+
1. Ensure your backend server is running and accessible
|
|
1473
|
+
2. Check CORS configuration on your backend
|
|
1474
|
+
3. Verify the API endpoint URL in your `.env.local` file
|
|
1475
|
+
4. Check the browser console for network errors
|
|
1476
|
+
|
|
1477
|
+
### Runtime Errors
|
|
1478
|
+
|
|
1479
|
+
1. Verify the backend response format matches assistant-stream expectations
|
|
1480
|
+
2. Check that the state converter function properly transforms your backend state
|
|
1481
|
+
3. Ensure all required dependencies are installed
|
|
1482
|
+
|
|
1483
|
+
## Learn More
|
|
1484
|
+
|
|
1485
|
+
- [assistant-ui Documentation](https://docs.assistant-ui.com)
|
|
1486
|
+
- [Assistant Transport Runtime API](https://docs.assistant-ui.com/runtimes/assistant-transport)
|
|
1487
|
+
- [Next.js Documentation](https://nextjs.org/docs)
|
|
1488
|
+
|
|
1489
|
+
```
|
|
1490
|
+
|
|
1491
|
+
## tailwind.config.js
|
|
1492
|
+
|
|
1493
|
+
```javascript
|
|
1494
|
+
/** @type {import('tailwindcss').Config} */
|
|
1495
|
+
const config = {
|
|
1496
|
+
darkMode: ["class"],
|
|
1497
|
+
content: [
|
|
1498
|
+
"./pages/**/*.{ts,tsx}",
|
|
1499
|
+
"./components/**/*.{ts,tsx}",
|
|
1500
|
+
"./app/**/*.{ts,tsx}",
|
|
1501
|
+
"./src/**/*.{ts,tsx}",
|
|
1502
|
+
],
|
|
1503
|
+
prefix: "",
|
|
1504
|
+
theme: {
|
|
1505
|
+
container: {
|
|
1506
|
+
center: true,
|
|
1507
|
+
padding: "2rem",
|
|
1508
|
+
screens: {
|
|
1509
|
+
"2xl": "1400px",
|
|
1510
|
+
},
|
|
1511
|
+
},
|
|
1512
|
+
extend: {
|
|
1513
|
+
colors: {
|
|
1514
|
+
border: "hsl(var(--border))",
|
|
1515
|
+
input: "hsl(var(--input))",
|
|
1516
|
+
ring: "hsl(var(--ring))",
|
|
1517
|
+
background: "hsl(var(--background))",
|
|
1518
|
+
foreground: "hsl(var(--foreground))",
|
|
1519
|
+
primary: {
|
|
1520
|
+
DEFAULT: "hsl(var(--primary))",
|
|
1521
|
+
foreground: "hsl(var(--primary-foreground))",
|
|
1522
|
+
},
|
|
1523
|
+
secondary: {
|
|
1524
|
+
DEFAULT: "hsl(var(--secondary))",
|
|
1525
|
+
foreground: "hsl(var(--secondary-foreground))",
|
|
1526
|
+
},
|
|
1527
|
+
destructive: {
|
|
1528
|
+
DEFAULT: "hsl(var(--destructive))",
|
|
1529
|
+
foreground: "hsl(var(--destructive-foreground))",
|
|
1530
|
+
},
|
|
1531
|
+
muted: {
|
|
1532
|
+
DEFAULT: "hsl(var(--muted))",
|
|
1533
|
+
foreground: "hsl(var(--muted-foreground))",
|
|
1534
|
+
},
|
|
1535
|
+
accent: {
|
|
1536
|
+
DEFAULT: "hsl(var(--accent))",
|
|
1537
|
+
foreground: "hsl(var(--accent-foreground))",
|
|
1538
|
+
},
|
|
1539
|
+
popover: {
|
|
1540
|
+
DEFAULT: "hsl(var(--popover))",
|
|
1541
|
+
foreground: "hsl(var(--popover-foreground))",
|
|
1542
|
+
},
|
|
1543
|
+
card: {
|
|
1544
|
+
DEFAULT: "hsl(var(--card))",
|
|
1545
|
+
foreground: "hsl(var(--card-foreground))",
|
|
1546
|
+
},
|
|
1547
|
+
},
|
|
1548
|
+
borderRadius: {
|
|
1549
|
+
lg: "var(--radius)",
|
|
1550
|
+
md: "calc(var(--radius) - 2px)",
|
|
1551
|
+
sm: "calc(var(--radius) - 4px)",
|
|
1552
|
+
},
|
|
1553
|
+
keyframes: {
|
|
1554
|
+
"accordion-down": {
|
|
1555
|
+
from: { height: "0" },
|
|
1556
|
+
to: { height: "var(--radix-accordion-content-height)" },
|
|
1557
|
+
},
|
|
1558
|
+
"accordion-up": {
|
|
1559
|
+
from: { height: "var(--radix-accordion-content-height)" },
|
|
1560
|
+
to: { height: "0" },
|
|
1561
|
+
},
|
|
1562
|
+
},
|
|
1563
|
+
animation: {
|
|
1564
|
+
"accordion-down": "accordion-down 0.2s ease-out",
|
|
1565
|
+
"accordion-up": "accordion-up 0.2s ease-out",
|
|
1566
|
+
},
|
|
1567
|
+
},
|
|
1568
|
+
},
|
|
1569
|
+
plugins: [],
|
|
1570
|
+
};
|
|
1571
|
+
|
|
1572
|
+
export default config;
|
|
1573
|
+
|
|
1574
|
+
```
|
|
1575
|
+
|
|
1576
|
+
## tsconfig.json
|
|
1577
|
+
|
|
1578
|
+
```json
|
|
1579
|
+
{
|
|
1580
|
+
"extends": "@assistant-ui/x-buildutils/ts/base",
|
|
1581
|
+
"compilerOptions": {
|
|
1582
|
+
"paths": {
|
|
1583
|
+
"@/*": ["./*"]
|
|
1584
|
+
},
|
|
1585
|
+
"allowJs": true,
|
|
1586
|
+
"incremental": true,
|
|
1587
|
+
"jsx": "preserve",
|
|
1588
|
+
"plugins": [
|
|
1589
|
+
{
|
|
1590
|
+
"name": "next"
|
|
1591
|
+
}
|
|
1592
|
+
]
|
|
1593
|
+
},
|
|
1594
|
+
"include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
1595
|
+
"exclude": ["node_modules", "dist"]
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
```
|
|
1599
|
+
|