@assistant-ui/mcp-docs-server 0.1.35 → 0.1.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.docs/organized/code-examples/waterfall.md +1 -1
- package/.docs/organized/code-examples/with-a2a.md +2 -2
- package/.docs/organized/code-examples/with-ag-ui.md +2 -2
- package/.docs/organized/code-examples/with-ai-sdk-v6.md +4 -4
- package/.docs/organized/code-examples/with-artifacts.md +4 -4
- package/.docs/organized/code-examples/with-assistant-transport.md +70 -54
- package/.docs/organized/code-examples/with-browser-extension.md +4 -4
- package/.docs/organized/code-examples/with-chain-of-thought.md +40 -89
- package/.docs/organized/code-examples/with-cloud-standalone.md +4 -4
- package/.docs/organized/code-examples/with-cloud.md +4 -4
- package/.docs/organized/code-examples/with-custom-thread-list.md +4 -4
- package/.docs/organized/code-examples/with-elevenlabs-conversational.md +6 -6
- package/.docs/organized/code-examples/with-elevenlabs-scribe.md +6 -6
- package/.docs/organized/code-examples/with-eve.md +343 -0
- package/.docs/organized/code-examples/with-expo.md +909 -932
- package/.docs/organized/code-examples/with-external-store.md +2 -2
- package/.docs/organized/code-examples/with-ffmpeg.md +5 -8
- package/.docs/organized/code-examples/with-generative-ui.md +5 -5
- package/.docs/organized/code-examples/with-google-adk.md +2 -2
- package/.docs/organized/code-examples/with-heat-graph.md +1 -1
- package/.docs/organized/code-examples/with-image-generation.md +4 -4
- package/.docs/organized/code-examples/with-interactables.md +166 -338
- package/.docs/organized/code-examples/with-langchain.md +4 -4
- package/.docs/organized/code-examples/with-langgraph.md +20 -157
- package/.docs/organized/code-examples/with-livekit.md +6 -6
- package/.docs/organized/code-examples/with-mcp.md +4 -4
- package/.docs/organized/code-examples/with-opencode.md +3 -3
- package/.docs/organized/code-examples/with-pi.md +9 -7
- package/.docs/organized/code-examples/with-react-hook-form.md +13 -6
- package/.docs/organized/code-examples/with-react-ink-web.md +5 -5
- package/.docs/organized/code-examples/with-react-ink.md +1 -1
- package/.docs/organized/code-examples/with-react-router.md +10 -10
- package/.docs/organized/code-examples/with-resumable-stream.md +5 -5
- package/.docs/organized/code-examples/with-store.md +1 -1
- package/.docs/organized/code-examples/with-tanstack.md +5 -5
- package/.docs/organized/code-examples/with-tap-runtime.md +2 -2
- package/.docs/organized/code-examples/with-virtualized-thread.md +44 -24
- package/.docs/raw/docs/(docs)/cli.mdx +4 -2
- package/.docs/raw/docs/(docs)/devtools.mdx +25 -2
- package/.docs/raw/docs/(docs)/index.mdx +5 -2
- package/.docs/raw/docs/(docs)/installation.mdx +5 -2
- package/.docs/raw/docs/(reference)/api-reference/external-store/message-conversion.mdx +16 -16
- package/.docs/raw/docs/(reference)/api-reference/hooks/composer-triggers.mdx +73 -42
- package/.docs/raw/docs/(reference)/api-reference/hooks/model-context.mdx +3 -20
- package/.docs/raw/docs/(reference)/api-reference/hooks/primitives.mdx +234 -166
- package/.docs/raw/docs/(reference)/api-reference/integrations/eve.mdx +51 -0
- package/.docs/raw/docs/(reference)/api-reference/integrations/index.mdx +4 -1
- package/.docs/raw/docs/(reference)/api-reference/integrations/react-ai-sdk.mdx +70 -38
- package/.docs/raw/docs/(reference)/api-reference/primitives/action-bar.mdx +30 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/attachment.mdx +7 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/branch-picker.mdx +26 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/chain-of-thought.mdx +15 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/composer.mdx +103 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/message-part.mdx +16 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/message.mdx +56 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/queue-item.mdx +12 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/selection-toolbar.mdx +12 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/suggestion.mdx +14 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/thread.mdx +57 -1
- package/.docs/raw/docs/(reference)/api-reference/tools/index.mdx +6 -0
- package/.docs/raw/docs/(reference)/api-reference/tools/interactables-legacy.mdx +55 -0
- package/.docs/raw/docs/(reference)/api-reference/tools/interactables.mdx +151 -0
- package/.docs/raw/docs/(reference)/api-reference/tools/rendering.mdx +15 -15
- package/.docs/raw/docs/(reference)/api-reference/tools/toolkits.mdx +20 -20
- package/.docs/raw/docs/(reference)/api-reference/utilities/miscellaneous.mdx +25 -9
- package/.docs/raw/docs/guides/headless-composer-input.mdx +113 -0
- package/.docs/raw/docs/guides/latex.mdx +28 -22
- package/.docs/raw/docs/guides/mentions.mdx +32 -7
- package/.docs/raw/docs/guides/slash-commands.mdx +3 -6
- package/.docs/raw/docs/guides/speech.mdx +5 -7
- package/.docs/raw/docs/guides/virtualization.mdx +77 -7
- package/.docs/raw/docs/guides/voice.mdx +3 -2
- package/.docs/raw/docs/integrations/attachments/custom-adapter.mdx +4 -12
- package/.docs/raw/docs/integrations/auth/better-auth.mdx +7 -10
- package/.docs/raw/docs/integrations/auth/clerk.mdx +3 -9
- package/.docs/raw/docs/integrations/auth/next-auth.mdx +3 -8
- package/.docs/raw/docs/integrations/frameworks/ai-sdk.mdx +8 -5
- package/.docs/raw/docs/integrations/index.mdx +5 -12
- package/.docs/raw/docs/integrations/observability/helicone.mdx +3 -4
- package/.docs/raw/docs/integrations/observability/langfuse.mdx +3 -5
- package/.docs/raw/docs/integrations/observability/langsmith.mdx +3 -2
- package/.docs/raw/docs/integrations/persistence/custom-adapter.mdx +4 -3
- package/.docs/raw/docs/primitives/composer.mdx +8 -0
- package/.docs/raw/docs/primitives/thread.mdx +24 -0
- package/.docs/raw/docs/runtimes/ag-ui/runtime-options.mdx +21 -0
- package/.docs/raw/docs/runtimes/custom/external-store.mdx +54 -0
- package/.docs/raw/docs/runtimes/eve/overview.mdx +100 -0
- package/.docs/raw/docs/runtimes/eve/quickstart.mdx +143 -0
- package/.docs/raw/docs/runtimes/langchain.mdx +386 -18
- package/.docs/raw/docs/runtimes/langgraph/quickstart.mdx +1 -1
- package/.docs/raw/docs/runtimes/langgraph/tutorial/part-1.mdx +1 -1
- package/.docs/raw/docs/runtimes/pick-a-runtime.mdx +6 -0
- package/.docs/raw/docs/tools/defining-tools.mdx +4 -0
- package/.docs/raw/docs/tools/interactables-legacy.mdx +410 -0
- package/.docs/raw/docs/tools/interactables.mdx +892 -223
- package/.docs/raw/docs/tools/mcp.mdx +4 -4
- package/.docs/raw/docs/tools/multi-agent.mdx +5 -0
- package/.docs/raw/docs/ui/composer-trigger-popover.mdx +2 -0
- package/.docs/raw/docs/ui/file.mdx +1 -1
- package/.docs/raw/docs/ui/part-grouping.mdx +38 -0
- package/.docs/raw/docs/utilities/react-o11y.mdx +92 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -2
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +14 -6
- package/src/tools/tests/mcp-protocol.test.ts +9 -0
|
@@ -4,17 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
```typescript
|
|
6
6
|
import { openai } from "@ai-sdk/openai";
|
|
7
|
-
import {
|
|
8
|
-
streamText,
|
|
9
|
-
convertToModelMessages,
|
|
10
|
-
stepCountIs,
|
|
11
|
-
jsonSchema,
|
|
12
|
-
} from "ai";
|
|
7
|
+
import { streamText, convertToModelMessages, stepCountIs } from "ai";
|
|
13
8
|
import type { UIMessage } from "ai";
|
|
9
|
+
import {
|
|
10
|
+
frontendTools,
|
|
11
|
+
unstable_injectInteractableContext,
|
|
12
|
+
} from "@assistant-ui/react-ai-sdk";
|
|
14
13
|
|
|
15
14
|
export const maxDuration = 30;
|
|
16
15
|
|
|
17
|
-
type
|
|
16
|
+
type FrontendTools = Parameters<typeof frontendTools>[0];
|
|
18
17
|
|
|
19
18
|
export async function POST(req: Request) {
|
|
20
19
|
const {
|
|
@@ -24,30 +23,19 @@ export async function POST(req: Request) {
|
|
|
24
23
|
}: {
|
|
25
24
|
messages: UIMessage[];
|
|
26
25
|
system?: string;
|
|
27
|
-
tools?:
|
|
26
|
+
tools?: FrontendTools;
|
|
28
27
|
} = await req.json();
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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;
|
|
29
|
+
const modelMessages = await convertToModelMessages(
|
|
30
|
+
unstable_injectInteractableContext(messages),
|
|
31
|
+
);
|
|
44
32
|
|
|
45
33
|
const result = streamText({
|
|
46
34
|
model: openai("gpt-5.4-nano"),
|
|
47
|
-
messages:
|
|
35
|
+
messages: modelMessages,
|
|
48
36
|
stopWhen: stepCountIs(10),
|
|
49
37
|
...(system ? { system } : {}),
|
|
50
|
-
...(
|
|
38
|
+
...(clientTools ? { tools: frontendTools(clientTools) } : {}),
|
|
51
39
|
} as Parameters<typeof streamText>[0]);
|
|
52
40
|
|
|
53
41
|
return result.toUIMessageStreamResponse();
|
|
@@ -214,25 +202,15 @@ export default function RootLayout({
|
|
|
214
202
|
```tsx
|
|
215
203
|
"use client";
|
|
216
204
|
|
|
217
|
-
import {
|
|
218
|
-
useRef,
|
|
219
|
-
useState,
|
|
220
|
-
useCallback,
|
|
221
|
-
useEffect,
|
|
222
|
-
type Dispatch,
|
|
223
|
-
type SetStateAction,
|
|
224
|
-
} from "react";
|
|
205
|
+
import { useCallback } from "react";
|
|
225
206
|
import { Thread } from "@/components/assistant-ui/thread";
|
|
226
207
|
import {
|
|
227
208
|
AssistantRuntimeProvider,
|
|
228
209
|
AuiProvider,
|
|
229
|
-
|
|
210
|
+
unstable_Interactables,
|
|
230
211
|
Suggestions,
|
|
231
|
-
Tools,
|
|
232
212
|
useAui,
|
|
233
|
-
|
|
234
|
-
useAssistantInteractable,
|
|
235
|
-
useInteractableState,
|
|
213
|
+
unstable_useInteractable,
|
|
236
214
|
} from "@assistant-ui/react";
|
|
237
215
|
import { useChatRuntime } from "@assistant-ui/react-ai-sdk";
|
|
238
216
|
import { lastAssistantMessageIsCompleteWithToolCalls } from "ai";
|
|
@@ -247,34 +225,29 @@ import {
|
|
|
247
225
|
} from "lucide-react";
|
|
248
226
|
import {
|
|
249
227
|
type NoteState,
|
|
250
|
-
type TaskBoardState,
|
|
251
228
|
noteInitialState,
|
|
252
|
-
|
|
229
|
+
notesInitialState,
|
|
230
|
+
notesSchema,
|
|
253
231
|
taskBoardInitialState,
|
|
254
232
|
taskBoardSchema,
|
|
255
233
|
} from "./state";
|
|
256
|
-
import toolkit from "./toolkits";
|
|
257
234
|
|
|
258
235
|
function TaskBoard() {
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
taskBoardInitialState,
|
|
236
|
+
const [state, { setState, isPending }] = unstable_useInteractable(
|
|
237
|
+
"taskBoard",
|
|
238
|
+
{
|
|
239
|
+
description:
|
|
240
|
+
"A task board showing the user's tasks. Use update_taskBoard with tasks.add/update/remove/clear to manage tasks. New tasks need a title and done=false.",
|
|
241
|
+
stateSchema: taskBoardSchema,
|
|
242
|
+
initialState: taskBoardInitialState,
|
|
243
|
+
},
|
|
268
244
|
);
|
|
269
245
|
|
|
270
|
-
const aui = useAui({ tools: Tools({ toolkit }) });
|
|
271
|
-
|
|
272
246
|
const doneCount = state.tasks.filter((t) => t.done).length;
|
|
273
247
|
|
|
274
248
|
return (
|
|
275
|
-
|
|
276
|
-
<
|
|
277
|
-
<div className="flex flex-col">
|
|
249
|
+
<>
|
|
250
|
+
<div className="flex h-full flex-col">
|
|
278
251
|
<div className="flex items-center gap-2 border-b px-4 py-3">
|
|
279
252
|
<ListTodoIcon className="text-muted-foreground size-4" />
|
|
280
253
|
<span className="text-sm font-semibold">Task Board</span>
|
|
@@ -294,9 +267,9 @@ function TaskBoard() {
|
|
|
294
267
|
</p>
|
|
295
268
|
) : (
|
|
296
269
|
<ul className="space-y-1">
|
|
297
|
-
{state.tasks.map((task) => (
|
|
270
|
+
{state.tasks.map((task, index) => (
|
|
298
271
|
<li
|
|
299
|
-
key={task.id}
|
|
272
|
+
key={task.id ?? `streaming-${index}`}
|
|
300
273
|
className="group hover:bg-muted flex items-center gap-2 rounded-lg px-3 py-2 transition-colors"
|
|
301
274
|
>
|
|
302
275
|
<button
|
|
@@ -309,6 +282,9 @@ function TaskBoard() {
|
|
|
309
282
|
}))
|
|
310
283
|
}
|
|
311
284
|
className="shrink-0"
|
|
285
|
+
aria-label={
|
|
286
|
+
task.done ? "Mark task undone" : "Mark task done"
|
|
287
|
+
}
|
|
312
288
|
>
|
|
313
289
|
{task.done ? (
|
|
314
290
|
<CheckCircle2Icon className="text-primary size-4" />
|
|
@@ -329,6 +305,7 @@ function TaskBoard() {
|
|
|
329
305
|
}))
|
|
330
306
|
}
|
|
331
307
|
className="shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
|
|
308
|
+
aria-label={`Remove task ${task.title}`}
|
|
332
309
|
>
|
|
333
310
|
<Trash2Icon className="text-muted-foreground hover:text-destructive size-3.5" />
|
|
334
311
|
</button>
|
|
@@ -338,59 +315,11 @@ function TaskBoard() {
|
|
|
338
315
|
)}
|
|
339
316
|
</div>
|
|
340
317
|
</div>
|
|
341
|
-
|
|
318
|
+
</>
|
|
342
319
|
);
|
|
343
320
|
}
|
|
344
321
|
|
|
345
|
-
|
|
346
|
-
setState,
|
|
347
|
-
}: {
|
|
348
|
-
setState: Dispatch<SetStateAction<TaskBoardState>>;
|
|
349
|
-
}) {
|
|
350
|
-
useAuiToolOverrides({
|
|
351
|
-
manage_tasks: {
|
|
352
|
-
execute: async (args) => {
|
|
353
|
-
switch (args.action) {
|
|
354
|
-
case "add": {
|
|
355
|
-
const id = crypto.randomUUID();
|
|
356
|
-
setState((prev) => ({
|
|
357
|
-
tasks: [
|
|
358
|
-
...prev.tasks,
|
|
359
|
-
{ id, title: args.title ?? "Untitled", done: false },
|
|
360
|
-
],
|
|
361
|
-
}));
|
|
362
|
-
return { success: true, id };
|
|
363
|
-
}
|
|
364
|
-
case "toggle": {
|
|
365
|
-
if (!args.id) return { success: false, error: "id is required" };
|
|
366
|
-
setState((prev) => ({
|
|
367
|
-
tasks: prev.tasks.map((t) =>
|
|
368
|
-
t.id === args.id ? { ...t, done: !t.done } : t,
|
|
369
|
-
),
|
|
370
|
-
}));
|
|
371
|
-
return { success: true };
|
|
372
|
-
}
|
|
373
|
-
case "remove": {
|
|
374
|
-
if (!args.id) return { success: false, error: "id is required" };
|
|
375
|
-
setState((prev) => ({
|
|
376
|
-
tasks: prev.tasks.filter((t) => t.id !== args.id),
|
|
377
|
-
}));
|
|
378
|
-
return { success: true };
|
|
379
|
-
}
|
|
380
|
-
case "clear": {
|
|
381
|
-
setState({ tasks: [] });
|
|
382
|
-
return { success: true };
|
|
383
|
-
}
|
|
384
|
-
default:
|
|
385
|
-
return { success: false, error: "Unknown action" };
|
|
386
|
-
}
|
|
387
|
-
},
|
|
388
|
-
},
|
|
389
|
-
});
|
|
390
|
-
return null;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const COLORS: Record<string, string> = {
|
|
322
|
+
const COLORS: Record<NoteState["color"], string> = {
|
|
394
323
|
yellow: "bg-yellow-100 border-yellow-300",
|
|
395
324
|
blue: "bg-blue-100 border-blue-300",
|
|
396
325
|
green: "bg-green-100 border-green-300",
|
|
@@ -398,62 +327,41 @@ const COLORS: Record<string, string> = {
|
|
|
398
327
|
};
|
|
399
328
|
|
|
400
329
|
function NoteCard({
|
|
401
|
-
|
|
402
|
-
|
|
330
|
+
note,
|
|
331
|
+
selected,
|
|
403
332
|
onSelect,
|
|
404
333
|
onRemove,
|
|
405
334
|
}: {
|
|
406
|
-
|
|
407
|
-
|
|
335
|
+
note: NoteState;
|
|
336
|
+
selected: boolean;
|
|
408
337
|
onSelect: (id: string) => void;
|
|
409
338
|
onRemove: (id: string) => void;
|
|
410
339
|
}) {
|
|
411
|
-
useAssistantInteractable("note", {
|
|
412
|
-
id: noteId,
|
|
413
|
-
description:
|
|
414
|
-
"A sticky note. The AI can partially update any field (title, content, color) without resending the others.",
|
|
415
|
-
stateSchema: noteSchema,
|
|
416
|
-
initialState: noteInitialState,
|
|
417
|
-
selected: selectedId === noteId,
|
|
418
|
-
});
|
|
419
|
-
const [state, { setSelected }] = useInteractableState<NoteState>(
|
|
420
|
-
noteId,
|
|
421
|
-
noteInitialState,
|
|
422
|
-
);
|
|
423
|
-
|
|
424
|
-
const isSelected = selectedId === noteId;
|
|
425
|
-
|
|
426
|
-
const handleClick = () => {
|
|
427
|
-
onSelect(noteId);
|
|
428
|
-
setSelected(true);
|
|
429
|
-
};
|
|
430
|
-
|
|
431
340
|
return (
|
|
432
|
-
<div
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
341
|
+
<div className="group relative">
|
|
342
|
+
<button
|
|
343
|
+
type="button"
|
|
344
|
+
onClick={() => onSelect(note.id)}
|
|
345
|
+
className={`flex w-full cursor-pointer flex-col gap-1 rounded-lg border-2 p-3 text-left transition-all ${COLORS[note.color] ?? COLORS.yellow} ${selected ? "ring-primary ring-2 ring-offset-1" : "hover:shadow-sm"}`}
|
|
346
|
+
aria-pressed={selected}
|
|
347
|
+
>
|
|
348
|
+
{selected && (
|
|
349
|
+
<span className="bg-primary text-primary-foreground absolute top-1.5 right-2 rounded px-1.5 py-0.5 text-[10px] leading-none font-medium">
|
|
350
|
+
SELECTED
|
|
351
|
+
</span>
|
|
352
|
+
)}
|
|
353
|
+
<span className="pr-16 text-sm font-semibold text-zinc-800">
|
|
354
|
+
{note.title}
|
|
355
|
+
</span>
|
|
356
|
+
<span className="line-clamp-3 text-xs text-zinc-600">
|
|
357
|
+
{note.content || "Empty note"}
|
|
442
358
|
</span>
|
|
443
|
-
|
|
444
|
-
<span className="pr-16 text-sm font-semibold text-zinc-800">
|
|
445
|
-
{state.title}
|
|
446
|
-
</span>
|
|
447
|
-
<span className="line-clamp-3 text-xs text-zinc-600">
|
|
448
|
-
{state.content || "Empty note"}
|
|
449
|
-
</span>
|
|
359
|
+
</button>
|
|
450
360
|
<button
|
|
451
361
|
type="button"
|
|
452
|
-
onClick={(
|
|
453
|
-
e.stopPropagation();
|
|
454
|
-
onRemove(noteId);
|
|
455
|
-
}}
|
|
362
|
+
onClick={() => onRemove(note.id)}
|
|
456
363
|
className="absolute right-2 bottom-2 rounded p-0.5 opacity-0 transition-opacity group-hover:opacity-100 hover:bg-black/10"
|
|
364
|
+
aria-label={`Remove note ${note.title}`}
|
|
457
365
|
>
|
|
458
366
|
<Trash2Icon className="size-3 text-zinc-500" />
|
|
459
367
|
</button>
|
|
@@ -461,79 +369,69 @@ function NoteCard({
|
|
|
461
369
|
);
|
|
462
370
|
}
|
|
463
371
|
|
|
464
|
-
const NOTE_IDS_KEY = "interactables-example-note-ids";
|
|
465
|
-
|
|
466
|
-
function loadNoteIds(): string[] {
|
|
467
|
-
try {
|
|
468
|
-
const saved = localStorage.getItem(NOTE_IDS_KEY);
|
|
469
|
-
return saved ? JSON.parse(saved) : [];
|
|
470
|
-
} catch {
|
|
471
|
-
return [];
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
372
|
function NotesPanel() {
|
|
476
|
-
const [
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
hydratedRef.current = true;
|
|
483
|
-
const saved = loadNoteIds();
|
|
484
|
-
if (saved.length > 0) setNoteIds(saved);
|
|
485
|
-
return;
|
|
486
|
-
}
|
|
487
|
-
localStorage.setItem(NOTE_IDS_KEY, JSON.stringify(noteIds));
|
|
488
|
-
}, [noteIds]);
|
|
489
|
-
|
|
490
|
-
const aui = useAui({ tools: Tools({ toolkit }) });
|
|
373
|
+
const [state, { setState }] = unstable_useInteractable("notes", {
|
|
374
|
+
description:
|
|
375
|
+
"The sticky-note collection and selected note id. Use update_notes with notes.add/update/remove/clear to manage notes, and set selectedId to change focus. New notes need a title, content, and color.",
|
|
376
|
+
stateSchema: notesSchema,
|
|
377
|
+
initialState: notesInitialState,
|
|
378
|
+
});
|
|
491
379
|
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
380
|
+
const handleAdd = useCallback(() => {
|
|
381
|
+
const id = `note-${crypto.randomUUID()}`;
|
|
382
|
+
setState((prev) => ({
|
|
383
|
+
notes: [...prev.notes, { id, ...noteInitialState }],
|
|
384
|
+
selectedId: id,
|
|
385
|
+
}));
|
|
386
|
+
}, [setState]);
|
|
387
|
+
|
|
388
|
+
const handleSelect = useCallback(
|
|
389
|
+
(id: string) => {
|
|
390
|
+
setState((prev) => ({ ...prev, selectedId: id }));
|
|
391
|
+
},
|
|
392
|
+
[setState],
|
|
393
|
+
);
|
|
495
394
|
|
|
496
|
-
const handleRemove = useCallback(
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
395
|
+
const handleRemove = useCallback(
|
|
396
|
+
(id: string) => {
|
|
397
|
+
setState((prev) => ({
|
|
398
|
+
notes: prev.notes.filter((note) => note.id !== id),
|
|
399
|
+
selectedId: prev.selectedId === id ? null : prev.selectedId,
|
|
400
|
+
}));
|
|
401
|
+
},
|
|
402
|
+
[setState],
|
|
403
|
+
);
|
|
500
404
|
|
|
501
405
|
return (
|
|
502
|
-
|
|
503
|
-
<
|
|
504
|
-
setNoteIds={setNoteIds}
|
|
505
|
-
setSelectedId={setSelectedId}
|
|
506
|
-
/>
|
|
507
|
-
<div className="flex flex-col">
|
|
406
|
+
<>
|
|
407
|
+
<div className="flex h-full flex-col">
|
|
508
408
|
<div className="flex items-center gap-2 border-b px-4 py-3">
|
|
509
409
|
<StickyNoteIcon className="text-muted-foreground size-4" />
|
|
510
410
|
<span className="text-sm font-semibold">Notes</span>
|
|
511
411
|
<span className="text-muted-foreground ml-auto text-xs">
|
|
512
|
-
{
|
|
412
|
+
{state.notes.length}
|
|
513
413
|
</span>
|
|
514
414
|
<button
|
|
515
415
|
type="button"
|
|
516
|
-
onClick={
|
|
517
|
-
const id = `note-${crypto.randomUUID()}`;
|
|
518
|
-
setNoteIds((prev) => [...prev, id]);
|
|
519
|
-
}}
|
|
416
|
+
onClick={handleAdd}
|
|
520
417
|
className="hover:bg-muted rounded p-1 transition-colors"
|
|
418
|
+
aria-label="Add note"
|
|
521
419
|
>
|
|
522
420
|
<PlusIcon className="text-muted-foreground size-3.5" />
|
|
523
421
|
</button>
|
|
524
422
|
</div>
|
|
525
423
|
<div className="flex-1 overflow-y-auto p-3">
|
|
526
|
-
{
|
|
424
|
+
{state.notes.length === 0 ? (
|
|
527
425
|
<p className="text-muted-foreground py-6 text-center text-xs">
|
|
528
426
|
No notes yet. Ask the assistant!
|
|
529
427
|
</p>
|
|
530
428
|
) : (
|
|
531
429
|
<div className="grid gap-2">
|
|
532
|
-
{
|
|
430
|
+
{state.notes.map((note, index) => (
|
|
533
431
|
<NoteCard
|
|
534
|
-
key={
|
|
535
|
-
|
|
536
|
-
|
|
432
|
+
key={note.id ?? `streaming-${index}`}
|
|
433
|
+
note={note}
|
|
434
|
+
selected={state.selectedId === note.id}
|
|
537
435
|
onSelect={handleSelect}
|
|
538
436
|
onRemove={handleRemove}
|
|
539
437
|
/>
|
|
@@ -542,74 +440,32 @@ function NotesPanel() {
|
|
|
542
440
|
)}
|
|
543
441
|
</div>
|
|
544
442
|
</div>
|
|
545
|
-
|
|
443
|
+
</>
|
|
546
444
|
);
|
|
547
445
|
}
|
|
548
446
|
|
|
549
|
-
function NotesToolOverrides({
|
|
550
|
-
setNoteIds,
|
|
551
|
-
setSelectedId,
|
|
552
|
-
}: {
|
|
553
|
-
setNoteIds: Dispatch<SetStateAction<string[]>>;
|
|
554
|
-
setSelectedId: Dispatch<SetStateAction<string | null>>;
|
|
555
|
-
}) {
|
|
556
|
-
useAuiToolOverrides({
|
|
557
|
-
manage_notes: {
|
|
558
|
-
execute: async (args) => {
|
|
559
|
-
switch (args.action) {
|
|
560
|
-
case "add": {
|
|
561
|
-
const id = `note-${crypto.randomUUID()}`;
|
|
562
|
-
setNoteIds((prev) => [...prev, id]);
|
|
563
|
-
return { success: true, noteId: id };
|
|
564
|
-
}
|
|
565
|
-
case "remove": {
|
|
566
|
-
if (args.noteId) {
|
|
567
|
-
setNoteIds((prev) => prev.filter((id) => id !== args.noteId));
|
|
568
|
-
}
|
|
569
|
-
return { success: true };
|
|
570
|
-
}
|
|
571
|
-
case "clear": {
|
|
572
|
-
setNoteIds([]);
|
|
573
|
-
setSelectedId(null);
|
|
574
|
-
return { success: true };
|
|
575
|
-
}
|
|
576
|
-
default:
|
|
577
|
-
return { success: false, error: "Unknown action" };
|
|
578
|
-
}
|
|
579
|
-
},
|
|
580
|
-
},
|
|
581
|
-
});
|
|
582
|
-
return null;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
447
|
const STORAGE_KEY = "interactables-example";
|
|
586
448
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
}
|
|
449
|
+
// Module-level so the adapter identity is stable across renders.
|
|
450
|
+
const persistenceAdapter = {
|
|
451
|
+
load: () => {
|
|
452
|
+
try {
|
|
453
|
+
const saved = localStorage.getItem(STORAGE_KEY);
|
|
454
|
+
return saved ? JSON.parse(saved) : undefined;
|
|
455
|
+
} catch {
|
|
456
|
+
return undefined;
|
|
596
457
|
}
|
|
458
|
+
},
|
|
459
|
+
save: (state: unknown) => {
|
|
460
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
|
461
|
+
},
|
|
462
|
+
};
|
|
597
463
|
|
|
598
|
-
|
|
599
|
-
save: (state) => {
|
|
600
|
-
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
|
601
|
-
},
|
|
602
|
-
});
|
|
603
|
-
}, [aui]);
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
export default function Home() {
|
|
607
|
-
const runtime = useChatRuntime({
|
|
608
|
-
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
|
|
609
|
-
});
|
|
610
|
-
|
|
464
|
+
function InteractablesExample() {
|
|
611
465
|
const aui = useAui({
|
|
612
|
-
|
|
466
|
+
unstable_interactables: unstable_Interactables({
|
|
467
|
+
persistence: persistenceAdapter,
|
|
468
|
+
}),
|
|
613
469
|
suggestions: Suggestions([
|
|
614
470
|
{
|
|
615
471
|
title: "Add 3 tasks",
|
|
@@ -630,23 +486,33 @@ export default function Home() {
|
|
|
630
486
|
]),
|
|
631
487
|
});
|
|
632
488
|
|
|
633
|
-
useInteractablePersistence(aui);
|
|
634
|
-
|
|
635
489
|
return (
|
|
636
|
-
<
|
|
637
|
-
<main className="flex h-full">
|
|
638
|
-
<div className="flex-1">
|
|
490
|
+
<AuiProvider value={aui}>
|
|
491
|
+
<main className="flex h-full min-h-0">
|
|
492
|
+
<div className="min-w-0 flex-1">
|
|
639
493
|
<Thread />
|
|
640
494
|
</div>
|
|
641
495
|
<div className="flex w-80 flex-col border-l">
|
|
642
|
-
<div className="flex-1 overflow-y-auto">
|
|
496
|
+
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
643
497
|
<NotesPanel />
|
|
644
498
|
</div>
|
|
645
|
-
<div className="flex-1 overflow-y-auto border-t">
|
|
499
|
+
<div className="min-h-0 flex-1 overflow-y-auto border-t">
|
|
646
500
|
<TaskBoard />
|
|
647
501
|
</div>
|
|
648
502
|
</div>
|
|
649
503
|
</main>
|
|
504
|
+
</AuiProvider>
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export default function Home() {
|
|
509
|
+
const runtime = useChatRuntime({
|
|
510
|
+
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
return (
|
|
514
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
515
|
+
<InteractablesExample />
|
|
650
516
|
</AssistantRuntimeProvider>
|
|
651
517
|
);
|
|
652
518
|
}
|
|
@@ -664,7 +530,7 @@ export type TaskBoardState = { tasks: Task[] };
|
|
|
664
530
|
export const taskBoardSchema = z.object({
|
|
665
531
|
tasks: z.array(
|
|
666
532
|
z.object({
|
|
667
|
-
id: z.string(),
|
|
533
|
+
id: z.string().describe("Stable task id, unique within this board."),
|
|
668
534
|
title: z.string(),
|
|
669
535
|
done: z.boolean(),
|
|
670
536
|
}),
|
|
@@ -673,69 +539,34 @@ export const taskBoardSchema = z.object({
|
|
|
673
539
|
|
|
674
540
|
export const taskBoardInitialState: TaskBoardState = { tasks: [] };
|
|
675
541
|
|
|
676
|
-
export const
|
|
677
|
-
action: z.enum(["add", "toggle", "remove", "clear"]),
|
|
678
|
-
title: z.string().optional(),
|
|
679
|
-
id: z.string().optional(),
|
|
680
|
-
});
|
|
681
|
-
|
|
682
|
-
export type ManageTasksArgs = z.infer<typeof manageTasksParameters>;
|
|
683
|
-
|
|
684
|
-
export type NoteState = { title: string; content: string; color: string };
|
|
542
|
+
export const noteColorSchema = z.enum(["yellow", "blue", "green", "pink"]);
|
|
685
543
|
|
|
686
544
|
export const noteSchema = z.object({
|
|
545
|
+
id: z.string().describe("Stable note id, unique within this collection."),
|
|
687
546
|
title: z.string(),
|
|
688
547
|
content: z.string(),
|
|
689
|
-
color:
|
|
548
|
+
color: noteColorSchema,
|
|
690
549
|
});
|
|
691
550
|
|
|
692
|
-
export
|
|
551
|
+
export type NoteState = z.infer<typeof noteSchema>;
|
|
552
|
+
|
|
553
|
+
export const noteInitialState: Omit<NoteState, "id"> = {
|
|
693
554
|
title: "New Note",
|
|
694
555
|
content: "",
|
|
695
556
|
color: "yellow",
|
|
696
557
|
};
|
|
697
558
|
|
|
698
|
-
export const
|
|
699
|
-
|
|
700
|
-
|
|
559
|
+
export const notesSchema = z.object({
|
|
560
|
+
notes: z.array(noteSchema),
|
|
561
|
+
selectedId: z.string().nullable(),
|
|
701
562
|
});
|
|
702
563
|
|
|
703
|
-
export type
|
|
704
|
-
|
|
705
|
-
```
|
|
706
|
-
|
|
707
|
-
## app/toolkits.ts
|
|
564
|
+
export type NotesState = z.infer<typeof notesSchema>;
|
|
708
565
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
import { manageNotesParameters, manageTasksParameters } from "./state";
|
|
714
|
-
|
|
715
|
-
export default defineToolkit({
|
|
716
|
-
manage_tasks: {
|
|
717
|
-
description:
|
|
718
|
-
'Manage tasks on the task board. Actions: "add" (requires title), "toggle" (requires id), "remove" (requires id), "clear" (no extra fields).',
|
|
719
|
-
parameters: manageTasksParameters,
|
|
720
|
-
// The generative compiler strips this stub; TaskBoardToolOverrides provides the real executor.
|
|
721
|
-
execute: stubTool(),
|
|
722
|
-
renderText: {
|
|
723
|
-
running: ({ args }) => `Updating tasks: ${args.action}`,
|
|
724
|
-
complete: "Tasks updated",
|
|
725
|
-
},
|
|
726
|
-
},
|
|
727
|
-
manage_notes: {
|
|
728
|
-
description:
|
|
729
|
-
'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.',
|
|
730
|
-
parameters: manageNotesParameters,
|
|
731
|
-
// The generative compiler strips this stub; NotesToolOverrides provides the real executor.
|
|
732
|
-
execute: stubTool(),
|
|
733
|
-
renderText: {
|
|
734
|
-
running: ({ args }) => `Updating notes: ${args.action}`,
|
|
735
|
-
complete: "Notes updated",
|
|
736
|
-
},
|
|
737
|
-
},
|
|
738
|
-
});
|
|
566
|
+
export const notesInitialState: NotesState = {
|
|
567
|
+
notes: [],
|
|
568
|
+
selectedId: null,
|
|
569
|
+
};
|
|
739
570
|
|
|
740
571
|
```
|
|
741
572
|
|
|
@@ -772,14 +603,13 @@ export default defineToolkit({
|
|
|
772
603
|
## next.config.ts
|
|
773
604
|
|
|
774
605
|
```typescript
|
|
775
|
-
import { withAui } from "@assistant-ui/next";
|
|
776
606
|
import type { NextConfig } from "next";
|
|
777
607
|
|
|
778
608
|
const nextConfig: NextConfig = {
|
|
779
609
|
/* config options here */
|
|
780
610
|
};
|
|
781
611
|
|
|
782
|
-
export default
|
|
612
|
+
export default nextConfig;
|
|
783
613
|
|
|
784
614
|
```
|
|
785
615
|
|
|
@@ -797,14 +627,14 @@ export default withAui(nextConfig);
|
|
|
797
627
|
"start": "next start"
|
|
798
628
|
},
|
|
799
629
|
"dependencies": {
|
|
800
|
-
"@ai-sdk/openai": "^3.0.
|
|
630
|
+
"@ai-sdk/openai": "^3.0.73",
|
|
801
631
|
"@assistant-ui/react": "workspace:*",
|
|
802
632
|
"@assistant-ui/react-ai-sdk": "workspace:*",
|
|
803
633
|
"@assistant-ui/ui": "workspace:*",
|
|
804
|
-
"ai": "^6.0.
|
|
634
|
+
"ai": "^6.0.208",
|
|
805
635
|
"class-variance-authority": "^0.7.1",
|
|
806
636
|
"clsx": "^2.1.1",
|
|
807
|
-
"lucide-react": "^1.
|
|
637
|
+
"lucide-react": "^1.21.0",
|
|
808
638
|
"next": "^16.2.9",
|
|
809
639
|
"react": "^19.2.7",
|
|
810
640
|
"react-dom": "^19.2.7",
|
|
@@ -812,10 +642,9 @@ export default withAui(nextConfig);
|
|
|
812
642
|
"zod": "^4.4.3"
|
|
813
643
|
},
|
|
814
644
|
"devDependencies": {
|
|
815
|
-
"@assistant-ui/next": "workspace:*",
|
|
816
645
|
"@assistant-ui/x-buildutils": "workspace:*",
|
|
817
646
|
"@tailwindcss/postcss": "^4.3.1",
|
|
818
|
-
"@types/node": "^
|
|
647
|
+
"@types/node": "^26.0.0",
|
|
819
648
|
"@types/react": "^19.2.17",
|
|
820
649
|
"@types/react-dom": "^19.2.3",
|
|
821
650
|
"postcss": "^8.5.15",
|
|
@@ -836,16 +665,16 @@ Demonstrates **interactable components** — persistent UI components whose stat
|
|
|
836
665
|
|
|
837
666
|
## Features Demonstrated
|
|
838
667
|
|
|
839
|
-
### Task Board
|
|
840
|
-
- `
|
|
841
|
-
- `
|
|
842
|
-
-
|
|
668
|
+
### Task Board
|
|
669
|
+
- `unstable_useInteractable("taskBoard", config)` — registers a single interactable
|
|
670
|
+
- Auto-generated `update_taskBoard` tool with partial scalar updates and array operations
|
|
671
|
+
- Collection edits go through `tasks.add`, `tasks.update`, `tasks.remove`, and `tasks.clear` on the generated update tool
|
|
843
672
|
|
|
844
|
-
### Sticky Notes
|
|
845
|
-
-
|
|
846
|
-
-
|
|
847
|
-
- **Selection**: click a note to
|
|
848
|
-
- **Partial updates**: AI can change just `{ color: "pink" }`
|
|
673
|
+
### Sticky Notes
|
|
674
|
+
- `unstable_useInteractable("notes", config)` — registers the sticky-note collection and `selectedId` as one state object
|
|
675
|
+
- Auto-generated `update_notes` manages both note collection edits and selected-note focus
|
|
676
|
+
- **Selection**: click a note to set `selectedId`; the AI can change focus by updating the same normal state field
|
|
677
|
+
- **Partial updates**: AI can change just one note field with `notes.update: [{ id, color: "pink" }]`
|
|
849
678
|
|
|
850
679
|
## Getting Started
|
|
851
680
|
|
|
@@ -865,12 +694,11 @@ Open [http://localhost:3000](http://localhost:3000) to see the example.
|
|
|
865
694
|
|
|
866
695
|
## Key Concepts
|
|
867
696
|
|
|
868
|
-
- **`
|
|
869
|
-
- **`
|
|
697
|
+
- **`unstable_Interactables({ persistence })`** — scope resource registered via `useAui`, with a `load`/`save` adapter
|
|
698
|
+
- **`unstable_useInteractable(name, config)`** — returns `[state, { id, setState, isPending, error, flush }]`
|
|
870
699
|
- **Partial updates** — auto-generated tools use partial schemas; AI only sends changed fields
|
|
871
|
-
- **
|
|
872
|
-
- **Selection** — `
|
|
873
|
-
- **`Tools({ toolkit })`** — custom frontend tools for fine-grained control
|
|
700
|
+
- **Array operations** — array fields support generated `add`, `update`, `remove`, `clear`, and `set` operations on the same `update_{name}` tool
|
|
701
|
+
- **Selection as state** — focused UI state lives in the interactable's normal schema (`selectedId`), not in a separate manage tool
|
|
874
702
|
- **`sendAutomaticallyWhen`** — auto-sends follow-up messages when tool calls complete
|
|
875
703
|
|
|
876
704
|
```
|