mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-05 17:04:27 +08:00
cf0561d12a
Move the canvas<->local-Agent bridge (agentSnapshot memo, applyAgentOps / undoAgentOps, agentUndoSnapshot state, and the publish-to-agent-store effect) out of InfiniteCanvasPage into a useAgentBridge hook. Only applyAgentOps is consumed outside (by the plugin host); the rest is fully encapsulated. The hook receives the shared canvas state/refs/setters explicitly and keeps the exact same dependency arrays, so behavior is unchanged. project.tsx drops the now-unused agent-ops imports and the setCanvasContext selector. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
98 lines
5.4 KiB
TypeScript
98 lines
5.4 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState, type Dispatch, type MutableRefObject, type SetStateAction } from "react";
|
||
|
||
import { useAgentStore } from "@/stores/use-agent-store";
|
||
import { applyCanvasAgentOps, type CanvasAgentOp, type CanvasAgentSnapshot } from "@/lib/canvas/canvas-agent-ops";
|
||
import type { CanvasNodeGenerationMode } from "@/components/canvas/canvas-node-prompt-panel";
|
||
import type { CanvasConnection, CanvasNodeData, ContextMenuState, ViewportTransform } from "@/types/canvas";
|
||
|
||
type GenerateNodeRef = MutableRefObject<((nodeId: string, mode: CanvasNodeGenerationMode, prompt: string) => Promise<void>) | null>;
|
||
|
||
type AgentBridgeParams = {
|
||
projectId: string;
|
||
title: string | undefined;
|
||
nodes: CanvasNodeData[];
|
||
connections: CanvasConnection[];
|
||
selectedNodeIds: Set<string>;
|
||
viewport: ViewportTransform;
|
||
nodesRef: MutableRefObject<CanvasNodeData[]>;
|
||
connectionsRef: MutableRefObject<CanvasConnection[]>;
|
||
selectedNodeIdsRef: MutableRefObject<Set<string>>;
|
||
viewportRef: MutableRefObject<ViewportTransform>;
|
||
generateNodeRef: GenerateNodeRef;
|
||
setNodes: Dispatch<SetStateAction<CanvasNodeData[]>>;
|
||
setConnections: Dispatch<SetStateAction<CanvasConnection[]>>;
|
||
setSelectedNodeIds: Dispatch<SetStateAction<Set<string>>>;
|
||
setSelectedConnectionId: Dispatch<SetStateAction<string | null>>;
|
||
setViewport: Dispatch<SetStateAction<ViewportTransform>>;
|
||
setContextMenu: Dispatch<SetStateAction<ContextMenuState | null>>;
|
||
};
|
||
|
||
/**
|
||
* 画布与本地 Agent 的桥接:把当前画布快照与 apply/undo 能力发布到 agent store,
|
||
* 供本地 Codex 面板读取。除 applyAgentOps(配置节点插件宿主会用到)外均为内部实现。
|
||
*/
|
||
export function useAgentBridge(params: AgentBridgeParams) {
|
||
const { projectId, title, nodes, connections, selectedNodeIds, viewport, nodesRef, connectionsRef, selectedNodeIdsRef, viewportRef, generateNodeRef, setNodes, setConnections, setSelectedNodeIds, setSelectedConnectionId, setViewport, setContextMenu } =
|
||
params;
|
||
const setAgentCanvasContext = useAgentStore((state) => state.setCanvasContext);
|
||
const [agentUndoSnapshot, setAgentUndoSnapshot] = useState<CanvasAgentSnapshot | null>(null);
|
||
const projectTitle = title || "未命名画布";
|
||
|
||
const agentSnapshot = useMemo<CanvasAgentSnapshot>(() => ({ projectId, title: projectTitle, nodes, connections, selectedNodeIds: Array.from(selectedNodeIds), viewport }), [connections, projectTitle, nodes, projectId, selectedNodeIds, viewport]);
|
||
const applyAgentOps = useCallback(
|
||
(ops?: CanvasAgentOp[]) => {
|
||
const safeOps = Array.isArray(ops) ? ops.filter((op) => op?.type) : [];
|
||
const before = { projectId, title: projectTitle, nodes: nodesRef.current, connections: connectionsRef.current, selectedNodeIds: Array.from(selectedNodeIdsRef.current), viewport: viewportRef.current };
|
||
const generationOps = safeOps.filter((op): op is Extract<CanvasAgentOp, { type: "run_generation" }> => op.type === "run_generation" && Boolean(op.nodeId));
|
||
const next = applyCanvasAgentOps(
|
||
before,
|
||
safeOps.filter((op) => op.type !== "run_generation"),
|
||
);
|
||
nodesRef.current = next.nodes;
|
||
connectionsRef.current = next.connections;
|
||
selectedNodeIdsRef.current = new Set(next.selectedNodeIds);
|
||
viewportRef.current = next.viewport;
|
||
setAgentUndoSnapshot(before);
|
||
setNodes(next.nodes);
|
||
setConnections(next.connections);
|
||
setSelectedNodeIds(new Set(next.selectedNodeIds));
|
||
setSelectedConnectionId(null);
|
||
setViewport(next.viewport);
|
||
setContextMenu(null);
|
||
if (generationOps.length) {
|
||
queueMicrotask(() =>
|
||
generationOps.forEach((op) => {
|
||
const target = nodesRef.current.find((node) => node.id === op.nodeId);
|
||
const prompt = op.prompt?.trim() ? op.prompt : (target?.metadata?.composerContent ?? target?.metadata?.prompt ?? "");
|
||
void generateNodeRef.current?.(op.nodeId, op.mode || target?.metadata?.generationMode || "image", prompt);
|
||
}),
|
||
);
|
||
}
|
||
return { ...next, projectId, title: projectTitle };
|
||
},
|
||
[projectTitle, projectId],
|
||
);
|
||
const undoAgentOps = useCallback(() => {
|
||
if (!agentUndoSnapshot) return null;
|
||
nodesRef.current = agentUndoSnapshot.nodes;
|
||
connectionsRef.current = agentUndoSnapshot.connections;
|
||
selectedNodeIdsRef.current = new Set(agentUndoSnapshot.selectedNodeIds);
|
||
viewportRef.current = agentUndoSnapshot.viewport;
|
||
setNodes(agentUndoSnapshot.nodes);
|
||
setConnections(agentUndoSnapshot.connections);
|
||
setSelectedNodeIds(new Set(agentUndoSnapshot.selectedNodeIds));
|
||
setSelectedConnectionId(null);
|
||
setViewport(agentUndoSnapshot.viewport);
|
||
setContextMenu(null);
|
||
setAgentUndoSnapshot(null);
|
||
return { ...agentUndoSnapshot, projectId, title: projectTitle };
|
||
}, [agentUndoSnapshot, projectTitle, projectId]);
|
||
|
||
useEffect(() => {
|
||
setAgentCanvasContext({ snapshot: agentSnapshot, applyOps: applyAgentOps, undoOps: undoAgentOps, canUndo: Boolean(agentUndoSnapshot) });
|
||
return () => setAgentCanvasContext(null);
|
||
}, [agentSnapshot, applyAgentOps, agentUndoSnapshot, setAgentCanvasContext, undoAgentOps]);
|
||
|
||
return { applyAgentOps };
|
||
}
|