import { FC, useCallback, useRef, useState, type Dispatch, type SetStateAction, } from "react"; import { ReactFlow, ReactFlowProvider, ConnectionMode, useReactFlow, type Node, type Edge, } from "@xyflow/react"; import "@xyflow/react/dist/style.css"; import "./blueprint.css"; import { BlueprintGraph, type BlueprintFlowNodeData } from "./graph"; import { BlueprintContextMenu, type BlueprintContextMenuState, } from "./BlueprintContextMenu"; import { BlueprintCanvasProvider } from "./BlueprintCanvasContext"; import { blueprintNodeTypes } from "./blueprintNodeTypes"; import { createBlueprintEdgeTypes } from "./createBlueprintEdgeTypes"; import { BLUEPRINT_DEFAULT_EDGE_OPTIONS } from "./flowDefaults"; import { useBlueprintFlowColorMode } from "./hooks/useBlueprintFlowColorMode"; import { useBlueprintFlowState } from "./hooks/useBlueprintFlowState"; import { useBlueprintFlowViewport } from "./hooks/useBlueprintFlowViewport"; import { clientToFlowNodePosition } from "./flowCoordinates"; import type { BlueprintExecutionOverlay } from "./runtime/execution-overlay"; export type { BlueprintExecutionOverlay } from "./runtime/execution-overlay"; export type { BlueprintNodeConfigSidebarProps, BlueprintViewElementOption, BlueprintLibraryOption } from "./BlueprintNodeConfigSidebar"; export { BlueprintNodeConfigSidebar } from "./BlueprintNodeConfigSidebar"; export type BluePrintReactRootProps = { style?: React.CSSProperties; graph: BlueprintGraph; onGraphChange: Dispatch>; selectedNodeId?: string | null; onSelectNode?: (nodeId: string | null) => void; /** 中止正在运行的时钟节点 */ onAbortClock?: (nodeId: string) => void; /** 调试执行时的画布高亮(节点 + 边线信号色) */ executionOverlay?: BlueprintExecutionOverlay | null; /** 蓝图库 id -> 名称,用于画布节点展示引用蓝图名 */ libraryNameById?: ReadonlyMap; }; type BlueprintCanvasProps = Omit & { containerRef: React.RefObject; }; const blueprintEdgeTypes = createBlueprintEdgeTypes(); function BlueprintCanvas({ graph, onGraphChange, selectedNodeId = null, executionOverlay = null, onSelectNode, onAbortClock, libraryNameById, containerRef, }: BlueprintCanvasProps) { const { screenToFlowPosition } = useReactFlow(); const [menu, setMenu] = useState(null); const applyGraphChange = useCallback( (updater: (prev: BlueprintGraph) => BlueprintGraph) => { onGraphChange((prev) => updater(prev)); }, [onGraphChange] ); const { nodes, edges, onNodesChange, onEdgesChange, onNodeDragStop, onConnect, onConnectEnd, isValidConnection, } = useBlueprintFlowState({ graph, selectedNodeId, executionOverlay, libraryNameById, onGraphChange: applyGraphChange, onSelectNode, }); const flowColorMode = useBlueprintFlowColorMode(); useBlueprintFlowViewport(containerRef, true); const onPaneContextMenu = useCallback( (event: MouseEvent | React.MouseEvent) => { event.preventDefault(); setMenu({ kind: "pane", clientX: event.clientX, clientY: event.clientY, }); }, [] ); const onNodeContextMenu = useCallback( ( event: MouseEvent | React.MouseEvent, node: Node ) => { event.preventDefault(); onSelectNode?.(node.id); setMenu({ kind: "node", clientX: event.clientX, clientY: event.clientY, nodeId: node.id, role: node.data.role, }); }, [onSelectNode] ); const onEdgeContextMenu = useCallback( (event: MouseEvent | React.MouseEvent, edge: Edge) => { event.preventDefault(); setMenu({ kind: "edge", clientX: event.clientX, clientY: event.clientY, edgeId: edge.id, }); }, [] ); const onEdgeClick = useCallback(() => { setMenu(null); }, []); const onPaneClick = useCallback(() => { setMenu(null); onSelectNode?.(null); }, [onSelectNode]); const setGraph = useCallback( (updater: (prev: BlueprintGraph) => BlueprintGraph) => { applyGraphChange(updater); }, [applyGraphChange] ); const handleAddBlueprintNode = useCallback( (clientX: number, clientY: number) => { const position = clientToFlowNodePosition( (point) => screenToFlowPosition(point, { snapToGrid: false }), clientX, clientY ); setGraph((prev) => prev.addBlueprintNode(position)); }, [screenToFlowPosition, setGraph] ); const handleDeleteNode = useCallback( (nodeId: string) => { if (nodeId === selectedNodeId) { onSelectNode?.(null); } setGraph((prev) => prev.removeNode(nodeId)); }, [onSelectNode, selectedNodeId, setGraph] ); const handleDeleteEdge = useCallback( (edgeId: string) => { setGraph((prev) => prev.removeEdge(edgeId)); }, [setGraph] ); return ( <> setMenu(null)} onAddBlueprintNode={handleAddBlueprintNode} onDeleteNode={handleDeleteNode} onDeleteEdge={handleDeleteEdge} /> ); } export const BluePrintReactRoot: FC = ({ style, graph, onGraphChange, selectedNodeId, executionOverlay, onSelectNode, onAbortClock, libraryNameById, }) => { const containerRef = useRef(null); return (
); };