@assistant-ui/mcp-docs-server 0.1.25 → 0.1.27
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 +4 -4
- package/.docs/organized/code-examples/with-a2a.md +5 -5
- package/.docs/organized/code-examples/with-ag-ui.md +6 -6
- package/.docs/organized/code-examples/with-ai-sdk-v6.md +7 -7
- package/.docs/organized/code-examples/with-artifacts.md +7 -7
- package/.docs/organized/code-examples/with-assistant-transport.md +5 -5
- package/.docs/organized/code-examples/with-chain-of-thought.md +7 -7
- package/.docs/organized/code-examples/with-cloud-standalone.md +8 -8
- package/.docs/organized/code-examples/with-cloud.md +7 -7
- package/.docs/organized/code-examples/with-custom-thread-list.md +7 -7
- package/.docs/organized/code-examples/with-elevenlabs-conversational.md +511 -0
- package/.docs/organized/code-examples/with-elevenlabs-scribe.md +10 -10
- package/.docs/organized/code-examples/with-expo.md +18 -18
- package/.docs/organized/code-examples/with-external-store.md +5 -5
- package/.docs/organized/code-examples/with-ffmpeg.md +220 -66
- package/.docs/organized/code-examples/with-google-adk.md +6 -6
- package/.docs/organized/code-examples/with-heat-graph.md +4 -4
- package/.docs/organized/code-examples/with-interactables.md +836 -0
- package/.docs/organized/code-examples/with-langgraph.md +6 -6
- package/.docs/organized/code-examples/with-livekit.md +591 -0
- package/.docs/organized/code-examples/with-parent-id-grouping.md +6 -6
- package/.docs/organized/code-examples/with-react-hook-form.md +8 -8
- package/.docs/organized/code-examples/with-react-ink.md +3 -3
- package/.docs/organized/code-examples/with-react-router.md +11 -11
- package/.docs/organized/code-examples/with-store.md +11 -6
- package/.docs/organized/code-examples/with-tanstack.md +8 -8
- package/.docs/organized/code-examples/with-tap-runtime.md +8 -8
- package/.docs/raw/blog/2026-03-launch-week/index.mdx +31 -0
- package/.docs/raw/docs/(docs)/cli.mdx +60 -0
- package/.docs/raw/docs/(docs)/copilots/model-context.mdx +9 -1
- package/.docs/raw/docs/(docs)/guides/attachments.mdx +65 -4
- package/.docs/raw/docs/(docs)/guides/interactables.mdx +354 -0
- package/.docs/raw/docs/(docs)/guides/message-timing.mdx +3 -3
- package/.docs/raw/docs/(docs)/guides/multi-agent.mdx +1 -0
- package/.docs/raw/docs/(docs)/guides/tool-ui.mdx +29 -0
- package/.docs/raw/docs/(docs)/guides/voice.mdx +333 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/composer.mdx +128 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/message-part.mdx +23 -0
- package/.docs/raw/docs/cloud/ai-sdk-assistant-ui.mdx +6 -0
- package/.docs/raw/docs/cloud/ai-sdk.mdx +81 -1
- package/.docs/raw/docs/ink/primitives.mdx +141 -0
- package/.docs/raw/docs/primitives/action-bar.mdx +351 -0
- package/.docs/raw/docs/primitives/assistant-modal.mdx +215 -0
- package/.docs/raw/docs/primitives/attachment.mdx +216 -0
- package/.docs/raw/docs/primitives/branch-picker.mdx +221 -0
- package/.docs/raw/docs/primitives/chain-of-thought.mdx +311 -0
- package/.docs/raw/docs/primitives/composer.mdx +526 -0
- package/.docs/raw/docs/primitives/error.mdx +141 -0
- package/.docs/raw/docs/primitives/index.mdx +98 -0
- package/.docs/raw/docs/primitives/message.mdx +524 -0
- package/.docs/raw/docs/primitives/selection-toolbar.mdx +165 -0
- package/.docs/raw/docs/primitives/suggestion.mdx +242 -0
- package/.docs/raw/docs/primitives/thread-list.mdx +404 -0
- package/.docs/raw/docs/primitives/thread.mdx +482 -0
- package/.docs/raw/docs/runtimes/a2a/index.mdx +4 -0
- package/.docs/raw/docs/runtimes/ai-sdk/v6.mdx +2 -2
- package/.docs/raw/docs/runtimes/assistant-transport.mdx +6 -2
- package/.docs/raw/docs/ui/context-display.mdx +2 -2
- package/.docs/raw/docs/ui/mention.mdx +168 -0
- package/.docs/raw/docs/ui/model-selector.mdx +1 -1
- package/.docs/raw/docs/ui/voice.mdx +172 -0
- package/package.json +3 -4
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
# Example: with-interactables
|
|
2
|
+
|
|
3
|
+
## app/api/chat/route.ts
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { openai } from "@ai-sdk/openai";
|
|
7
|
+
import {
|
|
8
|
+
streamText,
|
|
9
|
+
convertToModelMessages,
|
|
10
|
+
stepCountIs,
|
|
11
|
+
jsonSchema,
|
|
12
|
+
} from "ai";
|
|
13
|
+
import type { UIMessage } from "ai";
|
|
14
|
+
|
|
15
|
+
export const maxDuration = 30;
|
|
16
|
+
|
|
17
|
+
type ToolDef = { description?: string; parameters: Record<string, unknown> };
|
|
18
|
+
|
|
19
|
+
export async function POST(req: Request) {
|
|
20
|
+
const {
|
|
21
|
+
messages,
|
|
22
|
+
system,
|
|
23
|
+
tools: clientTools,
|
|
24
|
+
}: {
|
|
25
|
+
messages: UIMessage[];
|
|
26
|
+
system?: string;
|
|
27
|
+
tools?: Record<string, ToolDef>;
|
|
28
|
+
} = await req.json();
|
|
29
|
+
|
|
30
|
+
// Convert client-defined tools (forwarded from model context) to AI SDK format.
|
|
31
|
+
// These have no `execute` — they are frontend tools executed on the client
|
|
32
|
+
// via useAssistantTool / useAssistantInteractable.
|
|
33
|
+
const tools = clientTools
|
|
34
|
+
? Object.fromEntries(
|
|
35
|
+
Object.entries(clientTools).map(([name, def]) => [
|
|
36
|
+
name,
|
|
37
|
+
{
|
|
38
|
+
description: def.description ?? "",
|
|
39
|
+
inputSchema: jsonSchema(def.parameters),
|
|
40
|
+
},
|
|
41
|
+
]),
|
|
42
|
+
)
|
|
43
|
+
: undefined;
|
|
44
|
+
|
|
45
|
+
const result = streamText({
|
|
46
|
+
model: openai("gpt-4o"),
|
|
47
|
+
messages: await convertToModelMessages(messages),
|
|
48
|
+
stopWhen: stepCountIs(10),
|
|
49
|
+
...(system ? { system } : {}),
|
|
50
|
+
...(tools ? { tools } : {}),
|
|
51
|
+
} as Parameters<typeof streamText>[0]);
|
|
52
|
+
|
|
53
|
+
return result.toUIMessageStreamResponse();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## app/globals.css
|
|
59
|
+
|
|
60
|
+
```css
|
|
61
|
+
@import "tailwindcss";
|
|
62
|
+
@import "tw-animate-css";
|
|
63
|
+
|
|
64
|
+
@source "../../../packages/ui/src";
|
|
65
|
+
|
|
66
|
+
@custom-variant dark (&:is(.dark *));
|
|
67
|
+
|
|
68
|
+
@theme inline {
|
|
69
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
70
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
71
|
+
--radius-lg: var(--radius);
|
|
72
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
73
|
+
--color-background: var(--background);
|
|
74
|
+
--color-foreground: var(--foreground);
|
|
75
|
+
--color-card: var(--card);
|
|
76
|
+
--color-card-foreground: var(--card-foreground);
|
|
77
|
+
--color-popover: var(--popover);
|
|
78
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
79
|
+
--color-primary: var(--primary);
|
|
80
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
81
|
+
--color-secondary: var(--secondary);
|
|
82
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
83
|
+
--color-muted: var(--muted);
|
|
84
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
85
|
+
--color-accent: var(--accent);
|
|
86
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
87
|
+
--color-destructive: var(--destructive);
|
|
88
|
+
--color-border: var(--border);
|
|
89
|
+
--color-input: var(--input);
|
|
90
|
+
--color-ring: var(--ring);
|
|
91
|
+
--color-chart-1: var(--chart-1);
|
|
92
|
+
--color-chart-2: var(--chart-2);
|
|
93
|
+
--color-chart-3: var(--chart-3);
|
|
94
|
+
--color-chart-4: var(--chart-4);
|
|
95
|
+
--color-chart-5: var(--chart-5);
|
|
96
|
+
--color-sidebar: var(--sidebar);
|
|
97
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
98
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
99
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
100
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
101
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
102
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
103
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
:root {
|
|
107
|
+
--radius: 0.625rem;
|
|
108
|
+
--background: oklch(1 0 0);
|
|
109
|
+
--foreground: oklch(0.141 0.005 285.823);
|
|
110
|
+
--card: oklch(1 0 0);
|
|
111
|
+
--card-foreground: oklch(0.141 0.005 285.823);
|
|
112
|
+
--popover: oklch(1 0 0);
|
|
113
|
+
--popover-foreground: oklch(0.141 0.005 285.823);
|
|
114
|
+
--primary: oklch(0.21 0.006 285.885);
|
|
115
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
116
|
+
--secondary: oklch(0.967 0.001 286.375);
|
|
117
|
+
--secondary-foreground: oklch(0.21 0.006 285.885);
|
|
118
|
+
--muted: oklch(0.967 0.001 286.375);
|
|
119
|
+
--muted-foreground: oklch(0.552 0.016 285.938);
|
|
120
|
+
--accent: oklch(0.967 0.001 286.375);
|
|
121
|
+
--accent-foreground: oklch(0.21 0.006 285.885);
|
|
122
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
123
|
+
--border: oklch(0.92 0.004 286.32);
|
|
124
|
+
--input: oklch(0.92 0.004 286.32);
|
|
125
|
+
--ring: oklch(0.705 0.015 286.067);
|
|
126
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
127
|
+
--chart-2: oklch(0.6 0.118 184.704);
|
|
128
|
+
--chart-3: oklch(0.398 0.07 227.392);
|
|
129
|
+
--chart-4: oklch(0.828 0.189 84.429);
|
|
130
|
+
--chart-5: oklch(0.769 0.188 70.08);
|
|
131
|
+
--sidebar: oklch(0.985 0 0);
|
|
132
|
+
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
|
133
|
+
--sidebar-primary: oklch(0.21 0.006 285.885);
|
|
134
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
135
|
+
--sidebar-accent: oklch(0.967 0.001 286.375);
|
|
136
|
+
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
|
137
|
+
--sidebar-border: oklch(0.92 0.004 286.32);
|
|
138
|
+
--sidebar-ring: oklch(0.705 0.015 286.067);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.dark {
|
|
142
|
+
--background: oklch(0.141 0.005 285.823);
|
|
143
|
+
--foreground: oklch(0.985 0 0);
|
|
144
|
+
--card: oklch(0.21 0.006 285.885);
|
|
145
|
+
--card-foreground: oklch(0.985 0 0);
|
|
146
|
+
--popover: oklch(0.21 0.006 285.885);
|
|
147
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
148
|
+
--primary: oklch(0.92 0.004 286.32);
|
|
149
|
+
--primary-foreground: oklch(0.21 0.006 285.885);
|
|
150
|
+
--secondary: oklch(0.274 0.006 286.033);
|
|
151
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
152
|
+
--muted: oklch(0.274 0.006 286.033);
|
|
153
|
+
--muted-foreground: oklch(0.705 0.015 286.067);
|
|
154
|
+
--accent: oklch(0.274 0.006 286.033);
|
|
155
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
156
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
157
|
+
--border: oklch(1 0 0 / 10%);
|
|
158
|
+
--input: oklch(1 0 0 / 15%);
|
|
159
|
+
--ring: oklch(0.552 0.016 285.938);
|
|
160
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
161
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
162
|
+
--chart-3: oklch(0.769 0.188 70.08);
|
|
163
|
+
--chart-4: oklch(0.627 0.265 303.9);
|
|
164
|
+
--chart-5: oklch(0.645 0.246 16.439);
|
|
165
|
+
--sidebar: oklch(0.21 0.006 285.885);
|
|
166
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
167
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
168
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
169
|
+
--sidebar-accent: oklch(0.274 0.006 286.033);
|
|
170
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
171
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
172
|
+
--sidebar-ring: oklch(0.552 0.016 285.938);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@layer base {
|
|
176
|
+
* {
|
|
177
|
+
@apply border-border outline-ring/50;
|
|
178
|
+
}
|
|
179
|
+
body {
|
|
180
|
+
@apply bg-background text-foreground;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## app/layout.tsx
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
import type { Metadata } from "next";
|
|
190
|
+
import "./globals.css";
|
|
191
|
+
|
|
192
|
+
export const metadata: Metadata = {
|
|
193
|
+
title: "assistant-ui Interactables Example",
|
|
194
|
+
description:
|
|
195
|
+
"Example using assistant-ui interactable components with a task board",
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export default function RootLayout({
|
|
199
|
+
children,
|
|
200
|
+
}: Readonly<{
|
|
201
|
+
children: React.ReactNode;
|
|
202
|
+
}>) {
|
|
203
|
+
return (
|
|
204
|
+
<html lang="en">
|
|
205
|
+
<body className="h-dvh">{children}</body>
|
|
206
|
+
</html>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## app/page.tsx
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
"use client";
|
|
216
|
+
|
|
217
|
+
import { useRef, useState, useCallback, useEffect } from "react";
|
|
218
|
+
import { Thread } from "@/components/assistant-ui/thread";
|
|
219
|
+
import {
|
|
220
|
+
AssistantRuntimeProvider,
|
|
221
|
+
Interactables,
|
|
222
|
+
Suggestions,
|
|
223
|
+
useAui,
|
|
224
|
+
useAssistantInteractable,
|
|
225
|
+
useInteractableState,
|
|
226
|
+
useAssistantTool,
|
|
227
|
+
} from "@assistant-ui/react";
|
|
228
|
+
import { useChatRuntime } from "@assistant-ui/react-ai-sdk";
|
|
229
|
+
import { lastAssistantMessageIsCompleteWithToolCalls } from "ai";
|
|
230
|
+
import { z } from "zod";
|
|
231
|
+
import {
|
|
232
|
+
CheckCircle2Icon,
|
|
233
|
+
CircleIcon,
|
|
234
|
+
ListTodoIcon,
|
|
235
|
+
Loader2Icon,
|
|
236
|
+
StickyNoteIcon,
|
|
237
|
+
Trash2Icon,
|
|
238
|
+
PlusIcon,
|
|
239
|
+
} from "lucide-react";
|
|
240
|
+
|
|
241
|
+
// ===========================================================================
|
|
242
|
+
// 1. Task Board — single-instance interactable + custom tool
|
|
243
|
+
// ===========================================================================
|
|
244
|
+
|
|
245
|
+
type Task = { id: string; title: string; done: boolean };
|
|
246
|
+
type TaskBoardState = { tasks: Task[] };
|
|
247
|
+
|
|
248
|
+
const taskBoardSchema = z.object({
|
|
249
|
+
tasks: z.array(
|
|
250
|
+
z.object({
|
|
251
|
+
id: z.string(),
|
|
252
|
+
title: z.string(),
|
|
253
|
+
done: z.boolean(),
|
|
254
|
+
}),
|
|
255
|
+
),
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const taskBoardInitialState: TaskBoardState = { tasks: [] };
|
|
259
|
+
|
|
260
|
+
let nextTaskId = 0;
|
|
261
|
+
|
|
262
|
+
function TaskBoard() {
|
|
263
|
+
const id = useAssistantInteractable("taskBoard", {
|
|
264
|
+
description:
|
|
265
|
+
"A task board showing the user's tasks. Use the manage_tasks tool (not update_taskBoard) to add/toggle/remove/clear tasks.",
|
|
266
|
+
stateSchema: taskBoardSchema,
|
|
267
|
+
initialState: taskBoardInitialState,
|
|
268
|
+
});
|
|
269
|
+
const [state, { setState, isPending }] = useInteractableState<TaskBoardState>(
|
|
270
|
+
id,
|
|
271
|
+
taskBoardInitialState,
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
const setStateRef = useRef(setState);
|
|
275
|
+
setStateRef.current = setState;
|
|
276
|
+
|
|
277
|
+
useAssistantTool({
|
|
278
|
+
toolName: "manage_tasks",
|
|
279
|
+
description:
|
|
280
|
+
'Manage tasks on the task board. Actions: "add" (requires title), "toggle" (requires id), "remove" (requires id), "clear" (no extra fields).',
|
|
281
|
+
parameters: z.object({
|
|
282
|
+
action: z.enum(["add", "toggle", "remove", "clear"]),
|
|
283
|
+
title: z.string().optional(),
|
|
284
|
+
id: z.string().optional(),
|
|
285
|
+
}),
|
|
286
|
+
execute: async (args) => {
|
|
287
|
+
const set = setStateRef.current;
|
|
288
|
+
switch (args.action) {
|
|
289
|
+
case "add": {
|
|
290
|
+
const id = `task-${++nextTaskId}`;
|
|
291
|
+
set((prev) => ({
|
|
292
|
+
tasks: [
|
|
293
|
+
...prev.tasks,
|
|
294
|
+
{ id, title: args.title ?? "Untitled", done: false },
|
|
295
|
+
],
|
|
296
|
+
}));
|
|
297
|
+
return { success: true, id };
|
|
298
|
+
}
|
|
299
|
+
case "toggle": {
|
|
300
|
+
if (!args.id) return { success: false, error: "id is required" };
|
|
301
|
+
set((prev) => ({
|
|
302
|
+
tasks: prev.tasks.map((t) =>
|
|
303
|
+
t.id === args.id ? { ...t, done: !t.done } : t,
|
|
304
|
+
),
|
|
305
|
+
}));
|
|
306
|
+
return { success: true };
|
|
307
|
+
}
|
|
308
|
+
case "remove": {
|
|
309
|
+
if (!args.id) return { success: false, error: "id is required" };
|
|
310
|
+
set((prev) => ({
|
|
311
|
+
tasks: prev.tasks.filter((t) => t.id !== args.id),
|
|
312
|
+
}));
|
|
313
|
+
return { success: true };
|
|
314
|
+
}
|
|
315
|
+
case "clear": {
|
|
316
|
+
set({ tasks: [] });
|
|
317
|
+
return { success: true };
|
|
318
|
+
}
|
|
319
|
+
default:
|
|
320
|
+
return { success: false, error: "Unknown action" };
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const doneCount = state.tasks.filter((t) => t.done).length;
|
|
326
|
+
|
|
327
|
+
return (
|
|
328
|
+
<div className="flex flex-col">
|
|
329
|
+
<div className="flex items-center gap-2 border-b px-4 py-3">
|
|
330
|
+
<ListTodoIcon className="size-4 text-muted-foreground" />
|
|
331
|
+
<span className="font-semibold text-sm">Task Board</span>
|
|
332
|
+
{isPending && (
|
|
333
|
+
<Loader2Icon className="size-3 animate-spin text-muted-foreground" />
|
|
334
|
+
)}
|
|
335
|
+
{state.tasks.length > 0 && (
|
|
336
|
+
<span className="ml-auto rounded-full bg-primary/10 px-2 py-0.5 font-medium text-primary text-xs">
|
|
337
|
+
{doneCount}/{state.tasks.length}
|
|
338
|
+
</span>
|
|
339
|
+
)}
|
|
340
|
+
</div>
|
|
341
|
+
<div className="flex-1 overflow-y-auto p-3">
|
|
342
|
+
{state.tasks.length === 0 ? (
|
|
343
|
+
<p className="py-6 text-center text-muted-foreground text-xs">
|
|
344
|
+
No tasks yet. Ask the assistant!
|
|
345
|
+
</p>
|
|
346
|
+
) : (
|
|
347
|
+
<ul className="space-y-1">
|
|
348
|
+
{state.tasks.map((task) => (
|
|
349
|
+
<li
|
|
350
|
+
key={task.id}
|
|
351
|
+
className="group flex items-center gap-2 rounded-lg px-3 py-2 transition-colors hover:bg-muted"
|
|
352
|
+
>
|
|
353
|
+
<button
|
|
354
|
+
type="button"
|
|
355
|
+
onClick={() =>
|
|
356
|
+
setState((prev) => ({
|
|
357
|
+
tasks: prev.tasks.map((t) =>
|
|
358
|
+
t.id === task.id ? { ...t, done: !t.done } : t,
|
|
359
|
+
),
|
|
360
|
+
}))
|
|
361
|
+
}
|
|
362
|
+
className="shrink-0"
|
|
363
|
+
>
|
|
364
|
+
{task.done ? (
|
|
365
|
+
<CheckCircle2Icon className="size-4 text-primary" />
|
|
366
|
+
) : (
|
|
367
|
+
<CircleIcon className="size-4 text-muted-foreground" />
|
|
368
|
+
)}
|
|
369
|
+
</button>
|
|
370
|
+
<span
|
|
371
|
+
className={`flex-1 text-sm ${task.done ? "text-muted-foreground line-through" : ""}`}
|
|
372
|
+
>
|
|
373
|
+
{task.title}
|
|
374
|
+
</span>
|
|
375
|
+
<button
|
|
376
|
+
type="button"
|
|
377
|
+
onClick={() =>
|
|
378
|
+
setState((prev) => ({
|
|
379
|
+
tasks: prev.tasks.filter((t) => t.id !== task.id),
|
|
380
|
+
}))
|
|
381
|
+
}
|
|
382
|
+
className="shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
|
|
383
|
+
>
|
|
384
|
+
<Trash2Icon className="size-3.5 text-muted-foreground hover:text-destructive" />
|
|
385
|
+
</button>
|
|
386
|
+
</li>
|
|
387
|
+
))}
|
|
388
|
+
</ul>
|
|
389
|
+
)}
|
|
390
|
+
</div>
|
|
391
|
+
</div>
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ===========================================================================
|
|
396
|
+
// 2. Sticky Notes — multi-instance interactable + selection + partial updates
|
|
397
|
+
// ===========================================================================
|
|
398
|
+
|
|
399
|
+
type NoteState = { title: string; content: string; color: string };
|
|
400
|
+
|
|
401
|
+
const noteSchema = z.object({
|
|
402
|
+
title: z.string(),
|
|
403
|
+
content: z.string(),
|
|
404
|
+
color: z.enum(["yellow", "blue", "green", "pink"]),
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const noteInitialState: NoteState = {
|
|
408
|
+
title: "New Note",
|
|
409
|
+
content: "",
|
|
410
|
+
color: "yellow",
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const COLORS: Record<string, string> = {
|
|
414
|
+
yellow: "bg-yellow-100 border-yellow-300",
|
|
415
|
+
blue: "bg-blue-100 border-blue-300",
|
|
416
|
+
green: "bg-green-100 border-green-300",
|
|
417
|
+
pink: "bg-pink-100 border-pink-300",
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
function NoteCard({
|
|
421
|
+
noteId,
|
|
422
|
+
selectedId,
|
|
423
|
+
onSelect,
|
|
424
|
+
onRemove,
|
|
425
|
+
}: {
|
|
426
|
+
noteId: string;
|
|
427
|
+
selectedId: string | null;
|
|
428
|
+
onSelect: (id: string) => void;
|
|
429
|
+
onRemove: (id: string) => void;
|
|
430
|
+
}) {
|
|
431
|
+
// Multi-instance: each NoteCard has a unique `id`, all share name "note"
|
|
432
|
+
// Partial updates: AI can send { color: "blue" } without resending title/content
|
|
433
|
+
// Selection: clicking a note calls setSelected(true)
|
|
434
|
+
useAssistantInteractable("note", {
|
|
435
|
+
id: noteId,
|
|
436
|
+
description:
|
|
437
|
+
"A sticky note. The AI can partially update any field (title, content, color) without resending the others.",
|
|
438
|
+
stateSchema: noteSchema,
|
|
439
|
+
initialState: noteInitialState,
|
|
440
|
+
selected: selectedId === noteId,
|
|
441
|
+
});
|
|
442
|
+
const [state, { setSelected }] = useInteractableState<NoteState>(
|
|
443
|
+
noteId,
|
|
444
|
+
noteInitialState,
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
const isSelected = selectedId === noteId;
|
|
448
|
+
|
|
449
|
+
const handleClick = () => {
|
|
450
|
+
onSelect(noteId);
|
|
451
|
+
setSelected(true);
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
return (
|
|
455
|
+
<div
|
|
456
|
+
role="button"
|
|
457
|
+
tabIndex={0}
|
|
458
|
+
onClick={handleClick}
|
|
459
|
+
onKeyDown={(e) => (e.key === "Enter" || e.key === " ") && handleClick()}
|
|
460
|
+
className={`group relative flex w-full cursor-pointer flex-col gap-1 rounded-lg border-2 p-3 text-left transition-all ${COLORS[state.color] ?? COLORS.yellow} ${isSelected ? "ring-2 ring-primary ring-offset-1" : "hover:shadow-sm"}`}
|
|
461
|
+
>
|
|
462
|
+
{isSelected && (
|
|
463
|
+
<span className="absolute top-1.5 right-2 rounded bg-primary px-1.5 py-0.5 font-medium text-[10px] text-primary-foreground leading-none">
|
|
464
|
+
SELECTED
|
|
465
|
+
</span>
|
|
466
|
+
)}
|
|
467
|
+
<span className="pr-16 font-semibold text-sm text-zinc-800">
|
|
468
|
+
{state.title}
|
|
469
|
+
</span>
|
|
470
|
+
<span className="line-clamp-3 text-xs text-zinc-600">
|
|
471
|
+
{state.content || "Empty note"}
|
|
472
|
+
</span>
|
|
473
|
+
<button
|
|
474
|
+
type="button"
|
|
475
|
+
onClick={(e) => {
|
|
476
|
+
e.stopPropagation();
|
|
477
|
+
onRemove(noteId);
|
|
478
|
+
}}
|
|
479
|
+
className="absolute right-2 bottom-2 rounded p-0.5 opacity-0 transition-opacity hover:bg-black/10 group-hover:opacity-100"
|
|
480
|
+
>
|
|
481
|
+
<Trash2Icon className="size-3 text-zinc-500" />
|
|
482
|
+
</button>
|
|
483
|
+
</div>
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const NOTE_IDS_KEY = "interactables-example-note-ids";
|
|
488
|
+
|
|
489
|
+
function loadNoteIds(): string[] {
|
|
490
|
+
try {
|
|
491
|
+
const saved = localStorage.getItem(NOTE_IDS_KEY);
|
|
492
|
+
return saved ? JSON.parse(saved) : [];
|
|
493
|
+
} catch {
|
|
494
|
+
return [];
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function NotesPanel() {
|
|
499
|
+
const [noteIds, setNoteIds] = useState<string[]>([]);
|
|
500
|
+
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
501
|
+
const hydratedRef = useRef(false);
|
|
502
|
+
|
|
503
|
+
useEffect(() => {
|
|
504
|
+
if (!hydratedRef.current) {
|
|
505
|
+
hydratedRef.current = true;
|
|
506
|
+
const saved = loadNoteIds();
|
|
507
|
+
if (saved.length > 0) setNoteIds(saved);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
localStorage.setItem(NOTE_IDS_KEY, JSON.stringify(noteIds));
|
|
511
|
+
}, [noteIds]);
|
|
512
|
+
|
|
513
|
+
const noteIdsRef = useRef(noteIds);
|
|
514
|
+
noteIdsRef.current = noteIds;
|
|
515
|
+
const setNoteIdsRef = useRef(setNoteIds);
|
|
516
|
+
setNoteIdsRef.current = setNoteIds;
|
|
517
|
+
const setSelectedIdRef = useRef(setSelectedId);
|
|
518
|
+
setSelectedIdRef.current = setSelectedId;
|
|
519
|
+
|
|
520
|
+
// Tool to add/remove notes (manages the list, not the note content)
|
|
521
|
+
useAssistantTool({
|
|
522
|
+
toolName: "manage_notes",
|
|
523
|
+
description:
|
|
524
|
+
'Manage sticky notes. Actions: "add" (creates a new note, returns its id), "remove" (requires noteId), "clear" (removes all notes). After adding, use the update_note_{id} tool to set its content.',
|
|
525
|
+
parameters: z.object({
|
|
526
|
+
action: z.enum(["add", "remove", "clear"]),
|
|
527
|
+
noteId: z.string().optional(),
|
|
528
|
+
}),
|
|
529
|
+
execute: async (args) => {
|
|
530
|
+
switch (args.action) {
|
|
531
|
+
case "add": {
|
|
532
|
+
const id = `note-${Date.now().toString(36)}`;
|
|
533
|
+
setNoteIdsRef.current((prev) => [...prev, id]);
|
|
534
|
+
return { success: true, noteId: id };
|
|
535
|
+
}
|
|
536
|
+
case "remove": {
|
|
537
|
+
if (args.noteId) {
|
|
538
|
+
setNoteIdsRef.current((prev) =>
|
|
539
|
+
prev.filter((id) => id !== args.noteId),
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
return { success: true };
|
|
543
|
+
}
|
|
544
|
+
case "clear": {
|
|
545
|
+
setNoteIdsRef.current([]);
|
|
546
|
+
setSelectedIdRef.current(null);
|
|
547
|
+
return { success: true };
|
|
548
|
+
}
|
|
549
|
+
default:
|
|
550
|
+
return { success: false, error: "Unknown action" };
|
|
551
|
+
}
|
|
552
|
+
},
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
const handleSelect = useCallback((id: string) => {
|
|
556
|
+
setSelectedId(id);
|
|
557
|
+
}, []);
|
|
558
|
+
|
|
559
|
+
const handleRemove = useCallback((id: string) => {
|
|
560
|
+
setNoteIds((prev) => prev.filter((n) => n !== id));
|
|
561
|
+
setSelectedId((prev) => (prev === id ? null : prev));
|
|
562
|
+
}, []);
|
|
563
|
+
|
|
564
|
+
return (
|
|
565
|
+
<div className="flex flex-col">
|
|
566
|
+
<div className="flex items-center gap-2 border-b px-4 py-3">
|
|
567
|
+
<StickyNoteIcon className="size-4 text-muted-foreground" />
|
|
568
|
+
<span className="font-semibold text-sm">Notes</span>
|
|
569
|
+
<span className="ml-auto text-muted-foreground text-xs">
|
|
570
|
+
{noteIds.length}
|
|
571
|
+
</span>
|
|
572
|
+
<button
|
|
573
|
+
type="button"
|
|
574
|
+
onClick={() => {
|
|
575
|
+
const id = `note-${Date.now().toString(36)}`;
|
|
576
|
+
setNoteIds((prev) => [...prev, id]);
|
|
577
|
+
}}
|
|
578
|
+
className="rounded p-1 transition-colors hover:bg-muted"
|
|
579
|
+
>
|
|
580
|
+
<PlusIcon className="size-3.5 text-muted-foreground" />
|
|
581
|
+
</button>
|
|
582
|
+
</div>
|
|
583
|
+
<div className="flex-1 overflow-y-auto p-3">
|
|
584
|
+
{noteIds.length === 0 ? (
|
|
585
|
+
<p className="py-6 text-center text-muted-foreground text-xs">
|
|
586
|
+
No notes yet. Ask the assistant!
|
|
587
|
+
</p>
|
|
588
|
+
) : (
|
|
589
|
+
<div className="grid gap-2">
|
|
590
|
+
{noteIds.map((noteId) => (
|
|
591
|
+
<NoteCard
|
|
592
|
+
key={noteId}
|
|
593
|
+
noteId={noteId}
|
|
594
|
+
selectedId={selectedId}
|
|
595
|
+
onSelect={handleSelect}
|
|
596
|
+
onRemove={handleRemove}
|
|
597
|
+
/>
|
|
598
|
+
))}
|
|
599
|
+
</div>
|
|
600
|
+
)}
|
|
601
|
+
</div>
|
|
602
|
+
</div>
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// ===========================================================================
|
|
607
|
+
// App
|
|
608
|
+
// ===========================================================================
|
|
609
|
+
|
|
610
|
+
const STORAGE_KEY = "interactables-example";
|
|
611
|
+
|
|
612
|
+
function useInteractablePersistence(aui: ReturnType<typeof useAui>) {
|
|
613
|
+
useEffect(() => {
|
|
614
|
+
const saved = localStorage.getItem(STORAGE_KEY);
|
|
615
|
+
if (saved) {
|
|
616
|
+
try {
|
|
617
|
+
aui.interactables().importState(JSON.parse(saved));
|
|
618
|
+
} catch {
|
|
619
|
+
// ignore malformed data
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
aui.interactables().setPersistenceAdapter({
|
|
624
|
+
save: (state) => {
|
|
625
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
|
626
|
+
},
|
|
627
|
+
});
|
|
628
|
+
}, [aui]);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
export default function Home() {
|
|
632
|
+
const runtime = useChatRuntime({
|
|
633
|
+
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
const aui = useAui({
|
|
637
|
+
interactables: Interactables(),
|
|
638
|
+
suggestions: Suggestions([
|
|
639
|
+
{
|
|
640
|
+
title: "Add 3 tasks",
|
|
641
|
+
label: "for a grocery run",
|
|
642
|
+
prompt: "Add 3 tasks for a grocery run",
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
title: "Create 2 notes",
|
|
646
|
+
label: "and set different colors",
|
|
647
|
+
prompt:
|
|
648
|
+
"Create 2 sticky notes: one blue note about meeting prep, and one green note about project ideas",
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
title: "Change selected note",
|
|
652
|
+
label: "to pink color",
|
|
653
|
+
prompt: "Change the selected note's color to pink",
|
|
654
|
+
},
|
|
655
|
+
]),
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
useInteractablePersistence(aui);
|
|
659
|
+
|
|
660
|
+
return (
|
|
661
|
+
<AssistantRuntimeProvider aui={aui} runtime={runtime}>
|
|
662
|
+
<main className="flex h-full">
|
|
663
|
+
<div className="flex-1">
|
|
664
|
+
<Thread />
|
|
665
|
+
</div>
|
|
666
|
+
<div className="flex w-80 flex-col border-l">
|
|
667
|
+
<div className="flex-1 overflow-y-auto">
|
|
668
|
+
<NotesPanel />
|
|
669
|
+
</div>
|
|
670
|
+
<div className="flex-1 overflow-y-auto border-t">
|
|
671
|
+
<TaskBoard />
|
|
672
|
+
</div>
|
|
673
|
+
</div>
|
|
674
|
+
</main>
|
|
675
|
+
</AssistantRuntimeProvider>
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
## components.json
|
|
682
|
+
|
|
683
|
+
```json
|
|
684
|
+
{
|
|
685
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
686
|
+
"style": "new-york",
|
|
687
|
+
"rsc": true,
|
|
688
|
+
"tsx": true,
|
|
689
|
+
"tailwind": {
|
|
690
|
+
"config": "",
|
|
691
|
+
"css": "app/globals.css",
|
|
692
|
+
"baseColor": "zinc",
|
|
693
|
+
"cssVariables": true,
|
|
694
|
+
"prefix": ""
|
|
695
|
+
},
|
|
696
|
+
"aliases": {
|
|
697
|
+
"components": "@/components",
|
|
698
|
+
"utils": "@/lib/utils",
|
|
699
|
+
"ui": "@/components/ui",
|
|
700
|
+
"lib": "@/lib",
|
|
701
|
+
"hooks": "@/hooks"
|
|
702
|
+
},
|
|
703
|
+
"iconLibrary": "lucide",
|
|
704
|
+
"registries": {
|
|
705
|
+
"@assistant-ui": "https://r.assistant-ui.com/{name}.json"
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
## next.config.ts
|
|
712
|
+
|
|
713
|
+
```typescript
|
|
714
|
+
import type { NextConfig } from "next";
|
|
715
|
+
|
|
716
|
+
const nextConfig: NextConfig = {
|
|
717
|
+
/* config options here */
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
export default nextConfig;
|
|
721
|
+
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
## package.json
|
|
725
|
+
|
|
726
|
+
```json
|
|
727
|
+
{
|
|
728
|
+
"name": "with-interactables",
|
|
729
|
+
"version": "0.0.0",
|
|
730
|
+
"private": true,
|
|
731
|
+
"type": "module",
|
|
732
|
+
"scripts": {
|
|
733
|
+
"dev": "next dev",
|
|
734
|
+
"build": "next build",
|
|
735
|
+
"start": "next start"
|
|
736
|
+
},
|
|
737
|
+
"dependencies": {
|
|
738
|
+
"@ai-sdk/openai": "^3.0.50",
|
|
739
|
+
"@assistant-ui/react": "workspace:*",
|
|
740
|
+
"@assistant-ui/react-ai-sdk": "workspace:*",
|
|
741
|
+
"@assistant-ui/ui": "workspace:*",
|
|
742
|
+
"ai": "^6.0.144",
|
|
743
|
+
"class-variance-authority": "^0.7.1",
|
|
744
|
+
"clsx": "^2.1.1",
|
|
745
|
+
"lucide-react": "^1.7.0",
|
|
746
|
+
"next": "^16.2.2",
|
|
747
|
+
"react": "^19.2.4",
|
|
748
|
+
"react-dom": "^19.2.4",
|
|
749
|
+
"tailwind-merge": "^3.5.0",
|
|
750
|
+
"zod": "^4.3.6"
|
|
751
|
+
},
|
|
752
|
+
"devDependencies": {
|
|
753
|
+
"@assistant-ui/x-buildutils": "workspace:*",
|
|
754
|
+
"@tailwindcss/postcss": "^4.2.2",
|
|
755
|
+
"@types/node": "^25.5.0",
|
|
756
|
+
"@types/react": "^19.2.14",
|
|
757
|
+
"@types/react-dom": "^19.2.3",
|
|
758
|
+
"postcss": "^8.5.8",
|
|
759
|
+
"tailwindcss": "^4.2.2",
|
|
760
|
+
"tw-animate-css": "^1.4.0",
|
|
761
|
+
"typescript": "5.9.3"
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
## README.md
|
|
768
|
+
|
|
769
|
+
```markdown
|
|
770
|
+
# with-interactables
|
|
771
|
+
|
|
772
|
+
Demonstrates **interactable components** — persistent UI components whose state can be read and updated by both the user and the AI assistant.
|
|
773
|
+
|
|
774
|
+
## Features Demonstrated
|
|
775
|
+
|
|
776
|
+
### Task Board (single instance + custom tool)
|
|
777
|
+
- `useInteractable("taskBoard", config)` — registers a single interactable
|
|
778
|
+
- `useAssistantTool("manage_tasks")` — custom tool for incremental add/toggle/remove/clear
|
|
779
|
+
- Auto-generated `update_taskBoard` tool with **partial updates** (AI only sends changed fields)
|
|
780
|
+
|
|
781
|
+
### Sticky Notes (multi-instance + selection + partial updates)
|
|
782
|
+
- Multiple `<NoteCard>` components each call `useInteractable("note", { id: noteId, ... })`
|
|
783
|
+
- **Multi-instance**: each note gets its own `update_note_{id}` tool automatically
|
|
784
|
+
- **Selection**: click a note to select it; AI sees `(SELECTED)` in system prompt and prioritizes it
|
|
785
|
+
- **Partial updates**: AI can change just `{ color: "pink" }` without resending title and content
|
|
786
|
+
|
|
787
|
+
## Getting Started
|
|
788
|
+
|
|
789
|
+
```bash
|
|
790
|
+
# Install dependencies (from monorepo root)
|
|
791
|
+
pnpm install
|
|
792
|
+
|
|
793
|
+
# Set your OpenAI API key
|
|
794
|
+
cp .env.example .env.local
|
|
795
|
+
# Edit .env.local and add your OPENAI_API_KEY
|
|
796
|
+
|
|
797
|
+
# Run the development server
|
|
798
|
+
pnpm --filter with-interactables dev
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
Open [http://localhost:3000](http://localhost:3000) to see the example.
|
|
802
|
+
|
|
803
|
+
## Key Concepts
|
|
804
|
+
|
|
805
|
+
- **`Interactables()`** — scope resource registered via `useAui`
|
|
806
|
+
- **`useInteractable(name, config)`** — returns `[state, setState, { id, setSelected }]`
|
|
807
|
+
- **Partial updates** — auto-generated tools use partial schemas; AI only sends changed fields
|
|
808
|
+
- **Multi-instance** — same `name`, different `id`; tools named `update_{name}_{id}`
|
|
809
|
+
- **Selection** — `setSelected(true)` marks a component as focused for the AI
|
|
810
|
+
- **`useAssistantTool`** — custom frontend tools for fine-grained control
|
|
811
|
+
- **`sendAutomaticallyWhen`** — auto-sends follow-up messages when tool calls complete
|
|
812
|
+
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
## tsconfig.json
|
|
816
|
+
|
|
817
|
+
```json
|
|
818
|
+
{
|
|
819
|
+
"extends": "@assistant-ui/x-buildutils/ts/next",
|
|
820
|
+
"compilerOptions": {
|
|
821
|
+
"paths": {
|
|
822
|
+
"@/*": ["./*"],
|
|
823
|
+
"@/components/assistant-ui/*": [
|
|
824
|
+
"../../packages/ui/src/components/assistant-ui/*"
|
|
825
|
+
],
|
|
826
|
+
"@/components/ui/*": ["../../packages/ui/src/components/ui/*"],
|
|
827
|
+
"@/lib/utils": ["../../packages/ui/src/lib/utils"],
|
|
828
|
+
"@assistant-ui/ui/*": ["../../packages/ui/src/*"]
|
|
829
|
+
}
|
|
830
|
+
},
|
|
831
|
+
"include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
832
|
+
"exclude": ["node_modules"]
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
```
|
|
836
|
+
|