"use client"; import { useChat } from "@ai-sdk/react"; import { DefaultChatTransport, type UIMessage } from "ai"; import { ArrowUp, MessageSquare, X } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { CanvasAgentRunPanel } from "./canvas-agent-run-panel"; import { videoAgentSettingsSchema, type VideoAgentSettings, } from "@/lib/ai/canvas-agent-runtime/video-protocol"; type ModelOption = { id: string; displayName: string }; export function VideoCanvasAgentPanel(props: { projectId: string; selectedNodeIds: string[]; videoModels: ModelOption[]; imageModels: ModelOption[]; beforeSend: () => Promise; onResults: (projectId: string, nodeIds: string[]) => void; onClose: () => void; }) { const [input, setInput] = useState(""); const [settings, setSettings] = useState(() => videoAgentSettingsSchema.parse({}), ); const [preparing, setPreparing] = useState(false); const [loading, setLoading] = useState(true); const [backgroundRunning, setBackgroundRunning] = useState(false); const latest = useRef({ props, settings }); latest.current = { props, settings }; const resumeRef = useRef(undefined); const mounted = useRef(true); const resultIds = useRef([]); const restoredRunId = useRef(""); const { messages, setMessages, sendMessage, status } = useChat({ id: `video-agent-${props.projectId}`, generateId: () => crypto.randomUUID(), transport: new DefaultChatTransport({ api: "/api/canvas/agent-chat", prepareSendMessagesRequest: ({ messages }) => { const current = latest.current; const resumeRunId = resumeRef.current; resumeRef.current = undefined; return { body: { id: current.props.projectId, message: messages[messages.length - 1], selectedChatModel: "agent", selectedVisibilityType: "private", composerMode: "agent", workspaceVariant: "video", videoAgentSettings: current.settings, resumeRunId, contextPayload: { source: "canvas", projectId: current.props.projectId, selectedNodeIds: current.props.selectedNodeIds, }, }, }; }, }), onData: (part) => { if (part.type === "data-videoCanvasMutation") window.dispatchEvent(new CustomEvent("video-canvas-agent-mutation", { detail: part.data })); if (part.type === "data-canvasAgentUiCommand") window.dispatchEvent(new CustomEvent("canvas-agent-ui-command", { detail: part.data })); if (part.type === "data-canvasAgentRun") window.dispatchEvent(new CustomEvent("canvas-agent-run:update", { detail: part.data })); }, onError: (error) => toast.error(error.message || "视频任务启动失败,请重试"), }); useEffect(() => { mounted.current = true; const controller = new AbortController(); void fetch(`/api/canvas/video-agent?projectId=${props.projectId}`, { cache: "no-store", signal: controller.signal, }) .then(async (response) => { const data = await response.json(); if (!response.ok) throw new Error(data.error || "读取视频对话失败"); if (!controller.signal.aborted) setMessages(data.messages as UIMessage[]); }) .catch((error) => { if (!controller.signal.aborted) toast.error(error.message); }) .finally(() => { if (!controller.signal.aborted) setLoading(false); }); return () => { mounted.current = false; controller.abort(); }; }, [props.projectId, setMessages]); const busy = backgroundRunning || preparing || status === "submitted" || status === "streaming"; const submit = useCallback( async (resumeRunId?: string) => { if (busy || loading) return; const text = input.trim() || (resumeRunId ? "请继续尚未完成的视频创作步骤,保留已完成的脚本、关键帧和视频。" : ""); if (!text) return; setPreparing(true); try { await latest.current.props.beforeSend(); if (!mounted.current) return; resumeRef.current = resumeRunId; setInput(""); await sendMessage({ text }); } catch (error) { toast.error(error instanceof Error ? error.message : "视频任务启动失败"); } finally { resumeRef.current = undefined; if (mounted.current) setPreparing(false); } }, [busy, loading, input, sendMessage], ); const onRunChange = useCallback( (run: import("@/lib/ai/canvas-agent-runtime/protocol").AgentRunView | null) => { resultIds.current = run?.resultNodeIds || []; if (run?.videoSettings && restoredRunId.current !== run.id) { restoredRunId.current = run.id; setSettings(run.videoSettings); } setBackgroundRunning(run?.status === "running"); }, [], ); const onResults = useCallback( (projectId: string, ids: string[]) => latest.current.props.onResults(projectId, ids), [], ); useEffect(() => { const timer = setInterval(() => { if (resultIds.current.length) latest.current.props.onResults(latest.current.props.projectId, resultIds.current); }, 10_000); return () => clearInterval(timer); }, []); return (