Files
wehub-resource-sync e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

288 lines
9.9 KiB
TypeScript

"use client";
import type { MutableRefObject } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { defineToolkit } from "@assistant-ui/react";
import type { FFmpeg } from "@ffmpeg/ffmpeg";
import {
CircleCheckIcon,
DownloadIcon,
FileIcon,
RefreshCcwIcon,
TriangleAlertIcon,
} from "lucide-react";
import { z } from "zod";
export function useFfmpegToolkit(
file: File,
ffmpegRef: MutableRefObject<FFmpeg>,
) {
return useMemo(
() =>
defineToolkit({
run_ffmpeg: {
type: "frontend",
parameters: z.object({
command: z
.string()
.array()
.describe("The ffmpeg command line arguments to provide"),
}),
execute: async ({ command }) => {
const ffmpeg = ffmpegRef.current;
const logs: string[] = [];
const logger = ({ message }: { message: string }) => {
logs.push(message);
};
ffmpeg.on("log", logger);
await ffmpeg.writeFile(
file.name,
new Uint8Array(await file.arrayBuffer()),
);
const code = await ffmpeg.exec(command);
ffmpeg.off("log", logger);
return {
success: code === 0,
hint:
code === 0
? "Success. Now call display_file to show the output to the user."
: `some error happened, logs: ${logs.join("\n")}`,
};
},
render: function RenderFfmpeg({
args: { command },
result: { success } = {},
}) {
return (
<div className="mb-2 flex flex-col gap-2 rounded-lg border px-5 py-4">
<div className="flex items-center gap-2">
{success == null && (
<RefreshCcwIcon className="size-4 animate-spin text-blue-600" />
)}
{success === false && (
<TriangleAlertIcon className="size-4 text-red-600" />
)}
{success === true && (
<CircleCheckIcon className="size-4 text-green-600" />
)}
<p>Running ffmpeg</p>
</div>
<pre className="font-sm overflow-y-scroll">
ffmpeg {command?.join(" ")}
</pre>
{success === false && (
<div className="mt-2 border-t border-dashed pt-3">
Encountered an error.
</div>
)}
</div>
);
},
},
render_overlay: {
type: "frontend",
parameters: z.object({
html: z
.string()
.describe(
"HTML content to render. Use inline styles for all styling. The background is transparent by default.",
),
width: z.number().describe("Width of the output image in pixels"),
height: z.number().describe("Height of the output image in pixels"),
fileName: z
.string()
.describe(
"Output filename in ffmpeg filesystem, e.g. overlay.png",
),
}),
execute: async ({ html, width, height, fileName }) => {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
<foreignObject width="100%" height="100%">
<div xmlns="http://www.w3.org/1999/xhtml">${html}</div>
</foreignObject>
</svg>`;
const img = new Image();
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
await new Promise<void>((resolve, reject) => {
img.onload = () => resolve();
img.onerror = () => reject(new Error("Failed to render HTML"));
});
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d")!;
ctx.drawImage(img, 0, 0);
const blob = await new Promise<Blob>((resolve) =>
canvas.toBlob((b) => resolve(b!), "image/png"),
);
const data = new Uint8Array(await blob.arrayBuffer());
const ffmpeg = ffmpegRef.current;
await ffmpeg.writeFile(fileName, data);
return {
success: true,
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"`,
};
},
render: function RenderOverlay({ args, result }) {
return (
<div className="mb-2 flex items-center gap-2 rounded-lg border px-5 py-4">
{!result && (
<RefreshCcwIcon className="size-4 animate-spin text-blue-600" />
)}
{result?.success && (
<CircleCheckIcon className="size-4 text-green-600" />
)}
<p>
{result?.success
? `Rendered overlay -> ${args.fileName}`
: "Rendering overlay..."}
</p>
</div>
);
},
},
display_file: {
type: "frontend",
parameters: z.object({
fileName: z
.string()
.describe(
"The name of the file to display from the ffmpeg filesystem",
),
mimeType: z
.string()
.describe("The mime type of the file, e.g. image/png, video/mp4"),
}),
execute: async ({ fileName }) => {
const ffmpeg = ffmpegRef.current;
try {
const data = (await ffmpeg.readFile(
fileName,
)) as Uint8Array<ArrayBuffer>;
return {
success: true,
size: data.byteLength,
hint: "A file preview and download button is now visible to the user. Do not describe the file or repeat its contents.",
};
} catch {
return {
success: false,
error: `File "${fileName}" not found in ffmpeg filesystem`,
};
}
},
render: function RenderDisplayFile({
args: { fileName, mimeType },
result,
}) {
const [blobUrl, setBlobUrl] = useState<string | null>(null);
const readFile = useCallback(async () => {
const ffmpeg = ffmpegRef.current;
const data = (await ffmpeg.readFile(
fileName,
)) as Uint8Array<ArrayBuffer>;
return URL.createObjectURL(
new Blob([data.buffer], { type: mimeType }),
);
}, [fileName, mimeType]);
useEffect(() => {
if (!result?.success) return;
let revoked = false;
readFile().then((url) => {
if (revoked) {
URL.revokeObjectURL(url);
return;
}
setBlobUrl(url);
});
return () => {
revoked = true;
setBlobUrl((prev) => {
if (prev) URL.revokeObjectURL(prev);
return null;
});
};
}, [result?.success, readFile]);
const handleDownload = () => {
if (!blobUrl) return;
const a = document.createElement("a");
a.href = blobUrl;
a.download = fileName;
a.click();
};
if (!result) {
return (
<div className="flex items-center gap-2 rounded-lg border px-5 py-4">
<RefreshCcwIcon className="size-4 animate-spin text-blue-600" />
<p>Loading file...</p>
</div>
);
}
if (!result.success) {
return (
<div className="flex items-center gap-2 rounded-lg border px-5 py-4">
<TriangleAlertIcon className="size-4 text-red-600" />
<p>File not found</p>
</div>
);
}
const isImage = mimeType?.startsWith("image/");
const isVideo = mimeType?.startsWith("video/");
const isAudio = mimeType?.startsWith("audio/");
return (
<div className="mb-2 flex flex-col gap-3 rounded-lg border px-5 py-4">
{blobUrl && isImage && (
<img
src={blobUrl}
alt={fileName}
className="max-h-64 w-fit rounded"
/>
)}
{blobUrl && isVideo && (
<video
src={blobUrl}
controls
className="max-h-64 w-fit rounded"
/>
)}
{blobUrl && isAudio && <audio src={blobUrl} controls />}
{blobUrl && !isImage && !isVideo && !isAudio && (
<div className="text-muted-foreground flex items-center gap-2">
<FileIcon className="size-5" />
<span>{fileName}</span>
</div>
)}
<button
type="button"
onClick={handleDownload}
className="bg-background hover:bg-accent flex w-fit items-center gap-2 rounded-md border px-4 py-2 text-sm"
>
<DownloadIcon className="size-4" />
Download {fileName}
</button>
</div>
);
},
},
}),
[ffmpegRef, file],
);
}