@assistant-ui/mcp-docs-server 0.1.32 → 0.1.34
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 +20 -22
- package/.docs/organized/code-examples/with-a2a.md +24 -24
- package/.docs/organized/code-examples/with-ag-ui.md +30 -25
- package/.docs/organized/code-examples/with-ai-sdk-v6.md +11 -9
- package/.docs/organized/code-examples/with-artifacts.md +49 -37
- package/.docs/organized/code-examples/with-assistant-transport.md +63 -51
- package/.docs/organized/code-examples/with-browser-extension.md +22 -10
- package/.docs/organized/code-examples/with-chain-of-thought.md +406 -87
- package/.docs/organized/code-examples/with-cloud-standalone.md +25 -24
- package/.docs/organized/code-examples/with-cloud.md +11 -9
- package/.docs/organized/code-examples/with-custom-thread-list.md +14 -12
- package/.docs/organized/code-examples/with-elevenlabs-conversational.md +23 -21
- package/.docs/organized/code-examples/with-elevenlabs-scribe.md +14 -13
- package/.docs/organized/code-examples/with-expo.md +44 -29
- package/.docs/organized/code-examples/with-external-store.md +9 -7
- package/.docs/organized/code-examples/with-ffmpeg.md +322 -285
- package/.docs/organized/code-examples/with-generative-ui.md +1066 -256
- package/.docs/organized/code-examples/with-google-adk.md +10 -8
- package/.docs/organized/code-examples/with-heat-graph.md +13 -11
- package/.docs/organized/code-examples/with-image-generation.md +19 -17
- package/.docs/organized/code-examples/with-interactables.md +317 -239
- package/.docs/organized/code-examples/with-langchain.md +14 -12
- package/.docs/organized/code-examples/with-langgraph.md +91 -83
- package/.docs/organized/code-examples/with-livekit.md +24 -22
- package/.docs/organized/code-examples/with-mcp.md +18 -12
- package/.docs/organized/code-examples/with-opencode.md +66 -66
- package/.docs/organized/code-examples/with-react-hook-form.md +19 -17
- package/.docs/organized/code-examples/with-react-ink.md +297 -102
- package/.docs/organized/code-examples/with-react-router.md +17 -15
- package/.docs/organized/code-examples/with-resumable-stream.md +15 -14
- package/.docs/organized/code-examples/with-store.md +78 -76
- package/.docs/organized/code-examples/with-tanstack.md +13 -14
- package/.docs/organized/code-examples/with-tap-runtime.md +26 -24
- package/.docs/raw/docs/(docs)/architecture.mdx +94 -42
- package/.docs/raw/docs/(docs)/cli.mdx +1 -2
- package/.docs/raw/docs/(docs)/installation.mdx +1 -1
- package/.docs/raw/docs/(reference)/api-reference/adapters/persistence.mdx +5 -1
- package/.docs/raw/docs/(reference)/api-reference/external-store/message-conversion.mdx +15 -15
- package/.docs/raw/docs/(reference)/api-reference/external-store/runtime.mdx +14 -1
- package/.docs/raw/docs/(reference)/api-reference/generative-ui/index.mdx +18 -0
- package/.docs/raw/docs/(reference)/api-reference/generative-ui/rendering.mdx +33 -0
- package/.docs/raw/docs/(reference)/api-reference/generative-ui/spec.mdx +45 -0
- package/.docs/raw/docs/(reference)/api-reference/hooks/composer-triggers.mdx +41 -41
- package/.docs/raw/docs/(reference)/api-reference/hooks/model-context.mdx +19 -1
- package/.docs/raw/docs/(reference)/api-reference/hooks/primitives.mdx +122 -122
- package/.docs/raw/docs/(reference)/api-reference/integrations/react-ai-sdk.mdx +43 -2
- package/.docs/raw/docs/(reference)/api-reference/model-context/context.mdx +6 -0
- package/.docs/raw/docs/(reference)/api-reference/overview.mdx +4 -6
- package/.docs/raw/docs/(reference)/api-reference/primitives/message.mdx +30 -0
- package/.docs/raw/docs/(reference)/api-reference/tools/component-tools.mdx +20 -3
- package/.docs/raw/docs/(reference)/api-reference/tools/index.mdx +8 -8
- package/.docs/raw/docs/(reference)/api-reference/tools/rendering.mdx +52 -4
- package/.docs/raw/docs/(reference)/api-reference/tools/toolkits.mdx +100 -10
- package/.docs/raw/docs/(reference)/api-reference/utilities/miscellaneous.mdx +7 -59
- package/.docs/raw/docs/cloud/ai-sdk.mdx +0 -2
- package/.docs/raw/docs/{(docs)/copilots → copilots}/model-context.mdx +34 -26
- package/.docs/raw/docs/{(docs)/copilots → copilots}/motivation.mdx +32 -26
- package/.docs/raw/docs/guides/chain-of-thought.mdx +7 -9
- package/.docs/raw/docs/guides/context-api.mdx +2 -1
- package/.docs/raw/docs/guides/index.mdx +3 -12
- package/.docs/raw/docs/guides/mentions.mdx +4 -4
- package/.docs/raw/docs/guides/slash-commands.mdx +1 -1
- package/.docs/raw/docs/guides/suggestions.mdx +1 -1
- package/.docs/raw/docs/ink/adapters.mdx +23 -1
- package/.docs/raw/docs/ink/hooks.mdx +101 -85
- package/.docs/raw/docs/ink/migration.mdx +1 -1
- package/.docs/raw/docs/ink/primitives.mdx +2 -2
- package/.docs/raw/docs/integrations/frameworks/ai-sdk.mdx +1 -1
- package/.docs/raw/docs/integrations/index.mdx +2 -2
- package/.docs/raw/docs/integrations/persistence/custom-adapter.mdx +17 -2
- package/.docs/raw/docs/migrations/toolkit-tools.mdx +232 -0
- package/.docs/raw/docs/primitives/chain-of-thought.mdx +10 -16
- package/.docs/raw/docs/primitives/message.mdx +9 -10
- package/.docs/raw/docs/react-native/hooks.mdx +62 -79
- package/.docs/raw/docs/react-native/migration.mdx +1 -1
- package/.docs/raw/docs/react-native/primitives.mdx +2 -2
- package/.docs/raw/docs/runtimes/a2a/client-and-hooks.mdx +3 -0
- package/.docs/raw/docs/runtimes/ag-ui/runtime-options.mdx +41 -0
- package/.docs/raw/docs/runtimes/ai-sdk/v6.mdx +122 -1
- package/.docs/raw/docs/runtimes/concepts/architecture.mdx +46 -4
- package/.docs/raw/docs/runtimes/concepts/threads.mdx +7 -1
- package/.docs/raw/docs/runtimes/custom/assistant-transport.mdx +54 -12
- package/.docs/raw/docs/runtimes/custom/external-store.mdx +62 -4
- package/.docs/raw/docs/runtimes/custom/local-runtime.mdx +30 -7
- package/.docs/raw/docs/runtimes/google-adk/hooks.mdx +108 -38
- package/.docs/raw/docs/runtimes/langgraph/generative-ui.mdx +2 -2
- package/.docs/raw/docs/runtimes/langgraph/streaming.mdx +14 -1
- package/.docs/raw/docs/runtimes/langgraph/tutorial/part-2.mdx +64 -50
- package/.docs/raw/docs/runtimes/langgraph/tutorial/part-3.mdx +98 -86
- package/.docs/raw/docs/tools/backend.mdx +144 -0
- package/.docs/raw/docs/tools/defining-tools.mdx +538 -0
- package/.docs/raw/docs/tools/dynamic-tools.mdx +110 -0
- package/.docs/raw/docs/tools/generative-ui.mdx +214 -0
- package/.docs/raw/docs/tools/index.mdx +71 -0
- package/.docs/raw/docs/{guides → tools}/interactables.mdx +1 -1
- package/.docs/raw/docs/{integrations/tools → tools}/mcp.mdx +145 -50
- package/.docs/raw/docs/{guides → tools}/multi-agent.mdx +15 -17
- package/.docs/raw/docs/tools/tool-ui.mdx +967 -0
- package/.docs/raw/docs/{integrations/tools/react-mcp.mdx → tools/user-managed-mcp.mdx} +7 -7
- package/.docs/raw/docs/ui/directive-text.mdx +3 -3
- package/.docs/raw/docs/ui/mcp-config.mdx +4 -4
- package/.docs/raw/docs/ui/mermaid.mdx +16 -9
- package/.docs/raw/docs/ui/part-grouping.mdx +84 -50
- package/.docs/raw/docs/ui/reasoning.mdx +4 -5
- package/.docs/raw/docs/ui/tool-fallback.mdx +16 -0
- package/.docs/raw/docs/ui/tool-group.mdx +5 -6
- package/.docs/raw/docs/utilities/heat-graph.mdx +1 -1
- package/.docs/raw/docs/utilities/react-o11y.mdx +2 -2
- package/dist/constants.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/prepare-docs/code-examples.d.ts.map +1 -1
- package/dist/prepare-docs/code-examples.js.map +1 -1
- package/dist/prepare-docs/copy-raw.d.ts.map +1 -1
- package/dist/prepare-docs/copy-raw.js.map +1 -1
- package/dist/prepare-docs/prepare.js.map +1 -1
- package/dist/stdio.js.map +1 -1
- package/dist/tools/docs.js.map +1 -1
- package/dist/tools/examples.js.map +1 -1
- package/dist/tools/tests/test-setup.js.map +1 -1
- package/dist/utils/mdx.js.map +1 -1
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js.map +1 -1
- package/package.json +5 -5
- package/.docs/organized/code-examples/with-parent-id-grouping.md +0 -596
- package/.docs/raw/docs/(docs)/copilots/make-assistant-tool-ui.mdx +0 -151
- package/.docs/raw/docs/(docs)/copilots/make-assistant-tool.mdx +0 -230
- package/.docs/raw/docs/guides/generative-ui.mdx +0 -142
- package/.docs/raw/docs/guides/tool-ui.mdx +0 -858
- package/.docs/raw/docs/guides/tools.mdx +0 -736
- /package/.docs/raw/docs/{(docs)/copilots → copilots}/assistant-frame.mdx +0 -0
- /package/.docs/raw/docs/{(docs)/copilots → copilots}/make-assistant-visible.mdx +0 -0
- /package/.docs/raw/docs/{(docs)/copilots → copilots}/use-assistant-instructions.mdx +0 -0
- /package/.docs/raw/docs/{guides → tools}/mcp-apps.mdx +0 -0
|
@@ -8,16 +8,26 @@ import {
|
|
|
8
8
|
streamText,
|
|
9
9
|
convertToModelMessages,
|
|
10
10
|
stepCountIs,
|
|
11
|
-
jsonSchema,
|
|
12
11
|
tool,
|
|
13
12
|
zodSchema,
|
|
14
13
|
} from "ai";
|
|
15
14
|
import type { UIMessage } from "ai";
|
|
16
15
|
import { z } from "zod";
|
|
16
|
+
import {
|
|
17
|
+
AISDKToolkit,
|
|
18
|
+
type AISDKToolkitToolsOptions,
|
|
19
|
+
} from "@assistant-ui/react-ai-sdk";
|
|
20
|
+
import {
|
|
21
|
+
renderGuiToolDescription,
|
|
22
|
+
renderGuiToolInputSchema,
|
|
23
|
+
} from "../../../lib/render-gui-tool";
|
|
24
|
+
import toolkit from "../../toolkit";
|
|
17
25
|
|
|
18
26
|
export const maxDuration = 30;
|
|
19
27
|
|
|
20
|
-
|
|
28
|
+
const aiToolkit = new AISDKToolkit({ toolkit });
|
|
29
|
+
|
|
30
|
+
type FrontendToolDefs = NonNullable<AISDKToolkitToolsOptions["frontend"]>;
|
|
21
31
|
|
|
22
32
|
export async function POST(req: Request) {
|
|
23
33
|
const {
|
|
@@ -27,21 +37,12 @@ export async function POST(req: Request) {
|
|
|
27
37
|
}: {
|
|
28
38
|
messages: UIMessage[];
|
|
29
39
|
system?: string;
|
|
30
|
-
tools?:
|
|
40
|
+
tools?: FrontendToolDefs;
|
|
31
41
|
} = await req.json();
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
Object.entries(clientTools).map(([name, def]) => [
|
|
37
|
-
name,
|
|
38
|
-
{
|
|
39
|
-
description: def.description ?? "",
|
|
40
|
-
inputSchema: jsonSchema(def.parameters),
|
|
41
|
-
},
|
|
42
|
-
]),
|
|
43
|
-
)
|
|
44
|
-
: {};
|
|
43
|
+
const toolkitTools = await aiToolkit.tools({
|
|
44
|
+
...(clientTools && { frontend: clientTools }),
|
|
45
|
+
});
|
|
45
46
|
|
|
46
47
|
const result = streamText({
|
|
47
48
|
model: openai("gpt-5.4-nano"),
|
|
@@ -49,7 +50,15 @@ export async function POST(req: Request) {
|
|
|
49
50
|
stopWhen: stepCountIs(10),
|
|
50
51
|
...(system ? { system } : {}),
|
|
51
52
|
tools: {
|
|
52
|
-
...
|
|
53
|
+
...toolkitTools,
|
|
54
|
+
|
|
55
|
+
render_gui: tool({
|
|
56
|
+
description: renderGuiToolDescription,
|
|
57
|
+
inputSchema: zodSchema(renderGuiToolInputSchema),
|
|
58
|
+
execute: async (input) => ({
|
|
59
|
+
spec: input.spec,
|
|
60
|
+
}),
|
|
61
|
+
}),
|
|
53
62
|
|
|
54
63
|
// Backend tool: generate chart data
|
|
55
64
|
generate_chart: tool({
|
|
@@ -231,6 +240,65 @@ export async function POST(req: Request) {
|
|
|
231
240
|
|
|
232
241
|
```
|
|
233
242
|
|
|
243
|
+
## app/gui-chat/page.tsx
|
|
244
|
+
|
|
245
|
+
```tsx
|
|
246
|
+
"use client";
|
|
247
|
+
|
|
248
|
+
import { ExampleNav } from "@/components/example-nav";
|
|
249
|
+
import { GuiThread } from "@/components/gui-thread";
|
|
250
|
+
import {
|
|
251
|
+
AssistantRuntimeProvider,
|
|
252
|
+
Suggestions,
|
|
253
|
+
useAssistantInstructions,
|
|
254
|
+
useAui,
|
|
255
|
+
} from "@assistant-ui/react";
|
|
256
|
+
import { useChatRuntime } from "@assistant-ui/react-ai-sdk";
|
|
257
|
+
import { lastAssistantMessageIsCompleteWithToolCalls } from "ai";
|
|
258
|
+
import { renderGuiChatInstructions } from "@/lib/render-gui-tool";
|
|
259
|
+
|
|
260
|
+
const GuiChatInstructions = () => {
|
|
261
|
+
useAssistantInstructions(renderGuiChatInstructions);
|
|
262
|
+
return null;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
export default function GuiChatPage() {
|
|
266
|
+
const runtime = useChatRuntime({
|
|
267
|
+
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const aui = useAui({
|
|
271
|
+
suggestions: Suggestions([
|
|
272
|
+
{
|
|
273
|
+
title: "Welcome card",
|
|
274
|
+
label: "with a Get started button",
|
|
275
|
+
prompt:
|
|
276
|
+
"Show me a welcome card with a Get started button using render_gui.",
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
title: "Stats dashboard",
|
|
280
|
+
label: "revenue and users",
|
|
281
|
+
prompt:
|
|
282
|
+
"Use render_gui to show a stats card with Revenue $124k and Active Users 8.2k.",
|
|
283
|
+
},
|
|
284
|
+
]),
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
return (
|
|
288
|
+
<AssistantRuntimeProvider aui={aui} runtime={runtime}>
|
|
289
|
+
<GuiChatInstructions />
|
|
290
|
+
<div className="flex h-full flex-col">
|
|
291
|
+
<ExampleNav />
|
|
292
|
+
<main className="min-h-0 flex-1">
|
|
293
|
+
<GuiThread />
|
|
294
|
+
</main>
|
|
295
|
+
</div>
|
|
296
|
+
</AssistantRuntimeProvider>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
```
|
|
301
|
+
|
|
234
302
|
## app/layout.tsx
|
|
235
303
|
|
|
236
304
|
```tsx
|
|
@@ -266,44 +334,13 @@ import { Thread } from "@/components/assistant-ui/thread";
|
|
|
266
334
|
import {
|
|
267
335
|
AssistantRuntimeProvider,
|
|
268
336
|
Suggestions,
|
|
337
|
+
Tools,
|
|
269
338
|
useAui,
|
|
270
|
-
useAssistantTool,
|
|
271
339
|
} from "@assistant-ui/react";
|
|
272
340
|
import { useChatRuntime } from "@assistant-ui/react-ai-sdk";
|
|
273
341
|
import { lastAssistantMessageIsCompleteWithToolCalls } from "ai";
|
|
274
|
-
import {
|
|
275
|
-
import
|
|
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
|
-
}
|
|
342
|
+
import { ExampleNav } from "@/components/example-nav";
|
|
343
|
+
import toolkit from "./toolkit";
|
|
307
344
|
|
|
308
345
|
export default function Home() {
|
|
309
346
|
const runtime = useChatRuntime({
|
|
@@ -311,6 +348,7 @@ export default function Home() {
|
|
|
311
348
|
});
|
|
312
349
|
|
|
313
350
|
const aui = useAui({
|
|
351
|
+
tools: Tools({ toolkit }),
|
|
314
352
|
suggestions: Suggestions([
|
|
315
353
|
{
|
|
316
354
|
title: "Show a bar chart",
|
|
@@ -339,16 +377,12 @@ export default function Home() {
|
|
|
339
377
|
|
|
340
378
|
return (
|
|
341
379
|
<AssistantRuntimeProvider aui={aui} runtime={runtime}>
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
<ContactFormToolUI />
|
|
349
|
-
<main className="h-full">
|
|
350
|
-
<Thread />
|
|
351
|
-
</main>
|
|
380
|
+
<div className="flex h-full flex-col">
|
|
381
|
+
<ExampleNav />
|
|
382
|
+
<main className="min-h-0 flex-1">
|
|
383
|
+
<Thread />
|
|
384
|
+
</main>
|
|
385
|
+
</div>
|
|
352
386
|
</AssistantRuntimeProvider>
|
|
353
387
|
);
|
|
354
388
|
}
|
|
@@ -361,13 +395,11 @@ export default function Home() {
|
|
|
361
395
|
"use client";
|
|
362
396
|
|
|
363
397
|
import { GenerativeUIRender, type GenerativeUISpec } from "@assistant-ui/react";
|
|
364
|
-
import {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
</span>
|
|
370
|
-
);
|
|
398
|
+
import {
|
|
399
|
+
UnknownComponentFallback,
|
|
400
|
+
componentsAllowlist,
|
|
401
|
+
} from "@/components/gui";
|
|
402
|
+
import { ExampleNav } from "@/components/example-nav";
|
|
371
403
|
|
|
372
404
|
/**
|
|
373
405
|
* Self-contained demo of the GenerativeUI primitive.
|
|
@@ -433,28 +465,79 @@ const exampleSpec: GenerativeUISpec = {
|
|
|
433
465
|
|
|
434
466
|
export default function GenerativeUIPrimitivePage() {
|
|
435
467
|
return (
|
|
436
|
-
<
|
|
437
|
-
<
|
|
438
|
-
|
|
439
|
-
<
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
468
|
+
<div className="flex h-full flex-col">
|
|
469
|
+
<ExampleNav />
|
|
470
|
+
<main className="mx-auto flex h-full max-w-2xl flex-col gap-6 overflow-y-auto p-8">
|
|
471
|
+
<header>
|
|
472
|
+
<h1 className="text-2xl font-bold">MessagePrimitive.GenerativeUI</h1>
|
|
473
|
+
<p className="text-muted-foreground mt-1 text-sm">
|
|
474
|
+
Agent-described React UI rendered from a JSON spec via a
|
|
475
|
+
consumer-provided component allowlist.
|
|
476
|
+
</p>
|
|
477
|
+
</header>
|
|
478
|
+
|
|
479
|
+
<section className="flex flex-col gap-4">
|
|
480
|
+
<GenerativeUIRender
|
|
481
|
+
spec={exampleSpec}
|
|
482
|
+
components={componentsAllowlist}
|
|
483
|
+
Fallback={UnknownComponentFallback}
|
|
484
|
+
/>
|
|
485
|
+
</section>
|
|
486
|
+
</main>
|
|
487
|
+
</div>
|
|
453
488
|
);
|
|
454
489
|
}
|
|
455
490
|
|
|
456
491
|
```
|
|
457
492
|
|
|
493
|
+
## app/toolkit.tsx
|
|
494
|
+
|
|
495
|
+
```tsx
|
|
496
|
+
"use generative";
|
|
497
|
+
|
|
498
|
+
import { defineToolkit, externalTool, humanTool } from "@assistant-ui/react";
|
|
499
|
+
import { ChartToolUI } from "@/components/chart-tool-ui";
|
|
500
|
+
import { DatePickerToolUI } from "@/components/date-picker-tool-ui";
|
|
501
|
+
import { ContactFormToolUI } from "@/components/contact-form-tool-ui";
|
|
502
|
+
import { LocationToolUI } from "@/components/location-tool-ui";
|
|
503
|
+
import { z } from "zod";
|
|
504
|
+
|
|
505
|
+
export default defineToolkit({
|
|
506
|
+
select_date: {
|
|
507
|
+
description:
|
|
508
|
+
"Ask the user to select a date. Use this when you need to collect a date (e.g. for scheduling, booking, deadlines).",
|
|
509
|
+
parameters: z.object({
|
|
510
|
+
prompt: z.string().describe("Message to display to the user"),
|
|
511
|
+
minDate: z.string().optional().describe("Minimum date (ISO string)"),
|
|
512
|
+
maxDate: z.string().optional().describe("Maximum date (ISO string)"),
|
|
513
|
+
}),
|
|
514
|
+
execute: humanTool(),
|
|
515
|
+
render: DatePickerToolUI,
|
|
516
|
+
},
|
|
517
|
+
collect_contact: {
|
|
518
|
+
description:
|
|
519
|
+
"Collect contact information from the user. Use this when you need the user's name, email, or phone number.",
|
|
520
|
+
parameters: z.object({
|
|
521
|
+
prompt: z.string().describe("Message to display to the user"),
|
|
522
|
+
fields: z
|
|
523
|
+
.array(z.enum(["name", "email", "phone"]))
|
|
524
|
+
.describe("Which fields to collect"),
|
|
525
|
+
}),
|
|
526
|
+
execute: humanTool(),
|
|
527
|
+
render: ContactFormToolUI,
|
|
528
|
+
},
|
|
529
|
+
generate_chart: {
|
|
530
|
+
execute: externalTool(),
|
|
531
|
+
render: ChartToolUI,
|
|
532
|
+
},
|
|
533
|
+
show_location: {
|
|
534
|
+
execute: externalTool(),
|
|
535
|
+
render: LocationToolUI,
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
```
|
|
540
|
+
|
|
458
541
|
## components.json
|
|
459
542
|
|
|
460
543
|
```json
|
|
@@ -485,12 +568,257 @@ export default function GenerativeUIPrimitivePage() {
|
|
|
485
568
|
|
|
486
569
|
```
|
|
487
570
|
|
|
571
|
+
## components/assistant-message-gui.tsx
|
|
572
|
+
|
|
573
|
+
```tsx
|
|
574
|
+
"use client";
|
|
575
|
+
|
|
576
|
+
import { MarkdownText } from "@/components/assistant-ui/markdown-text";
|
|
577
|
+
import {
|
|
578
|
+
Reasoning,
|
|
579
|
+
ReasoningContent,
|
|
580
|
+
ReasoningRoot,
|
|
581
|
+
ReasoningText,
|
|
582
|
+
ReasoningTrigger,
|
|
583
|
+
} from "@/components/assistant-ui/reasoning";
|
|
584
|
+
import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
|
|
585
|
+
import {
|
|
586
|
+
ToolGroupContent,
|
|
587
|
+
ToolGroupRoot,
|
|
588
|
+
ToolGroupTrigger,
|
|
589
|
+
} from "@/components/assistant-ui/tool-group";
|
|
590
|
+
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
|
591
|
+
import {
|
|
592
|
+
UnknownComponentFallback,
|
|
593
|
+
componentsAllowlist,
|
|
594
|
+
} from "@/components/gui";
|
|
595
|
+
import {
|
|
596
|
+
RENDER_GUI_TOOL_NAME,
|
|
597
|
+
isLeakedRenderGuiText,
|
|
598
|
+
parseRenderGuiResult,
|
|
599
|
+
} from "@/lib/render-gui-tool";
|
|
600
|
+
import { cn } from "@/lib/utils";
|
|
601
|
+
import type { GenerativeUISpec } from "@assistant-ui/react";
|
|
602
|
+
import {
|
|
603
|
+
ActionBarPrimitive,
|
|
604
|
+
AuiIf,
|
|
605
|
+
BranchPickerPrimitive,
|
|
606
|
+
ErrorPrimitive,
|
|
607
|
+
getMcpAppFromToolPart,
|
|
608
|
+
MessagePrimitive,
|
|
609
|
+
useAuiState,
|
|
610
|
+
} from "@assistant-ui/react";
|
|
611
|
+
import {
|
|
612
|
+
CheckIcon,
|
|
613
|
+
ChevronLeftIcon,
|
|
614
|
+
ChevronRightIcon,
|
|
615
|
+
CopyIcon,
|
|
616
|
+
RefreshCwIcon,
|
|
617
|
+
} from "lucide-react";
|
|
618
|
+
import { type FC, useEffect, useRef } from "react";
|
|
619
|
+
|
|
620
|
+
type AssistantMessageGuiProps = {
|
|
621
|
+
/** Set false in dev to surface the dropped-bridge warning. */
|
|
622
|
+
bridgeEnabled?: boolean;
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
const AllowlistedGenerativeUI = ({ spec }: { spec?: GenerativeUISpec }) => (
|
|
626
|
+
<MessagePrimitive.GenerativeUI
|
|
627
|
+
spec={spec}
|
|
628
|
+
components={componentsAllowlist}
|
|
629
|
+
Fallback={UnknownComponentFallback}
|
|
630
|
+
/>
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
const useDroppedGenerativeUIWarning = (bridgeEnabled: boolean) => {
|
|
634
|
+
const content = useAuiState((s) => s.message.content);
|
|
635
|
+
const warned = useRef(false);
|
|
636
|
+
|
|
637
|
+
useEffect(() => {
|
|
638
|
+
if (
|
|
639
|
+
process.env.NODE_ENV === "production" ||
|
|
640
|
+
bridgeEnabled ||
|
|
641
|
+
warned.current
|
|
642
|
+
) {
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const hasRenderGui = content.some(
|
|
647
|
+
(part) =>
|
|
648
|
+
part.type === "tool-call" &&
|
|
649
|
+
part.toolName === RENDER_GUI_TOOL_NAME &&
|
|
650
|
+
part.result != null &&
|
|
651
|
+
parseRenderGuiResult(part.result) != null,
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
const hasUnhandledGenerativeUI = content.some(
|
|
655
|
+
(part) => part.type === "generative-ui",
|
|
656
|
+
);
|
|
657
|
+
|
|
658
|
+
if (hasRenderGui || hasUnhandledGenerativeUI) {
|
|
659
|
+
warned.current = true;
|
|
660
|
+
console.warn(
|
|
661
|
+
"[generative-ui] Message contains render_gui or generative-ui parts but the bridge is disabled. " +
|
|
662
|
+
"Enable the bridge in assistant-message-gui.tsx or wire MessagePrimitive.GenerativeUI.",
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
}, [bridgeEnabled, content]);
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
export const AssistantMessageGui: FC<AssistantMessageGuiProps> = ({
|
|
669
|
+
bridgeEnabled = true,
|
|
670
|
+
}) => {
|
|
671
|
+
useDroppedGenerativeUIWarning(bridgeEnabled);
|
|
672
|
+
|
|
673
|
+
const hasBridgedRenderGui = useAuiState((s) =>
|
|
674
|
+
s.message.content.some(
|
|
675
|
+
(part) =>
|
|
676
|
+
part.type === "tool-call" &&
|
|
677
|
+
part.toolName === RENDER_GUI_TOOL_NAME &&
|
|
678
|
+
parseRenderGuiResult(part.result) != null,
|
|
679
|
+
),
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
const ACTION_BAR_PT = "pt-1.5";
|
|
683
|
+
const ACTION_BAR_HEIGHT = `-mb-7.5 min-h-7.5 ${ACTION_BAR_PT}`;
|
|
684
|
+
|
|
685
|
+
return (
|
|
686
|
+
<MessagePrimitive.Root
|
|
687
|
+
data-slot="aui_assistant-message-root"
|
|
688
|
+
data-role="assistant"
|
|
689
|
+
className="fade-in slide-in-from-bottom-1 animate-in relative duration-150"
|
|
690
|
+
>
|
|
691
|
+
<div
|
|
692
|
+
data-slot="aui_assistant-message-content"
|
|
693
|
+
// [contain-intrinsic-size:auto_24px] fixes issue #4104, don't change without checking for regressions
|
|
694
|
+
className="text-foreground px-2 leading-relaxed wrap-break-word [contain-intrinsic-size:auto_24px] [content-visibility:auto]"
|
|
695
|
+
>
|
|
696
|
+
<MessagePrimitive.GroupedParts
|
|
697
|
+
groupBy={(part) => {
|
|
698
|
+
if (part.type === "reasoning") {
|
|
699
|
+
return ["group-chainOfThought", "group-reasoning"];
|
|
700
|
+
}
|
|
701
|
+
if (part.type === "tool-call") {
|
|
702
|
+
if (getMcpAppFromToolPart(part)) return [];
|
|
703
|
+
if (part.toolName === RENDER_GUI_TOOL_NAME) return [];
|
|
704
|
+
return ["group-chainOfThought", "group-tool"];
|
|
705
|
+
}
|
|
706
|
+
return [];
|
|
707
|
+
}}
|
|
708
|
+
>
|
|
709
|
+
{({ part, children }) => {
|
|
710
|
+
switch (part.type) {
|
|
711
|
+
case "group-chainOfThought":
|
|
712
|
+
return <div data-slot="aui_chain-of-thought">{children}</div>;
|
|
713
|
+
case "group-reasoning": {
|
|
714
|
+
const running = part.status.type === "running";
|
|
715
|
+
return (
|
|
716
|
+
<ReasoningRoot defaultOpen={running}>
|
|
717
|
+
<ReasoningTrigger active={running} />
|
|
718
|
+
<ReasoningContent aria-busy={running}>
|
|
719
|
+
<ReasoningText>{children}</ReasoningText>
|
|
720
|
+
</ReasoningContent>
|
|
721
|
+
</ReasoningRoot>
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
case "group-tool":
|
|
725
|
+
return (
|
|
726
|
+
<ToolGroupRoot>
|
|
727
|
+
<ToolGroupTrigger
|
|
728
|
+
count={part.indices.length}
|
|
729
|
+
active={part.status.type === "running"}
|
|
730
|
+
/>
|
|
731
|
+
<ToolGroupContent>{children}</ToolGroupContent>
|
|
732
|
+
</ToolGroupRoot>
|
|
733
|
+
);
|
|
734
|
+
case "text":
|
|
735
|
+
if (
|
|
736
|
+
bridgeEnabled &&
|
|
737
|
+
hasBridgedRenderGui &&
|
|
738
|
+
isLeakedRenderGuiText(part.text)
|
|
739
|
+
) {
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
return <MarkdownText />;
|
|
743
|
+
case "reasoning":
|
|
744
|
+
return <Reasoning {...part} />;
|
|
745
|
+
case "generative-ui":
|
|
746
|
+
return bridgeEnabled ? <AllowlistedGenerativeUI /> : null;
|
|
747
|
+
case "tool-call": {
|
|
748
|
+
if (bridgeEnabled && part.toolName === RENDER_GUI_TOOL_NAME) {
|
|
749
|
+
const spec = parseRenderGuiResult(part.result);
|
|
750
|
+
if (spec) return <AllowlistedGenerativeUI spec={spec} />;
|
|
751
|
+
}
|
|
752
|
+
return part.toolUI ?? <ToolFallback {...part} />;
|
|
753
|
+
}
|
|
754
|
+
default:
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
}}
|
|
758
|
+
</MessagePrimitive.GroupedParts>
|
|
759
|
+
<MessagePrimitive.Error>
|
|
760
|
+
<ErrorPrimitive.Root className="aui-message-error-root border-destructive bg-destructive/10 text-destructive dark:bg-destructive/5 mt-2 rounded-md border p-3 text-sm dark:text-red-200">
|
|
761
|
+
<ErrorPrimitive.Message className="aui-message-error-message line-clamp-2" />
|
|
762
|
+
</ErrorPrimitive.Root>
|
|
763
|
+
</MessagePrimitive.Error>
|
|
764
|
+
</div>
|
|
765
|
+
|
|
766
|
+
<div
|
|
767
|
+
data-slot="aui_assistant-message-footer"
|
|
768
|
+
className={cn("ms-2 flex items-center", ACTION_BAR_HEIGHT)}
|
|
769
|
+
>
|
|
770
|
+
<BranchPickerPrimitive.Root
|
|
771
|
+
hideWhenSingleBranch
|
|
772
|
+
className="aui-branch-picker-root text-muted-foreground -ms-2 me-2 inline-flex items-center text-xs"
|
|
773
|
+
>
|
|
774
|
+
<BranchPickerPrimitive.Previous asChild>
|
|
775
|
+
<TooltipIconButton tooltip="Previous">
|
|
776
|
+
<ChevronLeftIcon />
|
|
777
|
+
</TooltipIconButton>
|
|
778
|
+
</BranchPickerPrimitive.Previous>
|
|
779
|
+
<span className="aui-branch-picker-state font-medium">
|
|
780
|
+
<BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />
|
|
781
|
+
</span>
|
|
782
|
+
<BranchPickerPrimitive.Next asChild>
|
|
783
|
+
<TooltipIconButton tooltip="Next">
|
|
784
|
+
<ChevronRightIcon />
|
|
785
|
+
</TooltipIconButton>
|
|
786
|
+
</BranchPickerPrimitive.Next>
|
|
787
|
+
</BranchPickerPrimitive.Root>
|
|
788
|
+
<ActionBarPrimitive.Root
|
|
789
|
+
hideWhenRunning
|
|
790
|
+
autohide="not-last"
|
|
791
|
+
className="aui-assistant-action-bar-root text-muted-foreground -ms-1 flex gap-1"
|
|
792
|
+
>
|
|
793
|
+
<ActionBarPrimitive.Copy asChild>
|
|
794
|
+
<TooltipIconButton tooltip="Copy">
|
|
795
|
+
<AuiIf condition={(s) => s.message.isCopied}>
|
|
796
|
+
<CheckIcon />
|
|
797
|
+
</AuiIf>
|
|
798
|
+
<AuiIf condition={(s) => !s.message.isCopied}>
|
|
799
|
+
<CopyIcon />
|
|
800
|
+
</AuiIf>
|
|
801
|
+
</TooltipIconButton>
|
|
802
|
+
</ActionBarPrimitive.Copy>
|
|
803
|
+
<ActionBarPrimitive.Reload asChild>
|
|
804
|
+
<TooltipIconButton tooltip="Refresh">
|
|
805
|
+
<RefreshCwIcon />
|
|
806
|
+
</TooltipIconButton>
|
|
807
|
+
</ActionBarPrimitive.Reload>
|
|
808
|
+
</ActionBarPrimitive.Root>
|
|
809
|
+
</div>
|
|
810
|
+
</MessagePrimitive.Root>
|
|
811
|
+
);
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
```
|
|
815
|
+
|
|
488
816
|
## components/chart-tool-ui.tsx
|
|
489
817
|
|
|
490
818
|
```tsx
|
|
491
819
|
"use client";
|
|
492
820
|
|
|
493
|
-
import {
|
|
821
|
+
import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
|
|
494
822
|
import {
|
|
495
823
|
BarChart,
|
|
496
824
|
Bar,
|
|
@@ -528,13 +856,12 @@ const COLORS = [
|
|
|
528
856
|
"oklch(0.769 0.188 70.08)",
|
|
529
857
|
];
|
|
530
858
|
|
|
531
|
-
export const ChartToolUI
|
|
532
|
-
|
|
533
|
-
render: function ChartUI({ args, status }) {
|
|
859
|
+
export const ChartToolUI: ToolCallMessagePartComponent<ChartArgs, ChartResult> =
|
|
860
|
+
function ChartUI({ args, status }) {
|
|
534
861
|
if (status.type === "running" && !args.data?.length) {
|
|
535
862
|
return (
|
|
536
863
|
<div className="flex items-center gap-2 rounded-lg border p-4">
|
|
537
|
-
<Loader2Icon className="size-4 animate-spin
|
|
864
|
+
<Loader2Icon className="text-muted-foreground size-4 animate-spin" />
|
|
538
865
|
<span className="text-muted-foreground text-sm">
|
|
539
866
|
Generating chart...
|
|
540
867
|
</span>
|
|
@@ -547,7 +874,7 @@ export const ChartToolUI = makeAssistantToolUI<ChartArgs, ChartResult>({
|
|
|
547
874
|
|
|
548
875
|
return (
|
|
549
876
|
<div className="my-2 rounded-lg border p-4">
|
|
550
|
-
<h3 className="mb-3 text-center font-semibold
|
|
877
|
+
<h3 className="mb-3 text-center text-sm font-semibold">{title}</h3>
|
|
551
878
|
<div className="h-[280px] w-full">
|
|
552
879
|
<ResponsiveContainer width="100%" height="100%">
|
|
553
880
|
{type === "bar" ? (
|
|
@@ -620,8 +947,7 @@ export const ChartToolUI = makeAssistantToolUI<ChartArgs, ChartResult>({
|
|
|
620
947
|
</div>
|
|
621
948
|
</div>
|
|
622
949
|
);
|
|
623
|
-
}
|
|
624
|
-
});
|
|
950
|
+
};
|
|
625
951
|
|
|
626
952
|
```
|
|
627
953
|
|
|
@@ -631,7 +957,7 @@ export const ChartToolUI = makeAssistantToolUI<ChartArgs, ChartResult>({
|
|
|
631
957
|
"use client";
|
|
632
958
|
|
|
633
959
|
import { useState } from "react";
|
|
634
|
-
import {
|
|
960
|
+
import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
|
|
635
961
|
import { UserIcon, CheckCircle2Icon } from "lucide-react";
|
|
636
962
|
|
|
637
963
|
type ContactFormArgs = {
|
|
@@ -645,86 +971,79 @@ type ContactFormResult = {
|
|
|
645
971
|
phone?: string;
|
|
646
972
|
};
|
|
647
973
|
|
|
648
|
-
export const ContactFormToolUI
|
|
974
|
+
export const ContactFormToolUI: ToolCallMessagePartComponent<
|
|
649
975
|
ContactFormArgs,
|
|
650
976
|
ContactFormResult
|
|
651
|
-
>({
|
|
652
|
-
|
|
653
|
-
render: function ContactFormUI({ args, result, addResult }) {
|
|
654
|
-
const [form, setForm] = useState<ContactFormResult>({});
|
|
655
|
-
|
|
656
|
-
if (result) {
|
|
657
|
-
return (
|
|
658
|
-
<div className="my-2 rounded-lg border border-green-200 bg-green-50 p-3">
|
|
659
|
-
<div className="mb-1 flex items-center gap-2">
|
|
660
|
-
<CheckCircle2Icon className="size-4 text-green-600" />
|
|
661
|
-
<span className="font-medium text-green-800 text-sm">
|
|
662
|
-
Contact info collected
|
|
663
|
-
</span>
|
|
664
|
-
</div>
|
|
665
|
-
<div className="ml-6 space-y-0.5 text-green-700 text-xs">
|
|
666
|
-
{result.name && <p>Name: {result.name}</p>}
|
|
667
|
-
{result.email && <p>Email: {result.email}</p>}
|
|
668
|
-
{result.phone && <p>Phone: {result.phone}</p>}
|
|
669
|
-
</div>
|
|
670
|
-
</div>
|
|
671
|
-
);
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
const fields = args.fields ?? ["name", "email", "phone"];
|
|
675
|
-
const isValid = fields.every((f) => form[f]?.trim());
|
|
977
|
+
> = function ContactFormUI({ args, result, addResult }) {
|
|
978
|
+
const [form, setForm] = useState<ContactFormResult>({});
|
|
676
979
|
|
|
980
|
+
if (result) {
|
|
677
981
|
return (
|
|
678
|
-
<div className="my-2 rounded-lg border p-
|
|
679
|
-
<div className="mb-
|
|
680
|
-
<
|
|
681
|
-
<span className="font-medium text-
|
|
982
|
+
<div className="my-2 rounded-lg border border-green-200 bg-green-50 p-3">
|
|
983
|
+
<div className="mb-1 flex items-center gap-2">
|
|
984
|
+
<CheckCircle2Icon className="size-4 text-green-600" />
|
|
985
|
+
<span className="text-sm font-medium text-green-800">
|
|
986
|
+
Contact info collected
|
|
987
|
+
</span>
|
|
682
988
|
</div>
|
|
683
|
-
<div className="space-y-
|
|
684
|
-
{
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
placeholder="Name"
|
|
688
|
-
value={form.name ?? ""}
|
|
689
|
-
onChange={(e) => setForm((p) => ({ ...p, name: e.target.value }))}
|
|
690
|
-
className="w-full rounded-md border bg-background px-3 py-2 text-sm"
|
|
691
|
-
/>
|
|
692
|
-
)}
|
|
693
|
-
{fields.includes("email") && (
|
|
694
|
-
<input
|
|
695
|
-
type="email"
|
|
696
|
-
placeholder="Email"
|
|
697
|
-
value={form.email ?? ""}
|
|
698
|
-
onChange={(e) =>
|
|
699
|
-
setForm((p) => ({ ...p, email: e.target.value }))
|
|
700
|
-
}
|
|
701
|
-
className="w-full rounded-md border bg-background px-3 py-2 text-sm"
|
|
702
|
-
/>
|
|
703
|
-
)}
|
|
704
|
-
{fields.includes("phone") && (
|
|
705
|
-
<input
|
|
706
|
-
type="tel"
|
|
707
|
-
placeholder="Phone"
|
|
708
|
-
value={form.phone ?? ""}
|
|
709
|
-
onChange={(e) =>
|
|
710
|
-
setForm((p) => ({ ...p, phone: e.target.value }))
|
|
711
|
-
}
|
|
712
|
-
className="w-full rounded-md border bg-background px-3 py-2 text-sm"
|
|
713
|
-
/>
|
|
714
|
-
)}
|
|
715
|
-
<button
|
|
716
|
-
type="button"
|
|
717
|
-
disabled={!isValid}
|
|
718
|
-
onClick={() => addResult(form)}
|
|
719
|
-
className="w-full rounded-md bg-primary px-4 py-2 font-medium text-primary-foreground text-sm disabled:opacity-50"
|
|
720
|
-
>
|
|
721
|
-
Submit
|
|
722
|
-
</button>
|
|
989
|
+
<div className="ml-6 space-y-0.5 text-xs text-green-700">
|
|
990
|
+
{result.name && <p>Name: {result.name}</p>}
|
|
991
|
+
{result.email && <p>Email: {result.email}</p>}
|
|
992
|
+
{result.phone && <p>Phone: {result.phone}</p>}
|
|
723
993
|
</div>
|
|
724
994
|
</div>
|
|
725
995
|
);
|
|
726
|
-
}
|
|
727
|
-
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
const fields = args.fields ?? ["name", "email", "phone"];
|
|
999
|
+
const isValid = fields.every((f) => form[f]?.trim());
|
|
1000
|
+
|
|
1001
|
+
return (
|
|
1002
|
+
<div className="my-2 rounded-lg border p-4">
|
|
1003
|
+
<div className="mb-3 flex items-center gap-2">
|
|
1004
|
+
<UserIcon className="text-muted-foreground size-4" />
|
|
1005
|
+
<span className="text-sm font-medium">{args.prompt}</span>
|
|
1006
|
+
</div>
|
|
1007
|
+
<div className="space-y-2">
|
|
1008
|
+
{fields.includes("name") && (
|
|
1009
|
+
<input
|
|
1010
|
+
type="text"
|
|
1011
|
+
placeholder="Name"
|
|
1012
|
+
value={form.name ?? ""}
|
|
1013
|
+
onChange={(e) => setForm((p) => ({ ...p, name: e.target.value }))}
|
|
1014
|
+
className="bg-background w-full rounded-md border px-3 py-2 text-sm"
|
|
1015
|
+
/>
|
|
1016
|
+
)}
|
|
1017
|
+
{fields.includes("email") && (
|
|
1018
|
+
<input
|
|
1019
|
+
type="email"
|
|
1020
|
+
placeholder="Email"
|
|
1021
|
+
value={form.email ?? ""}
|
|
1022
|
+
onChange={(e) => setForm((p) => ({ ...p, email: e.target.value }))}
|
|
1023
|
+
className="bg-background w-full rounded-md border px-3 py-2 text-sm"
|
|
1024
|
+
/>
|
|
1025
|
+
)}
|
|
1026
|
+
{fields.includes("phone") && (
|
|
1027
|
+
<input
|
|
1028
|
+
type="tel"
|
|
1029
|
+
placeholder="Phone"
|
|
1030
|
+
value={form.phone ?? ""}
|
|
1031
|
+
onChange={(e) => setForm((p) => ({ ...p, phone: e.target.value }))}
|
|
1032
|
+
className="bg-background w-full rounded-md border px-3 py-2 text-sm"
|
|
1033
|
+
/>
|
|
1034
|
+
)}
|
|
1035
|
+
<button
|
|
1036
|
+
type="button"
|
|
1037
|
+
disabled={!isValid}
|
|
1038
|
+
onClick={() => addResult(form)}
|
|
1039
|
+
className="bg-primary text-primary-foreground w-full rounded-md px-4 py-2 text-sm font-medium disabled:opacity-50"
|
|
1040
|
+
>
|
|
1041
|
+
Submit
|
|
1042
|
+
</button>
|
|
1043
|
+
</div>
|
|
1044
|
+
</div>
|
|
1045
|
+
);
|
|
1046
|
+
};
|
|
728
1047
|
|
|
729
1048
|
```
|
|
730
1049
|
|
|
@@ -734,7 +1053,7 @@ export const ContactFormToolUI = makeAssistantToolUI<
|
|
|
734
1053
|
"use client";
|
|
735
1054
|
|
|
736
1055
|
import { useState } from "react";
|
|
737
|
-
import {
|
|
1056
|
+
import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
|
|
738
1057
|
import { CalendarIcon, CheckCircle2Icon } from "lucide-react";
|
|
739
1058
|
|
|
740
1059
|
type DatePickerArgs = {
|
|
@@ -747,53 +1066,378 @@ type DatePickerResult = {
|
|
|
747
1066
|
date: string;
|
|
748
1067
|
};
|
|
749
1068
|
|
|
750
|
-
export const DatePickerToolUI
|
|
1069
|
+
export const DatePickerToolUI: ToolCallMessagePartComponent<
|
|
751
1070
|
DatePickerArgs,
|
|
752
1071
|
DatePickerResult
|
|
753
|
-
>({
|
|
754
|
-
|
|
755
|
-
render: function DatePickerUI({ args, result, addResult }) {
|
|
756
|
-
const [value, setValue] = useState("");
|
|
1072
|
+
> = function DatePickerUI({ args, result, addResult }) {
|
|
1073
|
+
const [value, setValue] = useState("");
|
|
757
1074
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
1075
|
+
if (result) {
|
|
1076
|
+
return (
|
|
1077
|
+
<div className="my-2 flex items-center gap-2 rounded-lg border border-green-200 bg-green-50 p-3">
|
|
1078
|
+
<CheckCircle2Icon className="size-4 text-green-600" />
|
|
1079
|
+
<span className="text-sm text-green-800">
|
|
1080
|
+
Selected: {new Date(result.date).toLocaleDateString()}
|
|
1081
|
+
</span>
|
|
1082
|
+
</div>
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
return (
|
|
1087
|
+
<div className="my-2 rounded-lg border p-4">
|
|
1088
|
+
<div className="mb-3 flex items-center gap-2">
|
|
1089
|
+
<CalendarIcon className="text-muted-foreground size-4" />
|
|
1090
|
+
<span className="text-sm font-medium">{args.prompt}</span>
|
|
1091
|
+
</div>
|
|
1092
|
+
<div className="flex items-center gap-2">
|
|
1093
|
+
<input
|
|
1094
|
+
type="date"
|
|
1095
|
+
value={value}
|
|
1096
|
+
min={args.minDate}
|
|
1097
|
+
max={args.maxDate}
|
|
1098
|
+
onChange={(e) => setValue(e.target.value)}
|
|
1099
|
+
className="bg-background rounded-md border px-3 py-2 text-sm"
|
|
1100
|
+
/>
|
|
1101
|
+
<button
|
|
1102
|
+
type="button"
|
|
1103
|
+
disabled={!value}
|
|
1104
|
+
onClick={() => addResult({ date: new Date(value).toISOString() })}
|
|
1105
|
+
className="bg-primary text-primary-foreground rounded-md px-4 py-2 text-sm font-medium disabled:opacity-50"
|
|
1106
|
+
>
|
|
1107
|
+
Confirm
|
|
1108
|
+
</button>
|
|
1109
|
+
</div>
|
|
1110
|
+
</div>
|
|
1111
|
+
);
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1114
|
+
```
|
|
1115
|
+
|
|
1116
|
+
## components/example-nav.tsx
|
|
1117
|
+
|
|
1118
|
+
```tsx
|
|
1119
|
+
import Link from "next/link";
|
|
1120
|
+
|
|
1121
|
+
const links = [
|
|
1122
|
+
{ href: "/", label: "Tool UI demo" },
|
|
1123
|
+
{ href: "/primitive", label: "Static primitive" },
|
|
1124
|
+
{ href: "/gui-chat", label: "GUI chat" },
|
|
1125
|
+
] as const;
|
|
1126
|
+
|
|
1127
|
+
export function ExampleNav() {
|
|
1128
|
+
return (
|
|
1129
|
+
<nav className="bg-background flex flex-wrap items-center gap-2 border-b px-4 py-2 text-sm">
|
|
1130
|
+
<span className="text-muted-foreground font-medium">Examples:</span>
|
|
1131
|
+
{links.map(({ href, label }) => (
|
|
1132
|
+
<Link
|
|
1133
|
+
key={href}
|
|
1134
|
+
href={href}
|
|
1135
|
+
className="text-foreground hover:bg-muted rounded-md px-2 py-1"
|
|
1136
|
+
>
|
|
1137
|
+
{label}
|
|
1138
|
+
</Link>
|
|
1139
|
+
))}
|
|
1140
|
+
</nav>
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
```
|
|
1145
|
+
|
|
1146
|
+
## components/gui-thread.tsx
|
|
1147
|
+
|
|
1148
|
+
```tsx
|
|
1149
|
+
import {
|
|
1150
|
+
ComposerAddAttachment,
|
|
1151
|
+
ComposerAttachments,
|
|
1152
|
+
UserMessageAttachments,
|
|
1153
|
+
} from "@/components/assistant-ui/attachment";
|
|
1154
|
+
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
|
1155
|
+
import { Button } from "@/components/ui/button";
|
|
1156
|
+
import { cn } from "@/lib/utils";
|
|
1157
|
+
import {
|
|
1158
|
+
ActionBarPrimitive,
|
|
1159
|
+
AuiIf,
|
|
1160
|
+
BranchPickerPrimitive,
|
|
1161
|
+
ComposerPrimitive,
|
|
1162
|
+
MessagePrimitive,
|
|
1163
|
+
SuggestionPrimitive,
|
|
1164
|
+
ThreadPrimitive,
|
|
1165
|
+
useAuiState,
|
|
1166
|
+
} from "@assistant-ui/react";
|
|
1167
|
+
import {
|
|
1168
|
+
ArrowDownIcon,
|
|
1169
|
+
ArrowUpIcon,
|
|
1170
|
+
ChevronLeftIcon,
|
|
1171
|
+
ChevronRightIcon,
|
|
1172
|
+
PencilIcon,
|
|
1173
|
+
SquareIcon,
|
|
1174
|
+
} from "lucide-react";
|
|
1175
|
+
import type { FC } from "react";
|
|
1176
|
+
import { AssistantMessageGui } from "@/components/assistant-message-gui";
|
|
1177
|
+
|
|
1178
|
+
/** Thread fork: swaps the assistant message renderer for the generative-ui bridge. */
|
|
1179
|
+
export const GuiThread: FC = () => {
|
|
1180
|
+
return (
|
|
1181
|
+
<ThreadPrimitive.Root
|
|
1182
|
+
className="aui-root aui-thread-root bg-background @container flex h-full flex-col"
|
|
1183
|
+
style={{
|
|
1184
|
+
["--thread-max-width" as string]: "44rem",
|
|
1185
|
+
["--composer-radius" as string]: "24px",
|
|
1186
|
+
["--composer-padding" as string]: "10px",
|
|
1187
|
+
}}
|
|
1188
|
+
>
|
|
1189
|
+
<ThreadPrimitive.Viewport
|
|
1190
|
+
turnAnchor="top"
|
|
1191
|
+
data-slot="aui_thread-viewport"
|
|
1192
|
+
className="relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll scroll-smooth"
|
|
1193
|
+
>
|
|
1194
|
+
<div className="mx-auto flex w-full max-w-(--thread-max-width) flex-1 flex-col px-4 pt-4">
|
|
1195
|
+
<AuiIf condition={(s) => s.thread.isEmpty}>
|
|
1196
|
+
<ThreadWelcome />
|
|
1197
|
+
</AuiIf>
|
|
1198
|
+
|
|
1199
|
+
<div
|
|
1200
|
+
data-slot="aui_message-group"
|
|
1201
|
+
className="mb-10 flex flex-col gap-y-8 empty:hidden"
|
|
1202
|
+
>
|
|
1203
|
+
<ThreadPrimitive.Messages>
|
|
1204
|
+
{() => <ThreadMessage />}
|
|
1205
|
+
</ThreadPrimitive.Messages>
|
|
1206
|
+
</div>
|
|
1207
|
+
|
|
1208
|
+
<ThreadPrimitive.ViewportFooter className="aui-thread-viewport-footer bg-background sticky bottom-0 mt-auto flex flex-col gap-4 overflow-visible rounded-t-(--composer-radius) pb-4 md:pb-6">
|
|
1209
|
+
<ThreadScrollToBottom />
|
|
1210
|
+
<Composer />
|
|
1211
|
+
</ThreadPrimitive.ViewportFooter>
|
|
765
1212
|
</div>
|
|
766
|
-
|
|
767
|
-
|
|
1213
|
+
</ThreadPrimitive.Viewport>
|
|
1214
|
+
</ThreadPrimitive.Root>
|
|
1215
|
+
);
|
|
1216
|
+
};
|
|
768
1217
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1218
|
+
const ThreadMessage: FC = () => {
|
|
1219
|
+
const role = useAuiState((s) => s.message.role);
|
|
1220
|
+
const isEditing = useAuiState((s) => s.message.composer.isEditing);
|
|
1221
|
+
|
|
1222
|
+
if (isEditing) return <EditComposer />;
|
|
1223
|
+
if (role === "user") return <UserMessage />;
|
|
1224
|
+
return <AssistantMessageGui />;
|
|
1225
|
+
};
|
|
1226
|
+
|
|
1227
|
+
const ThreadScrollToBottom: FC = () => {
|
|
1228
|
+
return (
|
|
1229
|
+
<ThreadPrimitive.ScrollToBottom asChild>
|
|
1230
|
+
<TooltipIconButton
|
|
1231
|
+
tooltip="Scroll to bottom"
|
|
1232
|
+
variant="outline"
|
|
1233
|
+
className="aui-thread-scroll-to-bottom dark:border-border dark:bg-background dark:hover:bg-accent absolute -top-12 z-10 self-center rounded-full p-4 disabled:invisible"
|
|
1234
|
+
>
|
|
1235
|
+
<ArrowDownIcon />
|
|
1236
|
+
</TooltipIconButton>
|
|
1237
|
+
</ThreadPrimitive.ScrollToBottom>
|
|
1238
|
+
);
|
|
1239
|
+
};
|
|
1240
|
+
|
|
1241
|
+
const ThreadWelcome: FC = () => {
|
|
1242
|
+
return (
|
|
1243
|
+
<div className="aui-thread-welcome-root my-auto flex grow flex-col">
|
|
1244
|
+
<div className="aui-thread-welcome-center flex w-full grow flex-col items-center justify-center">
|
|
1245
|
+
<div className="aui-thread-welcome-message flex size-full flex-col justify-center px-4">
|
|
1246
|
+
<h1 className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in fill-mode-both text-2xl font-semibold duration-200">
|
|
1247
|
+
Compose UI from a spec
|
|
1248
|
+
</h1>
|
|
1249
|
+
<p className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in fill-mode-both text-muted-foreground text-xl delay-75 duration-200">
|
|
1250
|
+
Ask the agent to call render_gui with Card, Button, and Stack.
|
|
1251
|
+
</p>
|
|
774
1252
|
</div>
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
1253
|
+
</div>
|
|
1254
|
+
<ThreadSuggestions />
|
|
1255
|
+
</div>
|
|
1256
|
+
);
|
|
1257
|
+
};
|
|
1258
|
+
|
|
1259
|
+
const ThreadSuggestions: FC = () => {
|
|
1260
|
+
return (
|
|
1261
|
+
<div className="aui-thread-welcome-suggestions grid w-full gap-2 pb-4 @md:grid-cols-2">
|
|
1262
|
+
<ThreadPrimitive.Suggestions>
|
|
1263
|
+
{() => <ThreadSuggestionItem />}
|
|
1264
|
+
</ThreadPrimitive.Suggestions>
|
|
1265
|
+
</div>
|
|
1266
|
+
);
|
|
1267
|
+
};
|
|
1268
|
+
|
|
1269
|
+
const ThreadSuggestionItem: FC = () => {
|
|
1270
|
+
return (
|
|
1271
|
+
<div className="aui-thread-welcome-suggestion-display fade-in slide-in-from-bottom-2 animate-in fill-mode-both duration-200 nth-[n+3]:hidden @md:nth-[n+3]:block">
|
|
1272
|
+
<SuggestionPrimitive.Trigger send asChild>
|
|
1273
|
+
<Button
|
|
1274
|
+
variant="ghost"
|
|
1275
|
+
className="aui-thread-welcome-suggestion bg-background hover:bg-muted h-auto w-full flex-wrap items-start justify-start gap-1 rounded-3xl border px-4 py-3 text-start text-sm transition-colors @md:flex-col"
|
|
1276
|
+
>
|
|
1277
|
+
<SuggestionPrimitive.Title className="aui-thread-welcome-suggestion-text-1 font-medium" />
|
|
1278
|
+
<SuggestionPrimitive.Description className="aui-thread-welcome-suggestion-text-2 text-muted-foreground empty:hidden" />
|
|
1279
|
+
</Button>
|
|
1280
|
+
</SuggestionPrimitive.Trigger>
|
|
1281
|
+
</div>
|
|
1282
|
+
);
|
|
1283
|
+
};
|
|
1284
|
+
|
|
1285
|
+
const Composer: FC = () => {
|
|
1286
|
+
return (
|
|
1287
|
+
<ComposerPrimitive.Root className="aui-composer-root relative flex w-full flex-col">
|
|
1288
|
+
<ComposerPrimitive.AttachmentDropzone asChild>
|
|
1289
|
+
<div
|
|
1290
|
+
data-slot="aui_composer-shell"
|
|
1291
|
+
className="bg-background focus-within:border-ring/75 focus-within:ring-ring/20 data-[dragging=true]:border-ring data-[dragging=true]:bg-accent/50 flex w-full flex-col gap-2 rounded-(--composer-radius) border p-(--composer-padding) transition-shadow focus-within:ring-2 data-[dragging=true]:border-dashed"
|
|
1292
|
+
>
|
|
1293
|
+
<ComposerAttachments />
|
|
1294
|
+
<ComposerPrimitive.Input
|
|
1295
|
+
placeholder="Send a message..."
|
|
1296
|
+
className="aui-composer-input placeholder:text-muted-foreground/80 max-h-32 min-h-10 w-full resize-none bg-transparent px-1.75 py-1 text-sm outline-none"
|
|
1297
|
+
rows={1}
|
|
1298
|
+
autoFocus
|
|
1299
|
+
aria-label="Message input"
|
|
783
1300
|
/>
|
|
784
|
-
<
|
|
1301
|
+
<ComposerAction />
|
|
1302
|
+
</div>
|
|
1303
|
+
</ComposerPrimitive.AttachmentDropzone>
|
|
1304
|
+
</ComposerPrimitive.Root>
|
|
1305
|
+
);
|
|
1306
|
+
};
|
|
1307
|
+
|
|
1308
|
+
const ComposerAction: FC = () => {
|
|
1309
|
+
return (
|
|
1310
|
+
<div className="aui-composer-action-wrapper relative flex items-center justify-between">
|
|
1311
|
+
<ComposerAddAttachment />
|
|
1312
|
+
<AuiIf condition={(s) => !s.thread.isRunning}>
|
|
1313
|
+
<ComposerPrimitive.Send asChild>
|
|
1314
|
+
<TooltipIconButton
|
|
1315
|
+
tooltip="Send message"
|
|
1316
|
+
side="bottom"
|
|
785
1317
|
type="button"
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
className="
|
|
1318
|
+
variant="default"
|
|
1319
|
+
size="icon"
|
|
1320
|
+
className="aui-composer-send size-8 rounded-full"
|
|
1321
|
+
aria-label="Send message"
|
|
789
1322
|
>
|
|
790
|
-
|
|
791
|
-
</
|
|
1323
|
+
<ArrowUpIcon className="aui-composer-send-icon size-4" />
|
|
1324
|
+
</TooltipIconButton>
|
|
1325
|
+
</ComposerPrimitive.Send>
|
|
1326
|
+
</AuiIf>
|
|
1327
|
+
<AuiIf condition={(s) => s.thread.isRunning}>
|
|
1328
|
+
<ComposerPrimitive.Cancel asChild>
|
|
1329
|
+
<Button
|
|
1330
|
+
type="button"
|
|
1331
|
+
variant="default"
|
|
1332
|
+
size="icon"
|
|
1333
|
+
className="aui-composer-cancel size-8 rounded-full"
|
|
1334
|
+
aria-label="Stop generating"
|
|
1335
|
+
>
|
|
1336
|
+
<SquareIcon className="aui-composer-cancel-icon size-3 fill-current" />
|
|
1337
|
+
</Button>
|
|
1338
|
+
</ComposerPrimitive.Cancel>
|
|
1339
|
+
</AuiIf>
|
|
1340
|
+
</div>
|
|
1341
|
+
);
|
|
1342
|
+
};
|
|
1343
|
+
|
|
1344
|
+
const UserMessage: FC = () => {
|
|
1345
|
+
return (
|
|
1346
|
+
<MessagePrimitive.Root
|
|
1347
|
+
data-slot="aui_user-message-root"
|
|
1348
|
+
className="fade-in slide-in-from-bottom-1 animate-in grid auto-rows-auto grid-cols-[minmax(72px,1fr)_auto] content-start gap-y-2 px-2 duration-150 [contain-intrinsic-size:auto_60px] [content-visibility:auto] [&:where(>*)]:col-start-2"
|
|
1349
|
+
data-role="user"
|
|
1350
|
+
>
|
|
1351
|
+
<UserMessageAttachments />
|
|
1352
|
+
|
|
1353
|
+
<div className="aui-user-message-content-wrapper relative col-start-2 min-w-0">
|
|
1354
|
+
<div className="aui-user-message-content peer bg-muted text-foreground rounded-2xl px-4 py-2.5 wrap-break-word empty:hidden">
|
|
1355
|
+
<MessagePrimitive.Parts />
|
|
1356
|
+
</div>
|
|
1357
|
+
<div className="aui-user-action-bar-wrapper absolute start-0 top-1/2 -translate-x-full -translate-y-1/2 pe-2 peer-empty:hidden rtl:translate-x-full">
|
|
1358
|
+
<UserActionBar />
|
|
792
1359
|
</div>
|
|
793
1360
|
</div>
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
1361
|
+
|
|
1362
|
+
<BranchPicker
|
|
1363
|
+
data-slot="aui_user-branch-picker"
|
|
1364
|
+
className="col-span-full col-start-1 row-start-3 -me-1 justify-end"
|
|
1365
|
+
/>
|
|
1366
|
+
</MessagePrimitive.Root>
|
|
1367
|
+
);
|
|
1368
|
+
};
|
|
1369
|
+
|
|
1370
|
+
const UserActionBar: FC = () => {
|
|
1371
|
+
return (
|
|
1372
|
+
<ActionBarPrimitive.Root
|
|
1373
|
+
hideWhenRunning
|
|
1374
|
+
autohide="not-last"
|
|
1375
|
+
className="aui-user-action-bar-root flex flex-col items-end"
|
|
1376
|
+
>
|
|
1377
|
+
<ActionBarPrimitive.Edit asChild>
|
|
1378
|
+
<TooltipIconButton tooltip="Edit" className="aui-user-action-edit p-4">
|
|
1379
|
+
<PencilIcon />
|
|
1380
|
+
</TooltipIconButton>
|
|
1381
|
+
</ActionBarPrimitive.Edit>
|
|
1382
|
+
</ActionBarPrimitive.Root>
|
|
1383
|
+
);
|
|
1384
|
+
};
|
|
1385
|
+
|
|
1386
|
+
const EditComposer: FC = () => {
|
|
1387
|
+
return (
|
|
1388
|
+
<MessagePrimitive.Root
|
|
1389
|
+
data-slot="aui_edit-composer-wrapper"
|
|
1390
|
+
className="flex flex-col px-2"
|
|
1391
|
+
>
|
|
1392
|
+
<ComposerPrimitive.Root className="aui-edit-composer-root bg-muted ms-auto flex w-full max-w-[85%] flex-col rounded-2xl">
|
|
1393
|
+
<ComposerPrimitive.Input
|
|
1394
|
+
className="aui-edit-composer-input text-foreground min-h-14 w-full resize-none bg-transparent p-4 text-sm outline-none"
|
|
1395
|
+
autoFocus
|
|
1396
|
+
/>
|
|
1397
|
+
<div className="aui-edit-composer-footer mx-3 mb-3 flex items-center gap-2 self-end">
|
|
1398
|
+
<ComposerPrimitive.Cancel asChild>
|
|
1399
|
+
<Button variant="ghost" size="sm">
|
|
1400
|
+
Cancel
|
|
1401
|
+
</Button>
|
|
1402
|
+
</ComposerPrimitive.Cancel>
|
|
1403
|
+
<ComposerPrimitive.Send asChild>
|
|
1404
|
+
<Button size="sm">Update</Button>
|
|
1405
|
+
</ComposerPrimitive.Send>
|
|
1406
|
+
</div>
|
|
1407
|
+
</ComposerPrimitive.Root>
|
|
1408
|
+
</MessagePrimitive.Root>
|
|
1409
|
+
);
|
|
1410
|
+
};
|
|
1411
|
+
|
|
1412
|
+
const BranchPicker: FC<BranchPickerPrimitive.Root.Props> = ({
|
|
1413
|
+
className,
|
|
1414
|
+
...rest
|
|
1415
|
+
}) => {
|
|
1416
|
+
return (
|
|
1417
|
+
<BranchPickerPrimitive.Root
|
|
1418
|
+
hideWhenSingleBranch
|
|
1419
|
+
className={cn(
|
|
1420
|
+
"aui-branch-picker-root text-muted-foreground -ms-2 me-2 inline-flex items-center text-xs",
|
|
1421
|
+
className,
|
|
1422
|
+
)}
|
|
1423
|
+
{...rest}
|
|
1424
|
+
>
|
|
1425
|
+
<BranchPickerPrimitive.Previous asChild>
|
|
1426
|
+
<TooltipIconButton tooltip="Previous">
|
|
1427
|
+
<ChevronLeftIcon />
|
|
1428
|
+
</TooltipIconButton>
|
|
1429
|
+
</BranchPickerPrimitive.Previous>
|
|
1430
|
+
<span className="aui-branch-picker-state font-medium">
|
|
1431
|
+
<BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />
|
|
1432
|
+
</span>
|
|
1433
|
+
<BranchPickerPrimitive.Next asChild>
|
|
1434
|
+
<TooltipIconButton tooltip="Next">
|
|
1435
|
+
<ChevronRightIcon />
|
|
1436
|
+
</TooltipIconButton>
|
|
1437
|
+
</BranchPickerPrimitive.Next>
|
|
1438
|
+
</BranchPickerPrimitive.Root>
|
|
1439
|
+
);
|
|
1440
|
+
};
|
|
797
1441
|
|
|
798
1442
|
```
|
|
799
1443
|
|
|
@@ -809,13 +1453,23 @@ import type { ComponentType, PropsWithChildren } from "react";
|
|
|
809
1453
|
* The agent's JSON spec can reference any of these by name.
|
|
810
1454
|
*/
|
|
811
1455
|
|
|
1456
|
+
export const UnknownComponentFallback = ({
|
|
1457
|
+
component,
|
|
1458
|
+
}: {
|
|
1459
|
+
component: string;
|
|
1460
|
+
}) => (
|
|
1461
|
+
<span className="bg-muted rounded px-1.5 py-0.5 font-mono text-xs">
|
|
1462
|
+
unknown component: {component}
|
|
1463
|
+
</span>
|
|
1464
|
+
);
|
|
1465
|
+
|
|
812
1466
|
const Card: ComponentType<
|
|
813
1467
|
PropsWithChildren<{ title?: string; description?: string }>
|
|
814
1468
|
> = ({ title, description, children }) => (
|
|
815
|
-
<div className="rounded-xl border
|
|
816
|
-
{title ? <div className="font-semibold
|
|
1469
|
+
<div className="bg-card rounded-xl border p-4 shadow-sm">
|
|
1470
|
+
{title ? <div className="text-base font-semibold">{title}</div> : null}
|
|
817
1471
|
{description ? (
|
|
818
|
-
<div className="
|
|
1472
|
+
<div className="text-muted-foreground mt-1 text-sm">{description}</div>
|
|
819
1473
|
) : null}
|
|
820
1474
|
{children ? <div className="mt-3">{children}</div> : null}
|
|
821
1475
|
</div>
|
|
@@ -835,7 +1489,7 @@ const Button: ComponentType<
|
|
|
835
1489
|
return (
|
|
836
1490
|
<button
|
|
837
1491
|
type="button"
|
|
838
|
-
className={`mt-2 inline-flex items-center rounded-md px-3 py-1.5 font-medium
|
|
1492
|
+
className={`mt-2 inline-flex items-center rounded-md px-3 py-1.5 text-sm font-medium ${cls}`}
|
|
839
1493
|
onClick={() => {
|
|
840
1494
|
if (onClickPrompt) {
|
|
841
1495
|
// In a richer integration this would call composer.setText / send.
|
|
@@ -877,9 +1531,9 @@ const Stat: ComponentType<{ label: string; value: string | number }> = ({
|
|
|
877
1531
|
label,
|
|
878
1532
|
value,
|
|
879
1533
|
}) => (
|
|
880
|
-
<div className="flex flex-col rounded-lg border
|
|
1534
|
+
<div className="bg-muted/40 flex flex-col rounded-lg border p-3">
|
|
881
1535
|
<span className="text-muted-foreground text-xs">{label}</span>
|
|
882
|
-
<span className="font-semibold
|
|
1536
|
+
<span className="text-xl font-semibold">{value}</span>
|
|
883
1537
|
</div>
|
|
884
1538
|
);
|
|
885
1539
|
|
|
@@ -899,7 +1553,7 @@ export const componentsAllowlist = {
|
|
|
899
1553
|
```tsx
|
|
900
1554
|
"use client";
|
|
901
1555
|
|
|
902
|
-
import {
|
|
1556
|
+
import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
|
|
903
1557
|
import { MapPinIcon, Loader2Icon } from "lucide-react";
|
|
904
1558
|
|
|
905
1559
|
type LocationArgs = {
|
|
@@ -913,60 +1567,199 @@ type LocationResult = {
|
|
|
913
1567
|
success: boolean;
|
|
914
1568
|
};
|
|
915
1569
|
|
|
916
|
-
export const LocationToolUI
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1570
|
+
export const LocationToolUI: ToolCallMessagePartComponent<
|
|
1571
|
+
LocationArgs,
|
|
1572
|
+
LocationResult
|
|
1573
|
+
> = function LocationUI({ args, status }) {
|
|
1574
|
+
if (status.type === "running" && (args.lat == null || args.lng == null)) {
|
|
1575
|
+
return (
|
|
1576
|
+
<div className="flex items-center gap-2 rounded-lg border p-4">
|
|
1577
|
+
<Loader2Icon className="text-muted-foreground size-4 animate-spin" />
|
|
1578
|
+
<span className="text-muted-foreground text-sm">
|
|
1579
|
+
Loading location...
|
|
1580
|
+
</span>
|
|
1581
|
+
</div>
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
930
1584
|
|
|
931
|
-
|
|
932
|
-
|
|
1585
|
+
const { name, address, lat, lng } = args;
|
|
1586
|
+
if (lat == null || lng == null) return null;
|
|
933
1587
|
|
|
934
|
-
|
|
1588
|
+
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}`;
|
|
935
1589
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
</div>
|
|
946
|
-
</div>
|
|
947
|
-
<iframe
|
|
948
|
-
title={`Map of ${name}`}
|
|
949
|
-
src={mapSrc}
|
|
950
|
-
className="h-[200px] w-full border-0"
|
|
951
|
-
/>
|
|
1590
|
+
return (
|
|
1591
|
+
<div className="my-2 overflow-hidden rounded-lg border">
|
|
1592
|
+
<div className="flex items-center gap-2 border-b px-3 py-2">
|
|
1593
|
+
<MapPinIcon className="text-muted-foreground size-4" />
|
|
1594
|
+
<div>
|
|
1595
|
+
<p className="text-sm font-medium">{name}</p>
|
|
1596
|
+
{address && (
|
|
1597
|
+
<p className="text-muted-foreground text-xs">{address}</p>
|
|
1598
|
+
)}
|
|
952
1599
|
</div>
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1600
|
+
</div>
|
|
1601
|
+
<iframe
|
|
1602
|
+
title={`Map of ${name}`}
|
|
1603
|
+
src={mapSrc}
|
|
1604
|
+
className="h-[200px] w-full border-0"
|
|
1605
|
+
/>
|
|
1606
|
+
</div>
|
|
1607
|
+
);
|
|
1608
|
+
};
|
|
1609
|
+
|
|
1610
|
+
```
|
|
1611
|
+
|
|
1612
|
+
## lib/render-gui-tool.test.ts
|
|
1613
|
+
|
|
1614
|
+
```typescript
|
|
1615
|
+
import { describe, expect, it } from "vitest";
|
|
1616
|
+
import {
|
|
1617
|
+
generativeUISpecSchema,
|
|
1618
|
+
isLeakedRenderGuiText,
|
|
1619
|
+
parseRenderGuiResult,
|
|
1620
|
+
renderGuiToolResultSchema,
|
|
1621
|
+
} from "./render-gui-tool";
|
|
1622
|
+
|
|
1623
|
+
describe("render-gui-tool", () => {
|
|
1624
|
+
it("accepts a valid Card + Button spec", () => {
|
|
1625
|
+
const spec = {
|
|
1626
|
+
root: {
|
|
1627
|
+
component: "Card",
|
|
1628
|
+
props: { title: "Welcome" },
|
|
1629
|
+
children: [
|
|
1630
|
+
{
|
|
1631
|
+
component: "Button",
|
|
1632
|
+
props: { label: "Get started", variant: "primary" },
|
|
1633
|
+
},
|
|
1634
|
+
],
|
|
1635
|
+
},
|
|
1636
|
+
};
|
|
1637
|
+
|
|
1638
|
+
expect(generativeUISpecSchema.parse(spec)).toEqual(spec);
|
|
1639
|
+
expect(
|
|
1640
|
+
parseRenderGuiResult(renderGuiToolResultSchema.parse({ spec })),
|
|
1641
|
+
).toEqual(spec);
|
|
1642
|
+
});
|
|
1643
|
+
|
|
1644
|
+
it("rejects a spec with empty component name", () => {
|
|
1645
|
+
expect(() =>
|
|
1646
|
+
generativeUISpecSchema.parse({
|
|
1647
|
+
root: { component: "", props: { title: "Bad" } },
|
|
1648
|
+
}),
|
|
1649
|
+
).toThrow();
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1652
|
+
it("returns undefined for malformed tool results", () => {
|
|
1653
|
+
expect(parseRenderGuiResult({ notSpec: true })).toBeUndefined();
|
|
1654
|
+
expect(parseRenderGuiResult(null)).toBeUndefined();
|
|
1655
|
+
});
|
|
1656
|
+
|
|
1657
|
+
it("detects leaked render_gui tool result text", () => {
|
|
1658
|
+
const payload = renderGuiToolResultSchema.parse({
|
|
1659
|
+
spec: {
|
|
1660
|
+
root: {
|
|
1661
|
+
component: "Card",
|
|
1662
|
+
children: [{ component: "Text", children: ["Hello"] }],
|
|
1663
|
+
},
|
|
1664
|
+
},
|
|
1665
|
+
});
|
|
1666
|
+
|
|
1667
|
+
expect(isLeakedRenderGuiText(JSON.stringify(payload, null, 2))).toBe(true);
|
|
1668
|
+
expect(
|
|
1669
|
+
isLeakedRenderGuiText(
|
|
1670
|
+
"```json\n" + JSON.stringify(payload, null, 2) + "\n```",
|
|
1671
|
+
),
|
|
1672
|
+
).toBe(true);
|
|
1673
|
+
expect(isLeakedRenderGuiText("Here is your card.")).toBe(false);
|
|
1674
|
+
});
|
|
1675
|
+
});
|
|
1676
|
+
|
|
1677
|
+
```
|
|
1678
|
+
|
|
1679
|
+
## lib/render-gui-tool.ts
|
|
1680
|
+
|
|
1681
|
+
```typescript
|
|
1682
|
+
import { z } from "zod";
|
|
1683
|
+
import type { GenerativeUISpec } from "@assistant-ui/react";
|
|
1684
|
+
|
|
1685
|
+
export const RENDER_GUI_TOOL_NAME = "render_gui" as const;
|
|
1686
|
+
|
|
1687
|
+
const generativeUINodeSchema: z.ZodType<unknown> = z.lazy(() =>
|
|
1688
|
+
z.union([
|
|
1689
|
+
z.string(),
|
|
1690
|
+
z.object({
|
|
1691
|
+
component: z.string().min(1),
|
|
1692
|
+
props: z.record(z.string(), z.unknown()).optional(),
|
|
1693
|
+
children: z.array(generativeUINodeSchema).optional(),
|
|
1694
|
+
key: z.string().optional(),
|
|
1695
|
+
}),
|
|
1696
|
+
]),
|
|
956
1697
|
);
|
|
957
1698
|
|
|
1699
|
+
export const generativeUISpecSchema = z.object({
|
|
1700
|
+
root: z.union([
|
|
1701
|
+
generativeUINodeSchema,
|
|
1702
|
+
z.array(generativeUINodeSchema).min(1),
|
|
1703
|
+
]),
|
|
1704
|
+
});
|
|
1705
|
+
|
|
1706
|
+
export const renderGuiToolInputSchema = z.object({
|
|
1707
|
+
spec: generativeUISpecSchema,
|
|
1708
|
+
});
|
|
1709
|
+
|
|
1710
|
+
export const renderGuiToolResultSchema = renderGuiToolInputSchema;
|
|
1711
|
+
|
|
1712
|
+
export const parseRenderGuiResult = (
|
|
1713
|
+
result: unknown,
|
|
1714
|
+
): GenerativeUISpec | undefined => {
|
|
1715
|
+
const parsed = renderGuiToolResultSchema.safeParse(result);
|
|
1716
|
+
return parsed.success ? (parsed.data.spec as GenerativeUISpec) : undefined;
|
|
1717
|
+
};
|
|
1718
|
+
|
|
1719
|
+
export const renderGuiToolDescription =
|
|
1720
|
+
"Compose inline UI from the allowlisted component library (Card, Text, Button, Stack, Stat, Heading). " +
|
|
1721
|
+
"Pass a JSON spec with a root node tree. Use for dashboards, status panels, and structured layouts — not for user input forms (use interactive tool UI instead).";
|
|
1722
|
+
|
|
1723
|
+
export const renderGuiChatInstructions =
|
|
1724
|
+
"When the user asks for UI, call render_gui with the spec. " +
|
|
1725
|
+
"Do not paste JSON, code blocks, or the tool result in your text reply — the client renders the UI from the tool output automatically. " +
|
|
1726
|
+
"Keep any text reply brief or omit it when the UI alone answers the request.";
|
|
1727
|
+
|
|
1728
|
+
const stripMarkdownJsonFence = (text: string): string => {
|
|
1729
|
+
const trimmed = text.trim();
|
|
1730
|
+
const match = trimmed.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```\s*$/i);
|
|
1731
|
+
return match ? match[1]!.trim() : trimmed;
|
|
1732
|
+
};
|
|
1733
|
+
|
|
1734
|
+
/** Text echo of a render_gui tool result/spec — hide when the bridge already rendered it. */
|
|
1735
|
+
export const isLeakedRenderGuiText = (text: string): boolean => {
|
|
1736
|
+
const candidate = stripMarkdownJsonFence(text);
|
|
1737
|
+
if (!candidate.startsWith("{")) return false;
|
|
1738
|
+
|
|
1739
|
+
try {
|
|
1740
|
+
const parsed = JSON.parse(candidate);
|
|
1741
|
+
if (renderGuiToolResultSchema.safeParse(parsed).success) return true;
|
|
1742
|
+
if (generativeUISpecSchema.safeParse(parsed).success) return true;
|
|
1743
|
+
} catch {
|
|
1744
|
+
return false;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
return false;
|
|
1748
|
+
};
|
|
1749
|
+
|
|
958
1750
|
```
|
|
959
1751
|
|
|
960
1752
|
## next.config.ts
|
|
961
1753
|
|
|
962
1754
|
```typescript
|
|
1755
|
+
import { withAui } from "@assistant-ui/next";
|
|
963
1756
|
import type { NextConfig } from "next";
|
|
964
1757
|
|
|
965
1758
|
const nextConfig: NextConfig = {
|
|
966
1759
|
/* config options here */
|
|
967
1760
|
};
|
|
968
1761
|
|
|
969
|
-
export default nextConfig;
|
|
1762
|
+
export default withAui(nextConfig);
|
|
970
1763
|
|
|
971
1764
|
```
|
|
972
1765
|
|
|
@@ -981,34 +1774,37 @@ export default nextConfig;
|
|
|
981
1774
|
"scripts": {
|
|
982
1775
|
"dev": "next dev",
|
|
983
1776
|
"build": "next build",
|
|
984
|
-
"start": "next start"
|
|
1777
|
+
"start": "next start",
|
|
1778
|
+
"test": "vitest run"
|
|
985
1779
|
},
|
|
986
1780
|
"dependencies": {
|
|
987
|
-
"@ai-sdk/openai": "^3.0.
|
|
1781
|
+
"@ai-sdk/openai": "^3.0.68",
|
|
988
1782
|
"@assistant-ui/react": "workspace:*",
|
|
989
1783
|
"@assistant-ui/react-ai-sdk": "workspace:*",
|
|
990
1784
|
"@assistant-ui/ui": "workspace:*",
|
|
991
|
-
"ai": "^6.0.
|
|
1785
|
+
"ai": "^6.0.197",
|
|
992
1786
|
"class-variance-authority": "^0.7.1",
|
|
993
1787
|
"clsx": "^2.1.1",
|
|
994
|
-
"lucide-react": "^1.
|
|
995
|
-
"next": "^16.2.
|
|
996
|
-
"react": "^19.2.
|
|
997
|
-
"react-dom": "^19.2.
|
|
1788
|
+
"lucide-react": "^1.17.0",
|
|
1789
|
+
"next": "^16.2.7",
|
|
1790
|
+
"react": "^19.2.7",
|
|
1791
|
+
"react-dom": "^19.2.7",
|
|
998
1792
|
"recharts": "^3.8.1",
|
|
999
1793
|
"tailwind-merge": "^3.6.0",
|
|
1000
1794
|
"zod": "^4.4.3"
|
|
1001
1795
|
},
|
|
1002
1796
|
"devDependencies": {
|
|
1797
|
+
"@assistant-ui/next": "workspace:*",
|
|
1003
1798
|
"@assistant-ui/x-buildutils": "workspace:*",
|
|
1004
1799
|
"@tailwindcss/postcss": "^4.3.0",
|
|
1005
|
-
"@types/node": "^25.9.
|
|
1006
|
-
"@types/react": "^19.2.
|
|
1800
|
+
"@types/node": "^25.9.2",
|
|
1801
|
+
"@types/react": "^19.2.17",
|
|
1007
1802
|
"@types/react-dom": "^19.2.3",
|
|
1008
1803
|
"postcss": "^8.5.15",
|
|
1009
1804
|
"tailwindcss": "^4.3.0",
|
|
1010
1805
|
"tw-animate-css": "^1.4.0",
|
|
1011
|
-
"typescript": "^6.0.3"
|
|
1806
|
+
"typescript": "^6.0.3",
|
|
1807
|
+
"vitest": "^4.1.8"
|
|
1012
1808
|
}
|
|
1013
1809
|
}
|
|
1014
1810
|
|
|
@@ -1036,3 +1832,17 @@ export default nextConfig;
|
|
|
1036
1832
|
|
|
1037
1833
|
```
|
|
1038
1834
|
|
|
1835
|
+
## vitest.config.ts
|
|
1836
|
+
|
|
1837
|
+
```typescript
|
|
1838
|
+
import { defineConfig } from "vitest/config";
|
|
1839
|
+
|
|
1840
|
+
export default defineConfig({
|
|
1841
|
+
test: {
|
|
1842
|
+
environment: "node",
|
|
1843
|
+
include: ["lib/**/*.test.ts"],
|
|
1844
|
+
},
|
|
1845
|
+
});
|
|
1846
|
+
|
|
1847
|
+
```
|
|
1848
|
+
|