Spaces:
Running
Running
| import { useCallback, useMemo, useState } from "react"; | |
| import { | |
| ReactFlow, | |
| Background, | |
| Controls, | |
| MiniMap, | |
| type Edge, | |
| type NodeTypes, | |
| type EdgeTypes, | |
| } from "@xyflow/react"; | |
| import "@xyflow/react/dist/style.css"; | |
| import { AgentNode } from "./AgentNode"; | |
| import { ConditionalEdge } from "./ConditionalEdge"; | |
| import { EdgeConditionDialog } from "./EdgeConditionDialog"; | |
| import { AgentForm } from "@/components/agents/AgentForm"; | |
| import { useGraphStore, type AgentNodeData, type EdgeData } from "@/stores/graphStore"; | |
| import { useAgentStore } from "@/stores/agentStore"; | |
| import type { AgentCreateRequest } from "@/types/agent"; | |
| export function GraphEditor() { | |
| const { | |
| nodes, | |
| edges, | |
| onNodesChange, | |
| onEdgesChange, | |
| onConnect, | |
| addAgentNode, | |
| removeNode, | |
| setEdgeCondition, | |
| editingNodeId, | |
| setEditingNodeId, | |
| updateNodeData, | |
| } = useGraphStore(); | |
| const agents = useAgentStore((s) => s.agents); | |
| const updateAgent = useAgentStore((s) => s.updateAgent); | |
| const [selectedEdge, setSelectedEdge] = useState<Edge<EdgeData> | null>(null); | |
| const [conditionDialogOpen, setConditionDialogOpen] = useState(false); | |
| const nodeTypes: NodeTypes = useMemo(() => ({ agentNode: AgentNode }), []); | |
| const edgeTypes: EdgeTypes = useMemo(() => ({ conditionEdge: ConditionalEdge }), []); | |
| const handleEdgeClick = useCallback((_: React.MouseEvent, edge: Edge) => { | |
| setSelectedEdge(edge as Edge<EdgeData>); | |
| setConditionDialogOpen(true); | |
| }, []); | |
| const handleEdgeConditionSave = useCallback( | |
| (edgeId: string, condition: string, weight: number) => { | |
| setEdgeCondition(edgeId, condition, weight); | |
| }, | |
| [setEdgeCondition] | |
| ); | |
| const handleDragOver = useCallback((e: React.DragEvent) => { | |
| e.preventDefault(); | |
| e.dataTransfer.dropEffect = "move"; | |
| }, []); | |
| const handleDrop = useCallback( | |
| (e: React.DragEvent) => { | |
| e.preventDefault(); | |
| const agentId = e.dataTransfer.getData("application/agent-id"); | |
| if (!agentId) return; | |
| const agent = agents.find((a) => a.agent_id === agentId); | |
| if (!agent) return; | |
| const reactFlowBounds = (e.target as HTMLElement) | |
| .closest(".react-flow") | |
| ?.getBoundingClientRect(); | |
| if (!reactFlowBounds) return; | |
| const position = { | |
| x: e.clientX - reactFlowBounds.left - 100, | |
| y: e.clientY - reactFlowBounds.top - 60, | |
| }; | |
| addAgentNode(agent, position); | |
| }, | |
| [agents, addAgentNode] | |
| ); | |
| const handleKeyDown = useCallback( | |
| (e: React.KeyboardEvent) => { | |
| if (e.key === "Delete" || e.key === "Backspace") { | |
| const selectedNodes = nodes.filter((n) => n.selected); | |
| selectedNodes.forEach((n) => removeNode(n.id)); | |
| } | |
| }, | |
| [nodes, removeNode] | |
| ); | |
| // Build AgentProfile-like object from the node being edited | |
| const editingNode = editingNodeId | |
| ? nodes.find((n) => n.id === editingNodeId) | |
| : null; | |
| const editingAgentProfile = editingNode | |
| ? { | |
| agent_id: editingNode.data.agentId, | |
| display_name: editingNode.data.displayName, | |
| persona: editingNode.data.persona, | |
| description: editingNode.data.description, | |
| llm_backbone: editingNode.data.llmBackbone, | |
| tools: editingNode.data.tools, | |
| llm_config: null, | |
| input_schema: null, | |
| output_schema: null, | |
| } | |
| : null; | |
| const handleAgentEditSubmit = async (data: AgentCreateRequest) => { | |
| if (!editingNodeId) return; | |
| // Update the node on the canvas | |
| updateNodeData(editingNodeId, { | |
| displayName: data.display_name, | |
| persona: data.persona || "", | |
| description: data.description || "", | |
| llmBackbone: data.llm_backbone || null, | |
| tools: data.tools || [], | |
| }); | |
| // Also update the agent on the backend | |
| try { | |
| await updateAgent(editingNodeId, data); | |
| } catch { | |
| // agent may not exist on server yet (template-only) | |
| } | |
| }; | |
| return ( | |
| <div className="flex-1 h-full" onKeyDown={handleKeyDown} tabIndex={0}> | |
| <ReactFlow | |
| nodes={nodes} | |
| edges={edges} | |
| onNodesChange={onNodesChange} | |
| onEdgesChange={onEdgesChange} | |
| onConnect={onConnect} | |
| onEdgeClick={handleEdgeClick} | |
| onDragOver={handleDragOver} | |
| onDrop={handleDrop} | |
| nodeTypes={nodeTypes} | |
| edgeTypes={edgeTypes} | |
| fitView | |
| snapToGrid | |
| snapGrid={[15, 15]} | |
| defaultEdgeOptions={{ | |
| type: "conditionEdge", | |
| animated: true, | |
| }} | |
| deleteKeyCode={["Backspace", "Delete"]} | |
| > | |
| <Background gap={15} size={1} /> | |
| <Controls /> | |
| <MiniMap | |
| nodeColor={(n) => { | |
| const data = n.data as unknown as AgentNodeData; | |
| switch (data.executionStatus) { | |
| case "running": | |
| return "#3b82f6"; | |
| case "completed": | |
| return "#22c55e"; | |
| case "error": | |
| return "#ef4444"; | |
| case "pending": | |
| return "#eab308"; | |
| default: | |
| return "#94a3b8"; | |
| } | |
| }} | |
| maskColor="rgba(0, 0, 0, 0.1)" | |
| /> | |
| </ReactFlow> | |
| <EdgeConditionDialog | |
| open={conditionDialogOpen} | |
| onOpenChange={setConditionDialogOpen} | |
| edgeId={selectedEdge?.id || ""} | |
| currentCondition={(selectedEdge?.data as EdgeData | undefined)?.condition} | |
| currentWeight={(selectedEdge?.data as EdgeData | undefined)?.weight ?? 1.0} | |
| onSave={handleEdgeConditionSave} | |
| /> | |
| <AgentForm | |
| open={!!editingNodeId} | |
| onOpenChange={(open) => { if (!open) setEditingNodeId(null); }} | |
| agent={editingAgentProfile} | |
| onSubmit={handleAgentEditSubmit} | |
| /> | |
| </div> | |
| ); | |
| } | |