ai-chatbot/components/canvas/chat-panel/use-canvas-agent-run.ts

65 lines
3.8 KiB
TypeScript

"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import useSWR from "swr";
import { toast } from "sonner";
import type { AgentRunView } from "@/lib/ai/canvas-agent-runtime/protocol";
export type CanvasAgentRunOptions = {
enabled: boolean; chatId: string; projectId: string; busy: boolean;
onResults: (projectId: string, nodeIds: string[]) => void;
onRunChange?: (run: AgentRunView | null) => void;
};
export function useCanvasAgentRun(props: CanvasAgentRunOptions) {
const [sending, setSending] = useState(false);
const [stopping, setStopping] = useState(false);
const sendingRef = useRef(false);
const scope = `${props.chatId}:${props.projectId}`;
const latest = useRef({ props, scope }); latest.current = { props, scope };
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 = props.enabled && data?.run?.chatId === props.chatId && data.run.projectId === props.projectId ? data.run : null;
useEffect(() => { latest.current.props.onRunChange?.(run); }, [run]);
useEffect(() => { setSending(false); sendingRef.current = false; setStopping(false); resultSignature.current = ""; }, [scope]);
useEffect(() => {
if (!props.enabled) return;
const handler = (event: Event) => {
const update = (event as CustomEvent<AgentRunView>).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;
latest.current.props.onResults(run.projectId, run.resultNodeIds);
}, [run]);
const control = useCallback(async (action: "steer" | "cancel", text?: string) => {
if (!run || sendingRef.current) return false;
sendingRef.current = true;
const startedScope = scope;
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 (startedScope !== latest.current.scope) return false;
await mutate(result, false);
if (action === "cancel") setStopping(true);
toast.message(action === "steer" ? "补充要求已收到,将在下一步应用" : "正在停止后续步骤,已完成的结果会保留");
return true;
} catch (error) { toast.error(error instanceof Error ? error.message : "操作失败"); return false; }
finally { if (startedScope === latest.current.scope) { sendingRef.current = false; setSending(false); } }
}, [run, sending, scope, props.chatId, props.projectId, mutate]);
return { run, sending, stopPending: stopping || Boolean(run?.cancelRequested), control };
}