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