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 | 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); 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 (
{ 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)" /> { if (!open) setEditingNodeId(null); }} agent={editingAgentProfile} onSubmit={handleAgentEditSubmit} />
); }