import { useCallback, useEffect, useMemo, useState } from "react"; import { ReactFlow, Background, Controls, MiniMap, addEdge, useNodesState, useEdgesState, type Connection, type Node, type Edge, } from "@xyflow/react"; import "@xyflow/react/dist/style.css"; import { nodeTypes, type NodeStatus } from "./TypedNode"; import { Inspector } from "./Inspector"; import { NODE_KINDS, NODE_ORDER } from "./nodeCatalog"; import { exportDsl, validate, type Issue, type TaskDsl } from "../lib/dsl"; import type { RunPhase } from "../lib/run"; let seq = 0; // 编排 Studio:左节点面板 · 中画布 · 右检查器 · 顶工具栏。 export function StudioView({ onRun, phase, }: { onRun: (dsl: TaskDsl) => void; phase: RunPhase; }) { const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [selId, setSelId] = useState(null); const [issues, setIssues] = useState(null); const onConnect = useCallback((c: Connection) => setEdges((es) => addEdge(c, es)), [setEdges]); const addNode = useCallback( (kind: string) => { const id = `n${++seq}`; const k = NODE_KINDS[kind]; setNodes((ns) => [ ...ns, { id, type: "typed", position: { x: 120 + ((ns.length * 40) % 360), y: 60 + ns.length * 64 }, data: { kind, label: k.label, config: { ...k.defaults }, status: "idle" as NodeStatus }, }, ]); }, [setNodes], ); const patchNode = useCallback( (id: string, patch: Record) => setNodes((ns) => ns.map((n) => (n.id === id ? { ...n, data: { ...n.data, ...patch } } : n))), [setNodes], ); const deleteNode = useCallback( (id: string) => { setNodes((ns) => ns.filter((n) => n.id !== id)); setEdges((es) => es.filter((e) => e.source !== id && e.target !== id)); setSelId(null); }, [setNodes, setEdges], ); // 运行状态映射到节点徽标:流式中全部 running,完成 done,空闲清除。 useEffect(() => { const status: NodeStatus = phase === "streaming" || phase === "submitting" ? "running" : phase === "done" ? "done" : phase === "error" ? "error" : "idle"; setNodes((ns) => ns.map((n) => ({ ...n, data: { ...n.data, status } }))); }, [phase, setNodes]); const run = useCallback(() => { const found = validate(nodes, edges); setIssues(found); if (!found.some((i) => i.level === "error")) onRun(exportDsl(nodes, edges)); }, [nodes, edges, onRun]); const selected = useMemo(() => nodes.find((n) => n.id === selId) ?? null, [nodes, selId]); const running = phase === "submitting" || phase === "streaming"; return (
{/* 左节点面板 */}
节点
{NODE_ORDER.map((kind) => { const k = NODE_KINDS[kind]; return ( ); })}
{/* 中画布 */}
{nodes.length} 节点 · {edges.length} 连线 {issues && ( {issues.length === 0 ? ( ✓ 校验通过 ) : ( {issues.length} 项提示 )} )}
setSelId(n.id)} onPaneClick={() => setSelId(null)} fitView > {issues && issues.length > 0 && (
{issues.map((i, idx) => (
{i.level === "error" ? "✗" : "⚠"} {i.msg}
))}
)}
{/* 右检查器 */}
); }