@assistant-ui/mcp-docs-server 0.1.32 → 0.1.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.docs/organized/code-examples/waterfall.md +20 -22
- package/.docs/organized/code-examples/with-a2a.md +24 -24
- package/.docs/organized/code-examples/with-ag-ui.md +30 -25
- package/.docs/organized/code-examples/with-ai-sdk-v6.md +11 -9
- package/.docs/organized/code-examples/with-artifacts.md +49 -37
- package/.docs/organized/code-examples/with-assistant-transport.md +63 -51
- package/.docs/organized/code-examples/with-browser-extension.md +22 -10
- package/.docs/organized/code-examples/with-chain-of-thought.md +406 -87
- package/.docs/organized/code-examples/with-cloud-standalone.md +25 -24
- package/.docs/organized/code-examples/with-cloud.md +11 -9
- package/.docs/organized/code-examples/with-custom-thread-list.md +14 -12
- package/.docs/organized/code-examples/with-elevenlabs-conversational.md +23 -21
- package/.docs/organized/code-examples/with-elevenlabs-scribe.md +14 -13
- package/.docs/organized/code-examples/with-expo.md +44 -29
- package/.docs/organized/code-examples/with-external-store.md +9 -7
- package/.docs/organized/code-examples/with-ffmpeg.md +322 -285
- package/.docs/organized/code-examples/with-generative-ui.md +1066 -256
- package/.docs/organized/code-examples/with-google-adk.md +10 -8
- package/.docs/organized/code-examples/with-heat-graph.md +13 -11
- package/.docs/organized/code-examples/with-image-generation.md +19 -17
- package/.docs/organized/code-examples/with-interactables.md +317 -239
- package/.docs/organized/code-examples/with-langchain.md +14 -12
- package/.docs/organized/code-examples/with-langgraph.md +91 -83
- package/.docs/organized/code-examples/with-livekit.md +24 -22
- package/.docs/organized/code-examples/with-mcp.md +18 -12
- package/.docs/organized/code-examples/with-opencode.md +66 -66
- package/.docs/organized/code-examples/with-react-hook-form.md +19 -17
- package/.docs/organized/code-examples/with-react-ink.md +297 -102
- package/.docs/organized/code-examples/with-react-router.md +17 -15
- package/.docs/organized/code-examples/with-resumable-stream.md +15 -14
- package/.docs/organized/code-examples/with-store.md +78 -76
- package/.docs/organized/code-examples/with-tanstack.md +13 -14
- package/.docs/organized/code-examples/with-tap-runtime.md +26 -24
- package/.docs/raw/docs/(docs)/architecture.mdx +94 -42
- package/.docs/raw/docs/(docs)/cli.mdx +1 -2
- package/.docs/raw/docs/(docs)/installation.mdx +1 -1
- package/.docs/raw/docs/(reference)/api-reference/adapters/persistence.mdx +5 -1
- package/.docs/raw/docs/(reference)/api-reference/external-store/message-conversion.mdx +15 -15
- package/.docs/raw/docs/(reference)/api-reference/external-store/runtime.mdx +14 -1
- package/.docs/raw/docs/(reference)/api-reference/generative-ui/index.mdx +18 -0
- package/.docs/raw/docs/(reference)/api-reference/generative-ui/rendering.mdx +33 -0
- package/.docs/raw/docs/(reference)/api-reference/generative-ui/spec.mdx +45 -0
- package/.docs/raw/docs/(reference)/api-reference/hooks/composer-triggers.mdx +41 -41
- package/.docs/raw/docs/(reference)/api-reference/hooks/model-context.mdx +19 -1
- package/.docs/raw/docs/(reference)/api-reference/hooks/primitives.mdx +122 -122
- package/.docs/raw/docs/(reference)/api-reference/integrations/react-ai-sdk.mdx +43 -2
- package/.docs/raw/docs/(reference)/api-reference/model-context/context.mdx +6 -0
- package/.docs/raw/docs/(reference)/api-reference/overview.mdx +4 -6
- package/.docs/raw/docs/(reference)/api-reference/primitives/message.mdx +30 -0
- package/.docs/raw/docs/(reference)/api-reference/tools/component-tools.mdx +20 -3
- package/.docs/raw/docs/(reference)/api-reference/tools/index.mdx +8 -8
- package/.docs/raw/docs/(reference)/api-reference/tools/rendering.mdx +52 -4
- package/.docs/raw/docs/(reference)/api-reference/tools/toolkits.mdx +100 -10
- package/.docs/raw/docs/(reference)/api-reference/utilities/miscellaneous.mdx +7 -59
- package/.docs/raw/docs/cloud/ai-sdk.mdx +0 -2
- package/.docs/raw/docs/{(docs)/copilots → copilots}/model-context.mdx +34 -26
- package/.docs/raw/docs/{(docs)/copilots → copilots}/motivation.mdx +32 -26
- package/.docs/raw/docs/guides/chain-of-thought.mdx +7 -9
- package/.docs/raw/docs/guides/context-api.mdx +2 -1
- package/.docs/raw/docs/guides/index.mdx +3 -12
- package/.docs/raw/docs/guides/mentions.mdx +4 -4
- package/.docs/raw/docs/guides/slash-commands.mdx +1 -1
- package/.docs/raw/docs/guides/suggestions.mdx +1 -1
- package/.docs/raw/docs/ink/adapters.mdx +23 -1
- package/.docs/raw/docs/ink/hooks.mdx +101 -85
- package/.docs/raw/docs/ink/migration.mdx +1 -1
- package/.docs/raw/docs/ink/primitives.mdx +2 -2
- package/.docs/raw/docs/integrations/frameworks/ai-sdk.mdx +1 -1
- package/.docs/raw/docs/integrations/index.mdx +2 -2
- package/.docs/raw/docs/integrations/persistence/custom-adapter.mdx +17 -2
- package/.docs/raw/docs/migrations/toolkit-tools.mdx +232 -0
- package/.docs/raw/docs/primitives/chain-of-thought.mdx +10 -16
- package/.docs/raw/docs/primitives/message.mdx +9 -10
- package/.docs/raw/docs/react-native/hooks.mdx +62 -79
- package/.docs/raw/docs/react-native/migration.mdx +1 -1
- package/.docs/raw/docs/react-native/primitives.mdx +2 -2
- package/.docs/raw/docs/runtimes/a2a/client-and-hooks.mdx +3 -0
- package/.docs/raw/docs/runtimes/ag-ui/runtime-options.mdx +41 -0
- package/.docs/raw/docs/runtimes/ai-sdk/v6.mdx +122 -1
- package/.docs/raw/docs/runtimes/concepts/architecture.mdx +46 -4
- package/.docs/raw/docs/runtimes/concepts/threads.mdx +7 -1
- package/.docs/raw/docs/runtimes/custom/assistant-transport.mdx +54 -12
- package/.docs/raw/docs/runtimes/custom/external-store.mdx +62 -4
- package/.docs/raw/docs/runtimes/custom/local-runtime.mdx +30 -7
- package/.docs/raw/docs/runtimes/google-adk/hooks.mdx +108 -38
- package/.docs/raw/docs/runtimes/langgraph/generative-ui.mdx +2 -2
- package/.docs/raw/docs/runtimes/langgraph/streaming.mdx +14 -1
- package/.docs/raw/docs/runtimes/langgraph/tutorial/part-2.mdx +64 -50
- package/.docs/raw/docs/runtimes/langgraph/tutorial/part-3.mdx +98 -86
- package/.docs/raw/docs/tools/backend.mdx +144 -0
- package/.docs/raw/docs/tools/defining-tools.mdx +538 -0
- package/.docs/raw/docs/tools/dynamic-tools.mdx +110 -0
- package/.docs/raw/docs/tools/generative-ui.mdx +214 -0
- package/.docs/raw/docs/tools/index.mdx +71 -0
- package/.docs/raw/docs/{guides → tools}/interactables.mdx +1 -1
- package/.docs/raw/docs/{integrations/tools → tools}/mcp.mdx +145 -50
- package/.docs/raw/docs/{guides → tools}/multi-agent.mdx +15 -17
- package/.docs/raw/docs/tools/tool-ui.mdx +967 -0
- package/.docs/raw/docs/{integrations/tools/react-mcp.mdx → tools/user-managed-mcp.mdx} +7 -7
- package/.docs/raw/docs/ui/directive-text.mdx +3 -3
- package/.docs/raw/docs/ui/mcp-config.mdx +4 -4
- package/.docs/raw/docs/ui/mermaid.mdx +16 -9
- package/.docs/raw/docs/ui/part-grouping.mdx +84 -50
- package/.docs/raw/docs/ui/reasoning.mdx +4 -5
- package/.docs/raw/docs/ui/tool-fallback.mdx +16 -0
- package/.docs/raw/docs/ui/tool-group.mdx +5 -6
- package/.docs/raw/docs/utilities/heat-graph.mdx +1 -1
- package/.docs/raw/docs/utilities/react-o11y.mdx +2 -2
- package/dist/constants.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/prepare-docs/code-examples.d.ts.map +1 -1
- package/dist/prepare-docs/code-examples.js.map +1 -1
- package/dist/prepare-docs/copy-raw.d.ts.map +1 -1
- package/dist/prepare-docs/copy-raw.js.map +1 -1
- package/dist/prepare-docs/prepare.js.map +1 -1
- package/dist/stdio.js.map +1 -1
- package/dist/tools/docs.js.map +1 -1
- package/dist/tools/examples.js.map +1 -1
- package/dist/tools/tests/test-setup.js.map +1 -1
- package/dist/utils/mdx.js.map +1 -1
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js.map +1 -1
- package/package.json +5 -5
- package/.docs/organized/code-examples/with-parent-id-grouping.md +0 -596
- package/.docs/raw/docs/(docs)/copilots/make-assistant-tool-ui.mdx +0 -151
- package/.docs/raw/docs/(docs)/copilots/make-assistant-tool.mdx +0 -230
- package/.docs/raw/docs/guides/generative-ui.mdx +0 -142
- package/.docs/raw/docs/guides/tool-ui.mdx +0 -858
- package/.docs/raw/docs/guides/tools.mdx +0 -736
- /package/.docs/raw/docs/{(docs)/copilots → copilots}/assistant-frame.mdx +0 -0
- /package/.docs/raw/docs/{(docs)/copilots → copilots}/make-assistant-visible.mdx +0 -0
- /package/.docs/raw/docs/{(docs)/copilots → copilots}/use-assistant-instructions.mdx +0 -0
- /package/.docs/raw/docs/{guides → tools}/mcp-apps.mdx +0 -0
|
@@ -267,30 +267,26 @@ export default dynamic(() => Promise.resolve(NoSSRWrapper), {
|
|
|
267
267
|
|
|
268
268
|
import {
|
|
269
269
|
useAssistantInstructions,
|
|
270
|
-
useAssistantTool,
|
|
271
270
|
useAui,
|
|
272
271
|
useAuiState,
|
|
273
272
|
AuiProvider,
|
|
274
273
|
Suggestions,
|
|
274
|
+
Tools,
|
|
275
275
|
} from "@assistant-ui/react";
|
|
276
|
-
import { z } from "zod";
|
|
277
276
|
import { FFmpeg } from "@ffmpeg/ffmpeg";
|
|
278
277
|
import { toBlobURL } from "@ffmpeg/util";
|
|
279
|
-
import { type FC,
|
|
280
|
-
import {
|
|
281
|
-
CircleCheckIcon,
|
|
282
|
-
DownloadIcon,
|
|
283
|
-
FileIcon,
|
|
284
|
-
RefreshCcwIcon,
|
|
285
|
-
TriangleAlertIcon,
|
|
286
|
-
} from "lucide-react";
|
|
278
|
+
import { type FC, type ReactNode, useEffect, useRef, useState } from "react";
|
|
287
279
|
import { Thread } from "@/components/assistant-ui/thread";
|
|
280
|
+
import { useFfmpegToolkit } from "./toolkit";
|
|
288
281
|
|
|
289
282
|
// MVP: upload file, enter command
|
|
290
283
|
// MVP: convert command to tool call
|
|
291
284
|
// MVP: tool call: ffmpeg
|
|
292
285
|
|
|
293
|
-
const
|
|
286
|
+
const FfmpegToolsProvider: FC<{ file: File; children: ReactNode }> = ({
|
|
287
|
+
file,
|
|
288
|
+
children,
|
|
289
|
+
}) => {
|
|
294
290
|
const loadingRef = useRef(false);
|
|
295
291
|
const ffmpegRef = useRef(new FFmpeg());
|
|
296
292
|
|
|
@@ -321,272 +317,13 @@ const FfmpegTool: FC<{ file: File }> = ({ file }) => {
|
|
|
321
317
|
`The user has attached a file: ${file.name}. To add text overlays, use the render_overlay tool to render HTML to a PNG image, then use run_ffmpeg with the "overlay" filter to composite it onto the video. Do NOT use the drawtext filter.`,
|
|
322
318
|
);
|
|
323
319
|
|
|
324
|
-
|
|
325
|
-
toolName: "run_ffmpeg",
|
|
326
|
-
parameters: z.object({
|
|
327
|
-
command: z
|
|
328
|
-
.string()
|
|
329
|
-
.array()
|
|
330
|
-
.describe("The ffmpeg command line arguments to provide"),
|
|
331
|
-
}),
|
|
332
|
-
execute: async ({ command }) => {
|
|
333
|
-
const transcode = async () => {
|
|
334
|
-
const ffmpeg = ffmpegRef.current;
|
|
335
|
-
|
|
336
|
-
const logs: string[] = [];
|
|
337
|
-
const logger = ({ message }: { message: string }) => {
|
|
338
|
-
logs.push(message);
|
|
339
|
-
};
|
|
340
|
-
ffmpeg.on("log", logger);
|
|
341
|
-
|
|
342
|
-
await ffmpeg.writeFile(
|
|
343
|
-
file.name,
|
|
344
|
-
new Uint8Array(await file.arrayBuffer()),
|
|
345
|
-
);
|
|
346
|
-
|
|
347
|
-
const code = await ffmpeg.exec(command);
|
|
348
|
-
ffmpeg.off("log", logger);
|
|
349
|
-
|
|
350
|
-
return { code, logs };
|
|
351
|
-
};
|
|
352
|
-
const { code, logs } = await transcode();
|
|
353
|
-
|
|
354
|
-
return {
|
|
355
|
-
success: code === 0,
|
|
356
|
-
hint:
|
|
357
|
-
code === 0
|
|
358
|
-
? "Success. Now call display_file to show the output to the user."
|
|
359
|
-
: `some error happened, logs: ${logs.join("\n")}`,
|
|
360
|
-
};
|
|
361
|
-
},
|
|
362
|
-
render: function RenderFfmpeg({
|
|
363
|
-
args: { command },
|
|
364
|
-
result: { success } = {},
|
|
365
|
-
}) {
|
|
366
|
-
return (
|
|
367
|
-
<div className="mb-2 flex flex-col gap-2 rounded-lg border px-5 py-4">
|
|
368
|
-
<div className="flex items-center gap-2">
|
|
369
|
-
{success == null && (
|
|
370
|
-
<RefreshCcwIcon className="size-4 animate-spin text-blue-600" />
|
|
371
|
-
)}
|
|
372
|
-
{success === false && (
|
|
373
|
-
<TriangleAlertIcon className="size-4 text-red-600" />
|
|
374
|
-
)}
|
|
375
|
-
{success === true && (
|
|
376
|
-
<CircleCheckIcon className="size-4 text-green-600" />
|
|
377
|
-
)}
|
|
378
|
-
<p>Running ffmpeg</p>
|
|
379
|
-
</div>
|
|
380
|
-
<pre className="overflow-y-scroll font-sm">
|
|
381
|
-
ffmpeg {command?.join(" ")}
|
|
382
|
-
</pre>
|
|
383
|
-
{success === false && (
|
|
384
|
-
<div className="mt-2 border-t border-dashed pt-3">
|
|
385
|
-
Encountered an error.
|
|
386
|
-
</div>
|
|
387
|
-
)}
|
|
388
|
-
</div>
|
|
389
|
-
);
|
|
390
|
-
},
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
useAssistantTool({
|
|
394
|
-
toolName: "render_overlay",
|
|
395
|
-
parameters: z.object({
|
|
396
|
-
html: z
|
|
397
|
-
.string()
|
|
398
|
-
.describe(
|
|
399
|
-
"HTML content to render. Use inline styles for all styling. The background is transparent by default.",
|
|
400
|
-
),
|
|
401
|
-
width: z.number().describe("Width of the output image in pixels"),
|
|
402
|
-
height: z.number().describe("Height of the output image in pixels"),
|
|
403
|
-
fileName: z
|
|
404
|
-
.string()
|
|
405
|
-
.describe("Output filename in ffmpeg filesystem, e.g. overlay.png"),
|
|
406
|
-
}),
|
|
407
|
-
execute: async ({ html, width, height, fileName }) => {
|
|
408
|
-
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
|
|
409
|
-
<foreignObject width="100%" height="100%">
|
|
410
|
-
<div xmlns="http://www.w3.org/1999/xhtml">${html}</div>
|
|
411
|
-
</foreignObject>
|
|
412
|
-
</svg>`;
|
|
413
|
-
|
|
414
|
-
const img = new Image();
|
|
415
|
-
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
|
416
|
-
|
|
417
|
-
await new Promise<void>((resolve, reject) => {
|
|
418
|
-
img.onload = () => resolve();
|
|
419
|
-
img.onerror = () => reject(new Error("Failed to render HTML"));
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
const canvas = document.createElement("canvas");
|
|
423
|
-
canvas.width = width;
|
|
424
|
-
canvas.height = height;
|
|
425
|
-
const ctx = canvas.getContext("2d")!;
|
|
426
|
-
ctx.drawImage(img, 0, 0);
|
|
427
|
-
|
|
428
|
-
const blob = await new Promise<Blob>((resolve) =>
|
|
429
|
-
canvas.toBlob((b) => resolve(b!), "image/png"),
|
|
430
|
-
);
|
|
431
|
-
const data = new Uint8Array(await blob.arrayBuffer());
|
|
320
|
+
const toolkit = useFfmpegToolkit(file, ffmpegRef);
|
|
432
321
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
return {
|
|
437
|
-
success: true,
|
|
438
|
-
hint: `Overlay image "${fileName}" (${width}x${height}) written to ffmpeg filesystem. Use it with the overlay filter in run_ffmpeg, e.g. -i ${fileName} -filter_complex "overlay=x=0:y=0"`,
|
|
439
|
-
};
|
|
440
|
-
},
|
|
441
|
-
render: function RenderOverlay({ args, result }) {
|
|
442
|
-
return (
|
|
443
|
-
<div className="mb-2 flex items-center gap-2 rounded-lg border px-5 py-4">
|
|
444
|
-
{!result && (
|
|
445
|
-
<RefreshCcwIcon className="size-4 animate-spin text-blue-600" />
|
|
446
|
-
)}
|
|
447
|
-
{result?.success && (
|
|
448
|
-
<CircleCheckIcon className="size-4 text-green-600" />
|
|
449
|
-
)}
|
|
450
|
-
<p>
|
|
451
|
-
{result?.success
|
|
452
|
-
? `Rendered overlay → ${args.fileName}`
|
|
453
|
-
: "Rendering overlay..."}
|
|
454
|
-
</p>
|
|
455
|
-
</div>
|
|
456
|
-
);
|
|
457
|
-
},
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
useAssistantTool({
|
|
461
|
-
toolName: "display_file",
|
|
462
|
-
parameters: z.object({
|
|
463
|
-
fileName: z
|
|
464
|
-
.string()
|
|
465
|
-
.describe("The name of the file to display from the ffmpeg filesystem"),
|
|
466
|
-
mimeType: z
|
|
467
|
-
.string()
|
|
468
|
-
.describe("The mime type of the file, e.g. image/png, video/mp4"),
|
|
469
|
-
}),
|
|
470
|
-
execute: async ({ fileName }) => {
|
|
471
|
-
const ffmpeg = ffmpegRef.current;
|
|
472
|
-
try {
|
|
473
|
-
const data = (await ffmpeg.readFile(
|
|
474
|
-
fileName,
|
|
475
|
-
)) as Uint8Array<ArrayBuffer>;
|
|
476
|
-
return {
|
|
477
|
-
success: true,
|
|
478
|
-
size: data.byteLength,
|
|
479
|
-
hint: "A file preview and download button is now visible to the user. Do not describe the file or repeat its contents.",
|
|
480
|
-
};
|
|
481
|
-
} catch {
|
|
482
|
-
return {
|
|
483
|
-
success: false,
|
|
484
|
-
error: `File "${fileName}" not found in ffmpeg filesystem`,
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
},
|
|
488
|
-
render: function RenderDisplayFile({
|
|
489
|
-
args: { fileName, mimeType },
|
|
490
|
-
result,
|
|
491
|
-
}) {
|
|
492
|
-
// biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
|
|
493
|
-
const [blobUrl, setBlobUrl] = useState<string | null>(null);
|
|
494
|
-
|
|
495
|
-
// biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
|
|
496
|
-
const readFile = useCallback(async () => {
|
|
497
|
-
const ffmpeg = ffmpegRef.current;
|
|
498
|
-
const data = (await ffmpeg.readFile(
|
|
499
|
-
fileName,
|
|
500
|
-
)) as Uint8Array<ArrayBuffer>;
|
|
501
|
-
return URL.createObjectURL(new Blob([data.buffer], { type: mimeType }));
|
|
502
|
-
}, [fileName, mimeType]);
|
|
503
|
-
|
|
504
|
-
// biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
|
|
505
|
-
useEffect(() => {
|
|
506
|
-
if (!result?.success) return;
|
|
507
|
-
let revoked = false;
|
|
508
|
-
readFile().then((url) => {
|
|
509
|
-
if (revoked) {
|
|
510
|
-
URL.revokeObjectURL(url);
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
setBlobUrl(url);
|
|
514
|
-
});
|
|
515
|
-
return () => {
|
|
516
|
-
revoked = true;
|
|
517
|
-
setBlobUrl((prev) => {
|
|
518
|
-
if (prev) URL.revokeObjectURL(prev);
|
|
519
|
-
return null;
|
|
520
|
-
});
|
|
521
|
-
};
|
|
522
|
-
}, [result?.success, readFile]);
|
|
523
|
-
|
|
524
|
-
const handleDownload = () => {
|
|
525
|
-
if (!blobUrl) return;
|
|
526
|
-
const a = document.createElement("a");
|
|
527
|
-
a.href = blobUrl;
|
|
528
|
-
a.download = fileName;
|
|
529
|
-
a.click();
|
|
530
|
-
};
|
|
531
|
-
|
|
532
|
-
if (!result) {
|
|
533
|
-
return (
|
|
534
|
-
<div className="flex items-center gap-2 rounded-lg border px-5 py-4">
|
|
535
|
-
<RefreshCcwIcon className="size-4 animate-spin text-blue-600" />
|
|
536
|
-
<p>Loading file...</p>
|
|
537
|
-
</div>
|
|
538
|
-
);
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
if (!result.success) {
|
|
542
|
-
return (
|
|
543
|
-
<div className="flex items-center gap-2 rounded-lg border px-5 py-4">
|
|
544
|
-
<TriangleAlertIcon className="size-4 text-red-600" />
|
|
545
|
-
<p>File not found</p>
|
|
546
|
-
</div>
|
|
547
|
-
);
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
const isImage = mimeType?.startsWith("image/");
|
|
551
|
-
const isVideo = mimeType?.startsWith("video/");
|
|
552
|
-
const isAudio = mimeType?.startsWith("audio/");
|
|
553
|
-
|
|
554
|
-
return (
|
|
555
|
-
<div className="mb-2 flex flex-col gap-3 rounded-lg border px-5 py-4">
|
|
556
|
-
{blobUrl && isImage && (
|
|
557
|
-
// biome-ignore lint/performance/noImgElement: blob URL display
|
|
558
|
-
<img
|
|
559
|
-
src={blobUrl}
|
|
560
|
-
alt={fileName}
|
|
561
|
-
className="max-h-64 w-fit rounded"
|
|
562
|
-
/>
|
|
563
|
-
)}
|
|
564
|
-
{blobUrl && isVideo && (
|
|
565
|
-
// biome-ignore lint/a11y/useMediaCaption: generated output
|
|
566
|
-
<video src={blobUrl} controls className="max-h-64 w-fit rounded" />
|
|
567
|
-
)}
|
|
568
|
-
{/* biome-ignore lint/a11y/useMediaCaption: generated output */}
|
|
569
|
-
{blobUrl && isAudio && <audio src={blobUrl} controls />}
|
|
570
|
-
{blobUrl && !isImage && !isVideo && !isAudio && (
|
|
571
|
-
<div className="flex items-center gap-2 text-muted-foreground">
|
|
572
|
-
<FileIcon className="size-5" />
|
|
573
|
-
<span>{fileName}</span>
|
|
574
|
-
</div>
|
|
575
|
-
)}
|
|
576
|
-
<button
|
|
577
|
-
type="button"
|
|
578
|
-
onClick={handleDownload}
|
|
579
|
-
className="flex w-fit items-center gap-2 rounded-md border bg-background px-4 py-2 text-sm hover:bg-accent"
|
|
580
|
-
>
|
|
581
|
-
<DownloadIcon className="size-4" />
|
|
582
|
-
Download {fileName}
|
|
583
|
-
</button>
|
|
584
|
-
</div>
|
|
585
|
-
);
|
|
586
|
-
},
|
|
322
|
+
const aui = useAui({
|
|
323
|
+
tools: Tools({ toolkit }),
|
|
587
324
|
});
|
|
588
325
|
|
|
589
|
-
return
|
|
326
|
+
return <AuiProvider value={aui}>{children}</AuiProvider>;
|
|
590
327
|
};
|
|
591
328
|
|
|
592
329
|
export default function Home() {
|
|
@@ -616,15 +353,313 @@ export default function Home() {
|
|
|
616
353
|
return (
|
|
617
354
|
<div className="flex h-full flex-col">
|
|
618
355
|
<AuiProvider value={aui}>
|
|
619
|
-
|
|
356
|
+
{lastFile ? (
|
|
357
|
+
<FfmpegToolsProvider file={lastFile}>
|
|
358
|
+
<Thread />
|
|
359
|
+
</FfmpegToolsProvider>
|
|
360
|
+
) : (
|
|
361
|
+
<Thread />
|
|
362
|
+
)}
|
|
620
363
|
</AuiProvider>
|
|
621
|
-
{lastFile && <FfmpegTool file={lastFile} />}
|
|
622
364
|
</div>
|
|
623
365
|
);
|
|
624
366
|
}
|
|
625
367
|
|
|
626
368
|
```
|
|
627
369
|
|
|
370
|
+
## app/toolkit.tsx
|
|
371
|
+
|
|
372
|
+
```tsx
|
|
373
|
+
"use client";
|
|
374
|
+
|
|
375
|
+
import type { MutableRefObject } from "react";
|
|
376
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
377
|
+
import { defineToolkit } from "@assistant-ui/react";
|
|
378
|
+
import type { FFmpeg } from "@ffmpeg/ffmpeg";
|
|
379
|
+
import {
|
|
380
|
+
CircleCheckIcon,
|
|
381
|
+
DownloadIcon,
|
|
382
|
+
FileIcon,
|
|
383
|
+
RefreshCcwIcon,
|
|
384
|
+
TriangleAlertIcon,
|
|
385
|
+
} from "lucide-react";
|
|
386
|
+
import { z } from "zod";
|
|
387
|
+
|
|
388
|
+
export function useFfmpegToolkit(
|
|
389
|
+
file: File,
|
|
390
|
+
ffmpegRef: MutableRefObject<FFmpeg>,
|
|
391
|
+
) {
|
|
392
|
+
return useMemo(
|
|
393
|
+
() =>
|
|
394
|
+
defineToolkit({
|
|
395
|
+
run_ffmpeg: {
|
|
396
|
+
type: "frontend",
|
|
397
|
+
parameters: z.object({
|
|
398
|
+
command: z
|
|
399
|
+
.string()
|
|
400
|
+
.array()
|
|
401
|
+
.describe("The ffmpeg command line arguments to provide"),
|
|
402
|
+
}),
|
|
403
|
+
execute: async ({ command }) => {
|
|
404
|
+
const ffmpeg = ffmpegRef.current;
|
|
405
|
+
|
|
406
|
+
const logs: string[] = [];
|
|
407
|
+
const logger = ({ message }: { message: string }) => {
|
|
408
|
+
logs.push(message);
|
|
409
|
+
};
|
|
410
|
+
ffmpeg.on("log", logger);
|
|
411
|
+
|
|
412
|
+
await ffmpeg.writeFile(
|
|
413
|
+
file.name,
|
|
414
|
+
new Uint8Array(await file.arrayBuffer()),
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
const code = await ffmpeg.exec(command);
|
|
418
|
+
ffmpeg.off("log", logger);
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
success: code === 0,
|
|
422
|
+
hint:
|
|
423
|
+
code === 0
|
|
424
|
+
? "Success. Now call display_file to show the output to the user."
|
|
425
|
+
: `some error happened, logs: ${logs.join("\n")}`,
|
|
426
|
+
};
|
|
427
|
+
},
|
|
428
|
+
render: function RenderFfmpeg({
|
|
429
|
+
args: { command },
|
|
430
|
+
result: { success } = {},
|
|
431
|
+
}) {
|
|
432
|
+
return (
|
|
433
|
+
<div className="mb-2 flex flex-col gap-2 rounded-lg border px-5 py-4">
|
|
434
|
+
<div className="flex items-center gap-2">
|
|
435
|
+
{success == null && (
|
|
436
|
+
<RefreshCcwIcon className="size-4 animate-spin text-blue-600" />
|
|
437
|
+
)}
|
|
438
|
+
{success === false && (
|
|
439
|
+
<TriangleAlertIcon className="size-4 text-red-600" />
|
|
440
|
+
)}
|
|
441
|
+
{success === true && (
|
|
442
|
+
<CircleCheckIcon className="size-4 text-green-600" />
|
|
443
|
+
)}
|
|
444
|
+
<p>Running ffmpeg</p>
|
|
445
|
+
</div>
|
|
446
|
+
<pre className="font-sm overflow-y-scroll">
|
|
447
|
+
ffmpeg {command?.join(" ")}
|
|
448
|
+
</pre>
|
|
449
|
+
{success === false && (
|
|
450
|
+
<div className="mt-2 border-t border-dashed pt-3">
|
|
451
|
+
Encountered an error.
|
|
452
|
+
</div>
|
|
453
|
+
)}
|
|
454
|
+
</div>
|
|
455
|
+
);
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
render_overlay: {
|
|
459
|
+
type: "frontend",
|
|
460
|
+
parameters: z.object({
|
|
461
|
+
html: z
|
|
462
|
+
.string()
|
|
463
|
+
.describe(
|
|
464
|
+
"HTML content to render. Use inline styles for all styling. The background is transparent by default.",
|
|
465
|
+
),
|
|
466
|
+
width: z.number().describe("Width of the output image in pixels"),
|
|
467
|
+
height: z.number().describe("Height of the output image in pixels"),
|
|
468
|
+
fileName: z
|
|
469
|
+
.string()
|
|
470
|
+
.describe(
|
|
471
|
+
"Output filename in ffmpeg filesystem, e.g. overlay.png",
|
|
472
|
+
),
|
|
473
|
+
}),
|
|
474
|
+
execute: async ({ html, width, height, fileName }) => {
|
|
475
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
|
|
476
|
+
<foreignObject width="100%" height="100%">
|
|
477
|
+
<div xmlns="http://www.w3.org/1999/xhtml">${html}</div>
|
|
478
|
+
</foreignObject>
|
|
479
|
+
</svg>`;
|
|
480
|
+
|
|
481
|
+
const img = new Image();
|
|
482
|
+
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
|
483
|
+
|
|
484
|
+
await new Promise<void>((resolve, reject) => {
|
|
485
|
+
img.onload = () => resolve();
|
|
486
|
+
img.onerror = () => reject(new Error("Failed to render HTML"));
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
const canvas = document.createElement("canvas");
|
|
490
|
+
canvas.width = width;
|
|
491
|
+
canvas.height = height;
|
|
492
|
+
const ctx = canvas.getContext("2d")!;
|
|
493
|
+
ctx.drawImage(img, 0, 0);
|
|
494
|
+
|
|
495
|
+
const blob = await new Promise<Blob>((resolve) =>
|
|
496
|
+
canvas.toBlob((b) => resolve(b!), "image/png"),
|
|
497
|
+
);
|
|
498
|
+
const data = new Uint8Array(await blob.arrayBuffer());
|
|
499
|
+
|
|
500
|
+
const ffmpeg = ffmpegRef.current;
|
|
501
|
+
await ffmpeg.writeFile(fileName, data);
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
success: true,
|
|
505
|
+
hint: `Overlay image "${fileName}" (${width}x${height}) written to ffmpeg filesystem. Use it with the overlay filter in run_ffmpeg, e.g. -i ${fileName} -filter_complex "overlay=x=0:y=0"`,
|
|
506
|
+
};
|
|
507
|
+
},
|
|
508
|
+
render: function RenderOverlay({ args, result }) {
|
|
509
|
+
return (
|
|
510
|
+
<div className="mb-2 flex items-center gap-2 rounded-lg border px-5 py-4">
|
|
511
|
+
{!result && (
|
|
512
|
+
<RefreshCcwIcon className="size-4 animate-spin text-blue-600" />
|
|
513
|
+
)}
|
|
514
|
+
{result?.success && (
|
|
515
|
+
<CircleCheckIcon className="size-4 text-green-600" />
|
|
516
|
+
)}
|
|
517
|
+
<p>
|
|
518
|
+
{result?.success
|
|
519
|
+
? `Rendered overlay -> ${args.fileName}`
|
|
520
|
+
: "Rendering overlay..."}
|
|
521
|
+
</p>
|
|
522
|
+
</div>
|
|
523
|
+
);
|
|
524
|
+
},
|
|
525
|
+
},
|
|
526
|
+
display_file: {
|
|
527
|
+
type: "frontend",
|
|
528
|
+
parameters: z.object({
|
|
529
|
+
fileName: z
|
|
530
|
+
.string()
|
|
531
|
+
.describe(
|
|
532
|
+
"The name of the file to display from the ffmpeg filesystem",
|
|
533
|
+
),
|
|
534
|
+
mimeType: z
|
|
535
|
+
.string()
|
|
536
|
+
.describe("The mime type of the file, e.g. image/png, video/mp4"),
|
|
537
|
+
}),
|
|
538
|
+
execute: async ({ fileName }) => {
|
|
539
|
+
const ffmpeg = ffmpegRef.current;
|
|
540
|
+
try {
|
|
541
|
+
const data = (await ffmpeg.readFile(
|
|
542
|
+
fileName,
|
|
543
|
+
)) as Uint8Array<ArrayBuffer>;
|
|
544
|
+
return {
|
|
545
|
+
success: true,
|
|
546
|
+
size: data.byteLength,
|
|
547
|
+
hint: "A file preview and download button is now visible to the user. Do not describe the file or repeat its contents.",
|
|
548
|
+
};
|
|
549
|
+
} catch {
|
|
550
|
+
return {
|
|
551
|
+
success: false,
|
|
552
|
+
error: `File "${fileName}" not found in ffmpeg filesystem`,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
render: function RenderDisplayFile({
|
|
557
|
+
args: { fileName, mimeType },
|
|
558
|
+
result,
|
|
559
|
+
}) {
|
|
560
|
+
const [blobUrl, setBlobUrl] = useState<string | null>(null);
|
|
561
|
+
|
|
562
|
+
const readFile = useCallback(async () => {
|
|
563
|
+
const ffmpeg = ffmpegRef.current;
|
|
564
|
+
const data = (await ffmpeg.readFile(
|
|
565
|
+
fileName,
|
|
566
|
+
)) as Uint8Array<ArrayBuffer>;
|
|
567
|
+
return URL.createObjectURL(
|
|
568
|
+
new Blob([data.buffer], { type: mimeType }),
|
|
569
|
+
);
|
|
570
|
+
}, [fileName, mimeType]);
|
|
571
|
+
|
|
572
|
+
useEffect(() => {
|
|
573
|
+
if (!result?.success) return;
|
|
574
|
+
let revoked = false;
|
|
575
|
+
readFile().then((url) => {
|
|
576
|
+
if (revoked) {
|
|
577
|
+
URL.revokeObjectURL(url);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
setBlobUrl(url);
|
|
581
|
+
});
|
|
582
|
+
return () => {
|
|
583
|
+
revoked = true;
|
|
584
|
+
setBlobUrl((prev) => {
|
|
585
|
+
if (prev) URL.revokeObjectURL(prev);
|
|
586
|
+
return null;
|
|
587
|
+
});
|
|
588
|
+
};
|
|
589
|
+
}, [result?.success, readFile]);
|
|
590
|
+
|
|
591
|
+
const handleDownload = () => {
|
|
592
|
+
if (!blobUrl) return;
|
|
593
|
+
const a = document.createElement("a");
|
|
594
|
+
a.href = blobUrl;
|
|
595
|
+
a.download = fileName;
|
|
596
|
+
a.click();
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
if (!result) {
|
|
600
|
+
return (
|
|
601
|
+
<div className="flex items-center gap-2 rounded-lg border px-5 py-4">
|
|
602
|
+
<RefreshCcwIcon className="size-4 animate-spin text-blue-600" />
|
|
603
|
+
<p>Loading file...</p>
|
|
604
|
+
</div>
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (!result.success) {
|
|
609
|
+
return (
|
|
610
|
+
<div className="flex items-center gap-2 rounded-lg border px-5 py-4">
|
|
611
|
+
<TriangleAlertIcon className="size-4 text-red-600" />
|
|
612
|
+
<p>File not found</p>
|
|
613
|
+
</div>
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const isImage = mimeType?.startsWith("image/");
|
|
618
|
+
const isVideo = mimeType?.startsWith("video/");
|
|
619
|
+
const isAudio = mimeType?.startsWith("audio/");
|
|
620
|
+
|
|
621
|
+
return (
|
|
622
|
+
<div className="mb-2 flex flex-col gap-3 rounded-lg border px-5 py-4">
|
|
623
|
+
{blobUrl && isImage && (
|
|
624
|
+
<img
|
|
625
|
+
src={blobUrl}
|
|
626
|
+
alt={fileName}
|
|
627
|
+
className="max-h-64 w-fit rounded"
|
|
628
|
+
/>
|
|
629
|
+
)}
|
|
630
|
+
{blobUrl && isVideo && (
|
|
631
|
+
<video
|
|
632
|
+
src={blobUrl}
|
|
633
|
+
controls
|
|
634
|
+
className="max-h-64 w-fit rounded"
|
|
635
|
+
/>
|
|
636
|
+
)}
|
|
637
|
+
{blobUrl && isAudio && <audio src={blobUrl} controls />}
|
|
638
|
+
{blobUrl && !isImage && !isVideo && !isAudio && (
|
|
639
|
+
<div className="text-muted-foreground flex items-center gap-2">
|
|
640
|
+
<FileIcon className="size-5" />
|
|
641
|
+
<span>{fileName}</span>
|
|
642
|
+
</div>
|
|
643
|
+
)}
|
|
644
|
+
<button
|
|
645
|
+
type="button"
|
|
646
|
+
onClick={handleDownload}
|
|
647
|
+
className="bg-background hover:bg-accent flex w-fit items-center gap-2 rounded-md border px-4 py-2 text-sm"
|
|
648
|
+
>
|
|
649
|
+
<DownloadIcon className="size-4" />
|
|
650
|
+
Download {fileName}
|
|
651
|
+
</button>
|
|
652
|
+
</div>
|
|
653
|
+
);
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
}),
|
|
657
|
+
[ffmpegRef, file],
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
```
|
|
662
|
+
|
|
628
663
|
## components.json
|
|
629
664
|
|
|
630
665
|
```json
|
|
@@ -658,13 +693,14 @@ export default function Home() {
|
|
|
658
693
|
## next.config.ts
|
|
659
694
|
|
|
660
695
|
```typescript
|
|
696
|
+
import { withAui } from "@assistant-ui/next";
|
|
661
697
|
import type { NextConfig } from "next";
|
|
662
698
|
|
|
663
699
|
const nextConfig: NextConfig = {
|
|
664
700
|
/* config options here */
|
|
665
701
|
};
|
|
666
702
|
|
|
667
|
-
export default nextConfig;
|
|
703
|
+
export default withAui(nextConfig);
|
|
668
704
|
|
|
669
705
|
```
|
|
670
706
|
|
|
@@ -682,28 +718,29 @@ export default nextConfig;
|
|
|
682
718
|
"start": "next start"
|
|
683
719
|
},
|
|
684
720
|
"dependencies": {
|
|
685
|
-
"@ai-sdk/openai": "^3.0.
|
|
721
|
+
"@ai-sdk/openai": "^3.0.68",
|
|
686
722
|
"@assistant-ui/react": "workspace:*",
|
|
687
723
|
"@assistant-ui/react-ai-sdk": "workspace:*",
|
|
688
724
|
"@assistant-ui/react-markdown": "workspace:*",
|
|
689
725
|
"@assistant-ui/ui": "workspace:*",
|
|
690
726
|
"@ffmpeg/ffmpeg": "^0.12.15",
|
|
691
727
|
"@ffmpeg/util": "^0.12.2",
|
|
692
|
-
"ai": "^6.0.
|
|
728
|
+
"ai": "^6.0.197",
|
|
693
729
|
"class-variance-authority": "^0.7.1",
|
|
694
730
|
"clsx": "^2.1.1",
|
|
695
|
-
"lucide-react": "^1.
|
|
696
|
-
"next": "^16.2.
|
|
697
|
-
"react": "^19.2.
|
|
698
|
-
"react-dom": "^19.2.
|
|
731
|
+
"lucide-react": "^1.17.0",
|
|
732
|
+
"next": "^16.2.7",
|
|
733
|
+
"react": "^19.2.7",
|
|
734
|
+
"react-dom": "^19.2.7",
|
|
699
735
|
"tailwind-merge": "^3.6.0",
|
|
700
736
|
"zod": "^4.4.3"
|
|
701
737
|
},
|
|
702
738
|
"devDependencies": {
|
|
739
|
+
"@assistant-ui/next": "workspace:*",
|
|
703
740
|
"@assistant-ui/x-buildutils": "workspace:*",
|
|
704
741
|
"@tailwindcss/postcss": "^4.3.0",
|
|
705
|
-
"@types/node": "^25.9.
|
|
706
|
-
"@types/react": "^19.2.
|
|
742
|
+
"@types/node": "^25.9.2",
|
|
743
|
+
"@types/react": "^19.2.17",
|
|
707
744
|
"@types/react-dom": "^19.2.3",
|
|
708
745
|
"postcss": "^8.5.15",
|
|
709
746
|
"tailwindcss": "^4.3.0",
|