@assistant-ui/mcp-docs-server 0.1.27 → 0.1.28
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/waterfall.md +1 -1
- package/.docs/organized/code-examples/with-a2a.md +1 -1
- package/.docs/organized/code-examples/with-ag-ui.md +2 -2
- package/.docs/organized/code-examples/with-ai-sdk-v6.md +3 -3
- package/.docs/organized/code-examples/with-artifacts.md +3 -3
- package/.docs/organized/code-examples/with-assistant-transport.md +1 -1
- package/.docs/organized/code-examples/with-chain-of-thought.md +3 -3
- package/.docs/organized/code-examples/with-cloud-standalone.md +3 -3
- package/.docs/organized/code-examples/with-cloud.md +3 -3
- package/.docs/organized/code-examples/with-custom-thread-list.md +3 -3
- package/.docs/organized/code-examples/with-elevenlabs-conversational.md +5 -5
- package/.docs/organized/code-examples/with-elevenlabs-scribe.md +5 -5
- package/.docs/organized/code-examples/with-expo.md +3 -3
- package/.docs/organized/code-examples/with-external-store.md +1 -1
- package/.docs/organized/code-examples/with-ffmpeg.md +3 -3
- package/.docs/organized/code-examples/with-generative-ui.md +841 -0
- package/.docs/organized/code-examples/with-google-adk.md +1 -1
- package/.docs/organized/code-examples/with-heat-graph.md +1 -1
- package/.docs/organized/code-examples/with-interactables.md +3 -3
- package/.docs/organized/code-examples/with-langgraph.md +2 -2
- package/.docs/organized/code-examples/with-livekit.md +4 -4
- package/.docs/organized/code-examples/with-parent-id-grouping.md +2 -2
- package/.docs/organized/code-examples/with-react-hook-form.md +4 -4
- package/.docs/organized/code-examples/with-react-router.md +2 -2
- package/.docs/organized/code-examples/with-store.md +1 -1
- package/.docs/organized/code-examples/with-tanstack.md +2 -2
- package/.docs/organized/code-examples/with-tap-runtime.md +1 -1
- package/.docs/raw/docs/(docs)/guides/mentions.mdx +406 -0
- package/.docs/raw/docs/(docs)/guides/slash-commands.mdx +275 -0
- package/.docs/raw/docs/primitives/composer.mdx +27 -4
- package/package.json +3 -3
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
# Example: with-generative-ui
|
|
2
|
+
|
|
3
|
+
## app/api/chat/route.ts
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { openai } from "@ai-sdk/openai";
|
|
7
|
+
import {
|
|
8
|
+
streamText,
|
|
9
|
+
convertToModelMessages,
|
|
10
|
+
stepCountIs,
|
|
11
|
+
jsonSchema,
|
|
12
|
+
tool,
|
|
13
|
+
zodSchema,
|
|
14
|
+
} from "ai";
|
|
15
|
+
import type { UIMessage } from "ai";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
|
|
18
|
+
export const maxDuration = 30;
|
|
19
|
+
|
|
20
|
+
type ToolDef = { description?: string; parameters: Record<string, unknown> };
|
|
21
|
+
|
|
22
|
+
export async function POST(req: Request) {
|
|
23
|
+
const {
|
|
24
|
+
messages,
|
|
25
|
+
system,
|
|
26
|
+
tools: clientTools,
|
|
27
|
+
}: {
|
|
28
|
+
messages: UIMessage[];
|
|
29
|
+
system?: string;
|
|
30
|
+
tools?: Record<string, ToolDef>;
|
|
31
|
+
} = await req.json();
|
|
32
|
+
|
|
33
|
+
// Convert client-defined frontend tools to AI SDK format
|
|
34
|
+
const frontendToolDefs = clientTools
|
|
35
|
+
? Object.fromEntries(
|
|
36
|
+
Object.entries(clientTools).map(([name, def]) => [
|
|
37
|
+
name,
|
|
38
|
+
{
|
|
39
|
+
description: def.description ?? "",
|
|
40
|
+
inputSchema: jsonSchema(def.parameters),
|
|
41
|
+
},
|
|
42
|
+
]),
|
|
43
|
+
)
|
|
44
|
+
: {};
|
|
45
|
+
|
|
46
|
+
const result = streamText({
|
|
47
|
+
model: openai("gpt-4o"),
|
|
48
|
+
messages: await convertToModelMessages(messages),
|
|
49
|
+
stopWhen: stepCountIs(10),
|
|
50
|
+
...(system ? { system } : {}),
|
|
51
|
+
tools: {
|
|
52
|
+
...frontendToolDefs,
|
|
53
|
+
|
|
54
|
+
// Backend tool: generate chart data
|
|
55
|
+
generate_chart: tool({
|
|
56
|
+
description:
|
|
57
|
+
"Generate a chart. Return structured data for rendering a bar, line, or pie chart. Use this when the user asks for data visualization, charts, graphs, or comparisons.",
|
|
58
|
+
inputSchema: zodSchema(
|
|
59
|
+
z.object({
|
|
60
|
+
title: z.string().describe("Chart title"),
|
|
61
|
+
type: z
|
|
62
|
+
.enum(["bar", "line", "pie"])
|
|
63
|
+
.describe("Chart type to render"),
|
|
64
|
+
data: z
|
|
65
|
+
.array(z.record(z.string(), z.union([z.string(), z.number()])))
|
|
66
|
+
.describe(
|
|
67
|
+
"Array of data objects, e.g. [{month: 'Jan', revenue: 100}]",
|
|
68
|
+
),
|
|
69
|
+
xKey: z
|
|
70
|
+
.string()
|
|
71
|
+
.describe("Key in each data object to use for the x-axis/labels"),
|
|
72
|
+
dataKeys: z
|
|
73
|
+
.array(z.string())
|
|
74
|
+
.describe("Keys in each data object to chart as series/values"),
|
|
75
|
+
}),
|
|
76
|
+
),
|
|
77
|
+
execute: async () => {
|
|
78
|
+
return { success: true };
|
|
79
|
+
},
|
|
80
|
+
}),
|
|
81
|
+
|
|
82
|
+
// Backend tool: show location on map
|
|
83
|
+
show_location: tool({
|
|
84
|
+
description:
|
|
85
|
+
"Show a location on a map. Use this when the user asks about a place, wants to see directions, or needs to see a location.",
|
|
86
|
+
inputSchema: zodSchema(
|
|
87
|
+
z.object({
|
|
88
|
+
name: z.string().describe("Name of the place"),
|
|
89
|
+
address: z.string().optional().describe("Street address"),
|
|
90
|
+
lat: z.number().describe("Latitude"),
|
|
91
|
+
lng: z.number().describe("Longitude"),
|
|
92
|
+
}),
|
|
93
|
+
),
|
|
94
|
+
execute: async () => {
|
|
95
|
+
return { success: true };
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
},
|
|
99
|
+
} as Parameters<typeof streamText>[0]);
|
|
100
|
+
|
|
101
|
+
return result.toUIMessageStreamResponse();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## app/globals.css
|
|
107
|
+
|
|
108
|
+
```css
|
|
109
|
+
@import "tailwindcss";
|
|
110
|
+
@import "tw-animate-css";
|
|
111
|
+
|
|
112
|
+
@source "../../../packages/ui/src";
|
|
113
|
+
|
|
114
|
+
@custom-variant dark (&:is(.dark *));
|
|
115
|
+
|
|
116
|
+
@theme inline {
|
|
117
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
118
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
119
|
+
--radius-lg: var(--radius);
|
|
120
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
121
|
+
--color-background: var(--background);
|
|
122
|
+
--color-foreground: var(--foreground);
|
|
123
|
+
--color-card: var(--card);
|
|
124
|
+
--color-card-foreground: var(--card-foreground);
|
|
125
|
+
--color-popover: var(--popover);
|
|
126
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
127
|
+
--color-primary: var(--primary);
|
|
128
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
129
|
+
--color-secondary: var(--secondary);
|
|
130
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
131
|
+
--color-muted: var(--muted);
|
|
132
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
133
|
+
--color-accent: var(--accent);
|
|
134
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
135
|
+
--color-destructive: var(--destructive);
|
|
136
|
+
--color-border: var(--border);
|
|
137
|
+
--color-input: var(--input);
|
|
138
|
+
--color-ring: var(--ring);
|
|
139
|
+
--color-chart-1: var(--chart-1);
|
|
140
|
+
--color-chart-2: var(--chart-2);
|
|
141
|
+
--color-chart-3: var(--chart-3);
|
|
142
|
+
--color-chart-4: var(--chart-4);
|
|
143
|
+
--color-chart-5: var(--chart-5);
|
|
144
|
+
--color-sidebar: var(--sidebar);
|
|
145
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
146
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
147
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
148
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
149
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
150
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
151
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
:root {
|
|
155
|
+
--radius: 0.625rem;
|
|
156
|
+
--background: oklch(1 0 0);
|
|
157
|
+
--foreground: oklch(0.141 0.005 285.823);
|
|
158
|
+
--card: oklch(1 0 0);
|
|
159
|
+
--card-foreground: oklch(0.141 0.005 285.823);
|
|
160
|
+
--popover: oklch(1 0 0);
|
|
161
|
+
--popover-foreground: oklch(0.141 0.005 285.823);
|
|
162
|
+
--primary: oklch(0.21 0.006 285.885);
|
|
163
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
164
|
+
--secondary: oklch(0.967 0.001 286.375);
|
|
165
|
+
--secondary-foreground: oklch(0.21 0.006 285.885);
|
|
166
|
+
--muted: oklch(0.967 0.001 286.375);
|
|
167
|
+
--muted-foreground: oklch(0.552 0.016 285.938);
|
|
168
|
+
--accent: oklch(0.967 0.001 286.375);
|
|
169
|
+
--accent-foreground: oklch(0.21 0.006 285.885);
|
|
170
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
171
|
+
--border: oklch(0.92 0.004 286.32);
|
|
172
|
+
--input: oklch(0.92 0.004 286.32);
|
|
173
|
+
--ring: oklch(0.705 0.015 286.067);
|
|
174
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
175
|
+
--chart-2: oklch(0.6 0.118 184.704);
|
|
176
|
+
--chart-3: oklch(0.398 0.07 227.392);
|
|
177
|
+
--chart-4: oklch(0.828 0.189 84.429);
|
|
178
|
+
--chart-5: oklch(0.769 0.188 70.08);
|
|
179
|
+
--sidebar: oklch(0.985 0 0);
|
|
180
|
+
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
|
181
|
+
--sidebar-primary: oklch(0.21 0.006 285.885);
|
|
182
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
183
|
+
--sidebar-accent: oklch(0.967 0.001 286.375);
|
|
184
|
+
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
|
185
|
+
--sidebar-border: oklch(0.92 0.004 286.32);
|
|
186
|
+
--sidebar-ring: oklch(0.705 0.015 286.067);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.dark {
|
|
190
|
+
--background: oklch(0.141 0.005 285.823);
|
|
191
|
+
--foreground: oklch(0.985 0 0);
|
|
192
|
+
--card: oklch(0.21 0.006 285.885);
|
|
193
|
+
--card-foreground: oklch(0.985 0 0);
|
|
194
|
+
--popover: oklch(0.21 0.006 285.885);
|
|
195
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
196
|
+
--primary: oklch(0.92 0.004 286.32);
|
|
197
|
+
--primary-foreground: oklch(0.21 0.006 285.885);
|
|
198
|
+
--secondary: oklch(0.274 0.006 286.033);
|
|
199
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
200
|
+
--muted: oklch(0.274 0.006 286.033);
|
|
201
|
+
--muted-foreground: oklch(0.705 0.015 286.067);
|
|
202
|
+
--accent: oklch(0.274 0.006 286.033);
|
|
203
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
204
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
205
|
+
--border: oklch(1 0 0 / 10%);
|
|
206
|
+
--input: oklch(1 0 0 / 15%);
|
|
207
|
+
--ring: oklch(0.552 0.016 285.938);
|
|
208
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
209
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
210
|
+
--chart-3: oklch(0.769 0.188 70.08);
|
|
211
|
+
--chart-4: oklch(0.627 0.265 303.9);
|
|
212
|
+
--chart-5: oklch(0.645 0.246 16.439);
|
|
213
|
+
--sidebar: oklch(0.21 0.006 285.885);
|
|
214
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
215
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
216
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
217
|
+
--sidebar-accent: oklch(0.274 0.006 286.033);
|
|
218
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
219
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
220
|
+
--sidebar-ring: oklch(0.552 0.016 285.938);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
@layer base {
|
|
224
|
+
* {
|
|
225
|
+
@apply border-border outline-ring/50;
|
|
226
|
+
}
|
|
227
|
+
body {
|
|
228
|
+
@apply bg-background text-foreground;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## app/layout.tsx
|
|
235
|
+
|
|
236
|
+
```tsx
|
|
237
|
+
import type { Metadata } from "next";
|
|
238
|
+
import "./globals.css";
|
|
239
|
+
|
|
240
|
+
export const metadata: Metadata = {
|
|
241
|
+
title: "assistant-ui Generative UI Example",
|
|
242
|
+
description:
|
|
243
|
+
"Example showcasing generative UI tool components: charts, date pickers, forms, and maps",
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
export default function RootLayout({
|
|
247
|
+
children,
|
|
248
|
+
}: Readonly<{
|
|
249
|
+
children: React.ReactNode;
|
|
250
|
+
}>) {
|
|
251
|
+
return (
|
|
252
|
+
<html lang="en">
|
|
253
|
+
<body className="h-dvh">{children}</body>
|
|
254
|
+
</html>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## app/page.tsx
|
|
261
|
+
|
|
262
|
+
```tsx
|
|
263
|
+
"use client";
|
|
264
|
+
|
|
265
|
+
import { Thread } from "@/components/assistant-ui/thread";
|
|
266
|
+
import {
|
|
267
|
+
AssistantRuntimeProvider,
|
|
268
|
+
Suggestions,
|
|
269
|
+
useAui,
|
|
270
|
+
useAssistantTool,
|
|
271
|
+
} from "@assistant-ui/react";
|
|
272
|
+
import { useChatRuntime } from "@assistant-ui/react-ai-sdk";
|
|
273
|
+
import { lastAssistantMessageIsCompleteWithToolCalls } from "ai";
|
|
274
|
+
import { z } from "zod";
|
|
275
|
+
import { ChartToolUI } from "@/components/chart-tool-ui";
|
|
276
|
+
import { DatePickerToolUI } from "@/components/date-picker-tool-ui";
|
|
277
|
+
import { ContactFormToolUI } from "@/components/contact-form-tool-ui";
|
|
278
|
+
import { LocationToolUI } from "@/components/location-tool-ui";
|
|
279
|
+
|
|
280
|
+
// Register frontend tool schemas (no execute — resolved via addResult in the UI)
|
|
281
|
+
function FrontendTools() {
|
|
282
|
+
useAssistantTool({
|
|
283
|
+
toolName: "select_date",
|
|
284
|
+
description:
|
|
285
|
+
"Ask the user to select a date. Use this when you need to collect a date (e.g. for scheduling, booking, deadlines).",
|
|
286
|
+
parameters: z.object({
|
|
287
|
+
prompt: z.string().describe("Message to display to the user"),
|
|
288
|
+
minDate: z.string().optional().describe("Minimum date (ISO string)"),
|
|
289
|
+
maxDate: z.string().optional().describe("Maximum date (ISO string)"),
|
|
290
|
+
}),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
useAssistantTool({
|
|
294
|
+
toolName: "collect_contact",
|
|
295
|
+
description:
|
|
296
|
+
"Collect contact information from the user. Use this when you need the user's name, email, or phone number.",
|
|
297
|
+
parameters: z.object({
|
|
298
|
+
prompt: z.string().describe("Message to display to the user"),
|
|
299
|
+
fields: z
|
|
300
|
+
.array(z.enum(["name", "email", "phone"]))
|
|
301
|
+
.describe("Which fields to collect"),
|
|
302
|
+
}),
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export default function Home() {
|
|
309
|
+
const runtime = useChatRuntime({
|
|
310
|
+
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const aui = useAui({
|
|
314
|
+
suggestions: Suggestions([
|
|
315
|
+
{
|
|
316
|
+
title: "Show a bar chart",
|
|
317
|
+
label: "of quarterly revenue",
|
|
318
|
+
prompt:
|
|
319
|
+
"Create a bar chart showing quarterly revenue: Q1 $45k, Q2 $52k, Q3 $61k, Q4 $58k",
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
title: "Pick a date",
|
|
323
|
+
label: "for a meeting",
|
|
324
|
+
prompt: "I need to schedule a meeting. Ask me to pick a date.",
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
title: "Collect my contact info",
|
|
328
|
+
label: "name, email, phone",
|
|
329
|
+
prompt:
|
|
330
|
+
"I want to sign up for the newsletter. Ask for my name, email, and phone number.",
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
title: "Show me on the map",
|
|
334
|
+
label: "the Eiffel Tower",
|
|
335
|
+
prompt: "Show me the Eiffel Tower on a map",
|
|
336
|
+
},
|
|
337
|
+
]),
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
return (
|
|
341
|
+
<AssistantRuntimeProvider aui={aui} runtime={runtime}>
|
|
342
|
+
{/* Frontend tools: register schemas, resolved via addResult in UI */}
|
|
343
|
+
<FrontendTools />
|
|
344
|
+
{/* Tool UIs: render components for each tool call */}
|
|
345
|
+
<ChartToolUI />
|
|
346
|
+
<LocationToolUI />
|
|
347
|
+
<DatePickerToolUI />
|
|
348
|
+
<ContactFormToolUI />
|
|
349
|
+
<main className="h-full">
|
|
350
|
+
<Thread />
|
|
351
|
+
</main>
|
|
352
|
+
</AssistantRuntimeProvider>
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## components.json
|
|
359
|
+
|
|
360
|
+
```json
|
|
361
|
+
{
|
|
362
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
363
|
+
"style": "new-york",
|
|
364
|
+
"rsc": true,
|
|
365
|
+
"tsx": true,
|
|
366
|
+
"tailwind": {
|
|
367
|
+
"config": "",
|
|
368
|
+
"css": "app/globals.css",
|
|
369
|
+
"baseColor": "zinc",
|
|
370
|
+
"cssVariables": true,
|
|
371
|
+
"prefix": ""
|
|
372
|
+
},
|
|
373
|
+
"aliases": {
|
|
374
|
+
"components": "@/components",
|
|
375
|
+
"utils": "@/lib/utils",
|
|
376
|
+
"ui": "@/components/ui",
|
|
377
|
+
"lib": "@/lib",
|
|
378
|
+
"hooks": "@/hooks"
|
|
379
|
+
},
|
|
380
|
+
"iconLibrary": "lucide",
|
|
381
|
+
"registries": {
|
|
382
|
+
"@assistant-ui": "https://r.assistant-ui.com/{name}.json"
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## components/chart-tool-ui.tsx
|
|
389
|
+
|
|
390
|
+
```tsx
|
|
391
|
+
"use client";
|
|
392
|
+
|
|
393
|
+
import { makeAssistantToolUI } from "@assistant-ui/react";
|
|
394
|
+
import {
|
|
395
|
+
BarChart,
|
|
396
|
+
Bar,
|
|
397
|
+
LineChart,
|
|
398
|
+
Line,
|
|
399
|
+
PieChart,
|
|
400
|
+
Pie,
|
|
401
|
+
Cell,
|
|
402
|
+
XAxis,
|
|
403
|
+
YAxis,
|
|
404
|
+
CartesianGrid,
|
|
405
|
+
Tooltip,
|
|
406
|
+
Legend,
|
|
407
|
+
ResponsiveContainer,
|
|
408
|
+
} from "recharts";
|
|
409
|
+
import { Loader2Icon } from "lucide-react";
|
|
410
|
+
|
|
411
|
+
type ChartArgs = {
|
|
412
|
+
title: string;
|
|
413
|
+
type: "bar" | "line" | "pie";
|
|
414
|
+
data: Array<Record<string, string | number>>;
|
|
415
|
+
xKey: string;
|
|
416
|
+
dataKeys: string[];
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
type ChartResult = {
|
|
420
|
+
success: boolean;
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const COLORS = [
|
|
424
|
+
"oklch(0.646 0.222 41.116)",
|
|
425
|
+
"oklch(0.6 0.118 184.704)",
|
|
426
|
+
"oklch(0.398 0.07 227.392)",
|
|
427
|
+
"oklch(0.828 0.189 84.429)",
|
|
428
|
+
"oklch(0.769 0.188 70.08)",
|
|
429
|
+
];
|
|
430
|
+
|
|
431
|
+
export const ChartToolUI = makeAssistantToolUI<ChartArgs, ChartResult>({
|
|
432
|
+
toolName: "generate_chart",
|
|
433
|
+
render: function ChartUI({ args, status }) {
|
|
434
|
+
if (status.type === "running" && !args.data?.length) {
|
|
435
|
+
return (
|
|
436
|
+
<div className="flex items-center gap-2 rounded-lg border p-4">
|
|
437
|
+
<Loader2Icon className="size-4 animate-spin text-muted-foreground" />
|
|
438
|
+
<span className="text-muted-foreground text-sm">
|
|
439
|
+
Generating chart...
|
|
440
|
+
</span>
|
|
441
|
+
</div>
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const { title, type, data, xKey, dataKeys } = args;
|
|
446
|
+
if (!data?.length || !dataKeys?.length) return null;
|
|
447
|
+
|
|
448
|
+
return (
|
|
449
|
+
<div className="my-2 rounded-lg border p-4">
|
|
450
|
+
<h3 className="mb-3 text-center font-semibold text-sm">{title}</h3>
|
|
451
|
+
<div className="h-[280px] w-full">
|
|
452
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
453
|
+
{type === "bar" ? (
|
|
454
|
+
<BarChart data={data}>
|
|
455
|
+
<CartesianGrid strokeDasharray="3 3" vertical={false} />
|
|
456
|
+
<XAxis
|
|
457
|
+
dataKey={xKey}
|
|
458
|
+
tickLine={false}
|
|
459
|
+
axisLine={false}
|
|
460
|
+
fontSize={12}
|
|
461
|
+
/>
|
|
462
|
+
<YAxis tickLine={false} axisLine={false} fontSize={12} />
|
|
463
|
+
<Tooltip />
|
|
464
|
+
<Legend />
|
|
465
|
+
{dataKeys.map((key, i) => (
|
|
466
|
+
<Bar
|
|
467
|
+
key={key}
|
|
468
|
+
dataKey={key}
|
|
469
|
+
fill={COLORS[i % COLORS.length]}
|
|
470
|
+
radius={[4, 4, 0, 0]}
|
|
471
|
+
/>
|
|
472
|
+
))}
|
|
473
|
+
</BarChart>
|
|
474
|
+
) : type === "line" ? (
|
|
475
|
+
<LineChart data={data}>
|
|
476
|
+
<CartesianGrid strokeDasharray="3 3" vertical={false} />
|
|
477
|
+
<XAxis
|
|
478
|
+
dataKey={xKey}
|
|
479
|
+
tickLine={false}
|
|
480
|
+
axisLine={false}
|
|
481
|
+
fontSize={12}
|
|
482
|
+
/>
|
|
483
|
+
<YAxis tickLine={false} axisLine={false} fontSize={12} />
|
|
484
|
+
<Tooltip />
|
|
485
|
+
<Legend />
|
|
486
|
+
{dataKeys.map((key, i) => (
|
|
487
|
+
<Line
|
|
488
|
+
key={key}
|
|
489
|
+
type="monotone"
|
|
490
|
+
dataKey={key}
|
|
491
|
+
stroke={COLORS[i % COLORS.length] ?? "#8884d8"}
|
|
492
|
+
strokeWidth={2}
|
|
493
|
+
dot={false}
|
|
494
|
+
/>
|
|
495
|
+
))}
|
|
496
|
+
</LineChart>
|
|
497
|
+
) : (
|
|
498
|
+
<PieChart>
|
|
499
|
+
<Tooltip />
|
|
500
|
+
<Legend />
|
|
501
|
+
<Pie
|
|
502
|
+
data={data}
|
|
503
|
+
dataKey={dataKeys[0]!}
|
|
504
|
+
nameKey={xKey}
|
|
505
|
+
cx="50%"
|
|
506
|
+
cy="50%"
|
|
507
|
+
outerRadius={80}
|
|
508
|
+
label
|
|
509
|
+
>
|
|
510
|
+
{data.map((_, i) => (
|
|
511
|
+
<Cell
|
|
512
|
+
key={i}
|
|
513
|
+
fill={COLORS[i % COLORS.length] ?? "#8884d8"}
|
|
514
|
+
/>
|
|
515
|
+
))}
|
|
516
|
+
</Pie>
|
|
517
|
+
</PieChart>
|
|
518
|
+
)}
|
|
519
|
+
</ResponsiveContainer>
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
);
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
## components/contact-form-tool-ui.tsx
|
|
529
|
+
|
|
530
|
+
```tsx
|
|
531
|
+
"use client";
|
|
532
|
+
|
|
533
|
+
import { useState } from "react";
|
|
534
|
+
import { makeAssistantToolUI } from "@assistant-ui/react";
|
|
535
|
+
import { UserIcon, CheckCircle2Icon } from "lucide-react";
|
|
536
|
+
|
|
537
|
+
type ContactFormArgs = {
|
|
538
|
+
prompt: string;
|
|
539
|
+
fields: Array<"name" | "email" | "phone">;
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
type ContactFormResult = {
|
|
543
|
+
name?: string;
|
|
544
|
+
email?: string;
|
|
545
|
+
phone?: string;
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
export const ContactFormToolUI = makeAssistantToolUI<
|
|
549
|
+
ContactFormArgs,
|
|
550
|
+
ContactFormResult
|
|
551
|
+
>({
|
|
552
|
+
toolName: "collect_contact",
|
|
553
|
+
render: function ContactFormUI({ args, result, addResult }) {
|
|
554
|
+
const [form, setForm] = useState<ContactFormResult>({});
|
|
555
|
+
|
|
556
|
+
if (result) {
|
|
557
|
+
return (
|
|
558
|
+
<div className="my-2 rounded-lg border border-green-200 bg-green-50 p-3">
|
|
559
|
+
<div className="mb-1 flex items-center gap-2">
|
|
560
|
+
<CheckCircle2Icon className="size-4 text-green-600" />
|
|
561
|
+
<span className="font-medium text-green-800 text-sm">
|
|
562
|
+
Contact info collected
|
|
563
|
+
</span>
|
|
564
|
+
</div>
|
|
565
|
+
<div className="ml-6 space-y-0.5 text-green-700 text-xs">
|
|
566
|
+
{result.name && <p>Name: {result.name}</p>}
|
|
567
|
+
{result.email && <p>Email: {result.email}</p>}
|
|
568
|
+
{result.phone && <p>Phone: {result.phone}</p>}
|
|
569
|
+
</div>
|
|
570
|
+
</div>
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const fields = args.fields ?? ["name", "email", "phone"];
|
|
575
|
+
const isValid = fields.every((f) => form[f]?.trim());
|
|
576
|
+
|
|
577
|
+
return (
|
|
578
|
+
<div className="my-2 rounded-lg border p-4">
|
|
579
|
+
<div className="mb-3 flex items-center gap-2">
|
|
580
|
+
<UserIcon className="size-4 text-muted-foreground" />
|
|
581
|
+
<span className="font-medium text-sm">{args.prompt}</span>
|
|
582
|
+
</div>
|
|
583
|
+
<div className="space-y-2">
|
|
584
|
+
{fields.includes("name") && (
|
|
585
|
+
<input
|
|
586
|
+
type="text"
|
|
587
|
+
placeholder="Name"
|
|
588
|
+
value={form.name ?? ""}
|
|
589
|
+
onChange={(e) => setForm((p) => ({ ...p, name: e.target.value }))}
|
|
590
|
+
className="w-full rounded-md border bg-background px-3 py-2 text-sm"
|
|
591
|
+
/>
|
|
592
|
+
)}
|
|
593
|
+
{fields.includes("email") && (
|
|
594
|
+
<input
|
|
595
|
+
type="email"
|
|
596
|
+
placeholder="Email"
|
|
597
|
+
value={form.email ?? ""}
|
|
598
|
+
onChange={(e) =>
|
|
599
|
+
setForm((p) => ({ ...p, email: e.target.value }))
|
|
600
|
+
}
|
|
601
|
+
className="w-full rounded-md border bg-background px-3 py-2 text-sm"
|
|
602
|
+
/>
|
|
603
|
+
)}
|
|
604
|
+
{fields.includes("phone") && (
|
|
605
|
+
<input
|
|
606
|
+
type="tel"
|
|
607
|
+
placeholder="Phone"
|
|
608
|
+
value={form.phone ?? ""}
|
|
609
|
+
onChange={(e) =>
|
|
610
|
+
setForm((p) => ({ ...p, phone: e.target.value }))
|
|
611
|
+
}
|
|
612
|
+
className="w-full rounded-md border bg-background px-3 py-2 text-sm"
|
|
613
|
+
/>
|
|
614
|
+
)}
|
|
615
|
+
<button
|
|
616
|
+
type="button"
|
|
617
|
+
disabled={!isValid}
|
|
618
|
+
onClick={() => addResult(form)}
|
|
619
|
+
className="w-full rounded-md bg-primary px-4 py-2 font-medium text-primary-foreground text-sm disabled:opacity-50"
|
|
620
|
+
>
|
|
621
|
+
Submit
|
|
622
|
+
</button>
|
|
623
|
+
</div>
|
|
624
|
+
</div>
|
|
625
|
+
);
|
|
626
|
+
},
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
## components/date-picker-tool-ui.tsx
|
|
632
|
+
|
|
633
|
+
```tsx
|
|
634
|
+
"use client";
|
|
635
|
+
|
|
636
|
+
import { useState } from "react";
|
|
637
|
+
import { makeAssistantToolUI } from "@assistant-ui/react";
|
|
638
|
+
import { CalendarIcon, CheckCircle2Icon } from "lucide-react";
|
|
639
|
+
|
|
640
|
+
type DatePickerArgs = {
|
|
641
|
+
prompt: string;
|
|
642
|
+
minDate?: string;
|
|
643
|
+
maxDate?: string;
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
type DatePickerResult = {
|
|
647
|
+
date: string;
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
export const DatePickerToolUI = makeAssistantToolUI<
|
|
651
|
+
DatePickerArgs,
|
|
652
|
+
DatePickerResult
|
|
653
|
+
>({
|
|
654
|
+
toolName: "select_date",
|
|
655
|
+
render: function DatePickerUI({ args, result, addResult }) {
|
|
656
|
+
const [value, setValue] = useState("");
|
|
657
|
+
|
|
658
|
+
if (result) {
|
|
659
|
+
return (
|
|
660
|
+
<div className="my-2 flex items-center gap-2 rounded-lg border border-green-200 bg-green-50 p-3">
|
|
661
|
+
<CheckCircle2Icon className="size-4 text-green-600" />
|
|
662
|
+
<span className="text-green-800 text-sm">
|
|
663
|
+
Selected: {new Date(result.date).toLocaleDateString()}
|
|
664
|
+
</span>
|
|
665
|
+
</div>
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return (
|
|
670
|
+
<div className="my-2 rounded-lg border p-4">
|
|
671
|
+
<div className="mb-3 flex items-center gap-2">
|
|
672
|
+
<CalendarIcon className="size-4 text-muted-foreground" />
|
|
673
|
+
<span className="font-medium text-sm">{args.prompt}</span>
|
|
674
|
+
</div>
|
|
675
|
+
<div className="flex items-center gap-2">
|
|
676
|
+
<input
|
|
677
|
+
type="date"
|
|
678
|
+
value={value}
|
|
679
|
+
min={args.minDate}
|
|
680
|
+
max={args.maxDate}
|
|
681
|
+
onChange={(e) => setValue(e.target.value)}
|
|
682
|
+
className="rounded-md border bg-background px-3 py-2 text-sm"
|
|
683
|
+
/>
|
|
684
|
+
<button
|
|
685
|
+
type="button"
|
|
686
|
+
disabled={!value}
|
|
687
|
+
onClick={() => addResult({ date: new Date(value).toISOString() })}
|
|
688
|
+
className="rounded-md bg-primary px-4 py-2 font-medium text-primary-foreground text-sm disabled:opacity-50"
|
|
689
|
+
>
|
|
690
|
+
Confirm
|
|
691
|
+
</button>
|
|
692
|
+
</div>
|
|
693
|
+
</div>
|
|
694
|
+
);
|
|
695
|
+
},
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
## components/location-tool-ui.tsx
|
|
701
|
+
|
|
702
|
+
```tsx
|
|
703
|
+
"use client";
|
|
704
|
+
|
|
705
|
+
import { makeAssistantToolUI } from "@assistant-ui/react";
|
|
706
|
+
import { MapPinIcon, Loader2Icon } from "lucide-react";
|
|
707
|
+
|
|
708
|
+
type LocationArgs = {
|
|
709
|
+
name: string;
|
|
710
|
+
address?: string;
|
|
711
|
+
lat: number;
|
|
712
|
+
lng: number;
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
type LocationResult = {
|
|
716
|
+
success: boolean;
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
export const LocationToolUI = makeAssistantToolUI<LocationArgs, LocationResult>(
|
|
720
|
+
{
|
|
721
|
+
toolName: "show_location",
|
|
722
|
+
render: function LocationUI({ args, status }) {
|
|
723
|
+
if (status.type === "running" && !args.lat) {
|
|
724
|
+
return (
|
|
725
|
+
<div className="flex items-center gap-2 rounded-lg border p-4">
|
|
726
|
+
<Loader2Icon className="size-4 animate-spin text-muted-foreground" />
|
|
727
|
+
<span className="text-muted-foreground text-sm">
|
|
728
|
+
Loading location...
|
|
729
|
+
</span>
|
|
730
|
+
</div>
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const { name, address, lat, lng } = args;
|
|
735
|
+
if (!lat || !lng) return null;
|
|
736
|
+
|
|
737
|
+
const mapSrc = `https://www.openstreetmap.org/export/embed.html?bbox=${lng - 0.01},${lat - 0.01},${lng + 0.01},${lat + 0.01}&layer=mapnik&marker=${lat},${lng}`;
|
|
738
|
+
|
|
739
|
+
return (
|
|
740
|
+
<div className="my-2 overflow-hidden rounded-lg border">
|
|
741
|
+
<div className="flex items-center gap-2 border-b px-3 py-2">
|
|
742
|
+
<MapPinIcon className="size-4 text-muted-foreground" />
|
|
743
|
+
<div>
|
|
744
|
+
<p className="font-medium text-sm">{name}</p>
|
|
745
|
+
{address && (
|
|
746
|
+
<p className="text-muted-foreground text-xs">{address}</p>
|
|
747
|
+
)}
|
|
748
|
+
</div>
|
|
749
|
+
</div>
|
|
750
|
+
<iframe
|
|
751
|
+
title={`Map of ${name}`}
|
|
752
|
+
src={mapSrc}
|
|
753
|
+
className="h-[200px] w-full border-0"
|
|
754
|
+
/>
|
|
755
|
+
</div>
|
|
756
|
+
);
|
|
757
|
+
},
|
|
758
|
+
},
|
|
759
|
+
);
|
|
760
|
+
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
## next.config.ts
|
|
764
|
+
|
|
765
|
+
```typescript
|
|
766
|
+
import type { NextConfig } from "next";
|
|
767
|
+
|
|
768
|
+
const nextConfig: NextConfig = {
|
|
769
|
+
/* config options here */
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
export default nextConfig;
|
|
773
|
+
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
## package.json
|
|
777
|
+
|
|
778
|
+
```json
|
|
779
|
+
{
|
|
780
|
+
"name": "with-generative-ui",
|
|
781
|
+
"version": "0.0.0",
|
|
782
|
+
"private": true,
|
|
783
|
+
"type": "module",
|
|
784
|
+
"scripts": {
|
|
785
|
+
"dev": "next dev",
|
|
786
|
+
"build": "next build",
|
|
787
|
+
"start": "next start"
|
|
788
|
+
},
|
|
789
|
+
"dependencies": {
|
|
790
|
+
"@ai-sdk/openai": "^3.0.51",
|
|
791
|
+
"@assistant-ui/react": "workspace:*",
|
|
792
|
+
"@assistant-ui/react-ai-sdk": "workspace:*",
|
|
793
|
+
"@assistant-ui/ui": "workspace:*",
|
|
794
|
+
"ai": "^6.0.148",
|
|
795
|
+
"class-variance-authority": "^0.7.1",
|
|
796
|
+
"clsx": "^2.1.1",
|
|
797
|
+
"lucide-react": "^1.7.0",
|
|
798
|
+
"next": "^16.2.2",
|
|
799
|
+
"react": "^19.2.4",
|
|
800
|
+
"react-dom": "^19.2.4",
|
|
801
|
+
"recharts": "^3.8.1",
|
|
802
|
+
"tailwind-merge": "^3.5.0",
|
|
803
|
+
"zod": "^4.3.6"
|
|
804
|
+
},
|
|
805
|
+
"devDependencies": {
|
|
806
|
+
"@assistant-ui/x-buildutils": "workspace:*",
|
|
807
|
+
"@tailwindcss/postcss": "^4.2.2",
|
|
808
|
+
"@types/node": "^25.5.2",
|
|
809
|
+
"@types/react": "^19.2.14",
|
|
810
|
+
"@types/react-dom": "^19.2.3",
|
|
811
|
+
"postcss": "^8.5.8",
|
|
812
|
+
"tailwindcss": "^4.2.2",
|
|
813
|
+
"tw-animate-css": "^1.4.0",
|
|
814
|
+
"typescript": "5.9.3"
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
## tsconfig.json
|
|
821
|
+
|
|
822
|
+
```json
|
|
823
|
+
{
|
|
824
|
+
"extends": "@assistant-ui/x-buildutils/ts/next",
|
|
825
|
+
"compilerOptions": {
|
|
826
|
+
"paths": {
|
|
827
|
+
"@/*": ["./*"],
|
|
828
|
+
"@/components/assistant-ui/*": [
|
|
829
|
+
"../../packages/ui/src/components/assistant-ui/*"
|
|
830
|
+
],
|
|
831
|
+
"@/components/ui/*": ["../../packages/ui/src/components/ui/*"],
|
|
832
|
+
"@/lib/utils": ["../../packages/ui/src/lib/utils"],
|
|
833
|
+
"@assistant-ui/ui/*": ["../../packages/ui/src/*"]
|
|
834
|
+
}
|
|
835
|
+
},
|
|
836
|
+
"include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
837
|
+
"exclude": ["node_modules"]
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
```
|
|
841
|
+
|