"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import useSWR from "swr"; import { Loader2, Square, ArrowUp, RotateCcw } from "lucide-react"; import { toast } from "sonner"; import type { AgentRunView } from "@/lib/ai/canvas-agent-runtime/protocol"; export function CanvasAgentRunPanel(props: { enabled: boolean; chatId: string; projectId: string; busy: boolean; onResume: (runId: string) => void; onResults: (projectId: string, nodeIds: string[]) => void; onRunChange?: (run: AgentRunView | null) => void; }) { const [text, setText] = useState(""); const [sending, setSending] = useState(false); const [stopping, setStopping] = useState(false); const activeScope = useRef(""); activeScope.current = `${props.chatId}:${props.projectId}`; const resultSignature = useRef(""); const key = props.enabled ? `/api/canvas/agent-runs?${new URLSearchParams({ chatId: props.chatId, projectId: props.projectId })}` : null; const { data, mutate } = useSWR<{ run: AgentRunView | null }>( key, async (url: string) => { const response = await fetch(url, { cache: "no-store" }); if (!response.ok) return { run: null }; return response.json(); }, { refreshInterval: (latest) => (latest?.run?.status === "running" || props.busy ? 2000 : 0), revalidateOnFocus: true, shouldRetryOnError: false, }, ); const run = data?.run; useEffect(() => { if (data) props.onRunChange?.(data.run); }, [data, props.onRunChange]); useEffect(() => { setText(""); setStopping(false); resultSignature.current = ""; }, [props.chatId, props.projectId]); useEffect(() => { if (!props.enabled) return; const handler = (event: Event) => { const update = (event as CustomEvent).detail; if (update?.chatId === props.chatId && update?.projectId === props.projectId) void mutate({ run: update }, false); }; window.addEventListener("canvas-agent-run:update", handler); return () => window.removeEventListener("canvas-agent-run:update", handler); }, [props.enabled, props.chatId, props.projectId, mutate]); useEffect(() => { if (!run?.resultNodeIds.length) return; const signature = `${run.id}:${run.resultNodeIds.join(",")}:${run.status}`; if (signature === resultSignature.current) return; resultSignature.current = signature; props.onResults(run.projectId, run.resultNodeIds); }, [run, props.onResults]); const control = useCallback( async (action: "steer" | "cancel") => { if (!run || sending) return; const scope = activeScope.current; setSending(true); try { const response = await fetch("/api/canvas/agent-runs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action, chatId: props.chatId, projectId: props.projectId, runId: run.id, ...(action === "steer" ? { inputId: crypto.randomUUID(), text: text.trim() } : {}), }), }); const result = await response.json(); if (!response.ok) throw new Error(result.error || "操作失败,请重试"); if (scope !== activeScope.current) return; await mutate(result, false); if (action === "steer") { setText(""); toast.message("补充要求已收到,将在下一步应用"); } else { setStopping(true); toast.message("将停止后续步骤,已发出的生成请求会先收好结果"); } } catch (error) { toast.error(error instanceof Error ? error.message : "操作失败"); } finally { if (scope === activeScope.current) setSending(false); } }, [run, sending, props.chatId, props.projectId, text, mutate], ); if (!props.enabled || !run || run.status === "completed") return null; const running = run.status === "running"; const stopPending = stopping || run.cancelRequested; return (
{running ? : null} {stopPending && running ? "正在停止后续步骤,等待当前生成结果" : run.message} {running ? ( ) : run.resumable ? ( ) : null}
{run.runtime?.engine === "zcode" ? ZCode · {!run.runtime.sandbox ? "尚未启动沙箱" : run.runtime.state === "starting" ? "正在启动独立沙箱" : run.runtime.state === "running" ? "独立沙箱运行中" : "沙箱已停止"} : null} 已处理 {run.steps} 轮 · {run.toolCount} 个工具步骤
{run.inputs.slice(-2).map((input) => (
{input.applied ? "已应用" : "待应用"}:{input.text}
))} {running && !stopPending ? (
{ event.preventDefault(); if (text.trim()) void control("steer"); }} > setText(event.target.value)} maxLength={2000} disabled={sending} placeholder="补充要求,将从下一步生效" aria-label="补充任务要求" className="min-w-0 flex-1 rounded-xl border border-zinc-200 bg-white px-3 py-2 outline-none focus:border-zinc-400 dark:border-zinc-700 dark:bg-zinc-950" />
) : null}
); }