65 lines
3.8 KiB
TypeScript
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 };
|
|
}
|