summaryrefslogblamecommitdiff
path: root/makima/frontend/src/components/chains/ChainEditor.tsx
blob: 5b77170bcdef6d32b47d9ae0ad40fcff866f681a (plain) (tree)



















                                                                  


                     






                               
                        



                          

                       



                                              
                             

  








                                                

                             
                       


































































































































































                                                                                          








                             





                                                                                                    
                                                                            
 




                                                                                    
 
















                                                          
















































































                                                                                             
         
       


                             
 



                                                           
 


                                                                            
 
                                           
 






                                                                    
 














                                                                                       
 



                                          
      
      

    




                                             
       


                                      
 



                                             
 



                                                              
 
                                          




                                                                        




                                               







                                         



                                                                         













































                                                                                     
         

                                 
                   


                                                                             
     
                            
 



                                                                      
         

                                
                   


                                                                            
     
                            
 



                                                           
 
                                                                              
           
                                                                              
 








                                                                                     
       


                           
 









                                                                                                 
                      

                     


                                                                                       




                                                   
                                                            
                                                           

                 
                            
                   
                                                                      






                                                                                                                                                                    









                                                                                                                                                              







                                                                                                                                                          




                                                                                                          



                                           

                                             



                                                                                  
                                    


                                                                     


                                                       
                    



                                                                             
                    
                                                






                                                                                                                                                    


                    

















                                                                              
                

                                                                         
             










                                                                                     



                            


                                           
                                                   
                                             
                                                       






                                                                                      


                                                           
                           



                                                                                  


                                                                                 

                   
                                         
                           











                                                                                   

                                                                                             


















                                                                                            



          
                               



                                      
                                                                





                                
                     
                                

                                                       



                                                                                               
                                                                                            



                                                                             
               


















                                                                                         
                                                                                        


                
                    

                                                                                       
                
                  
                                                                                       


                            
                                     

                                                                                         
                          

                                       



                                                                                                            
                 






                                                                                     





                      

                                                                      





                                                                                                                                      





              
                       





                                                        
                                                                                             




                                                             

                                                     


                              
                                               


                                                   
                                                            
                                                              
      
                  

















                                                                                              

                                                                                         




                                                       
                                                                                                                                                                        
                                                




                             

                                                                                         



                                                              

                                                                                                                                                                                         


                
                      
               
                                                                                         




                                                               
                                                                                                                                                                        

                                                    
                                                                         


                     
                                                     

                             
                                                                                           




                                                                 
                                                                                                                                                                          


                                                        



                       


                                        
                                                                                           

                          
                                                    
                                                 











                                                                                                



                    
              
 














                                                                                                                                                                





              
        

                                                           





                                                                                                                                              


          


                                                           





                                                                                                     


          
import { useState, useCallback, useEffect, useMemo } from "react";
import {
  ReactFlow,
  Node,
  Edge,
  Controls,
  Background,
  useNodesState,
  useEdgesState,
  addEdge,
  Connection,
  NodeProps,
  Handle,
  Position,
  BackgroundVariant,
  NodeChange,
  MarkerType,
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";

import type {
  ChainWithContracts,
  ChainGraphResponse,
  ChainContractDefinition,
  ChainDefinitionGraphResponse,
  AddContractDefinitionRequest,
} from "../../lib/api";
import {
  listChainDefinitions,
  createChainDefinition,
  updateChainDefinition,
  deleteChainDefinition,
  getChainDefinitionGraph,
  startChain,
  stopChain,
} from "../../lib/api";

const statusColors: Record<string, string> = {
  active: "text-green-400",
  completed: "text-blue-400",
  archived: "text-[#555]",
  pending: "text-yellow-400",
};

interface ChainEditorProps {
  chain: ChainWithContracts;
  graph: ChainGraphResponse | null;
  loading: boolean;
  onBack: () => void;
  onRefresh: () => void;
  onContractClick: (contractId: string) => void;
}

// Node dimensions for layout
const NODE_WIDTH = 200;
const NODE_HEIGHT = 80;
const GRID_SPACING_X = 280;
const GRID_SPACING_Y = 120;

// Custom node component for definitions
function DefinitionNode({ data, selected }: NodeProps) {
  const isCheckpoint = data.contractType === "checkpoint";
  const status = data.isInstantiated ? data.contractStatus || "pending" : "pending";
  const colors = getStatusColor(status, isCheckpoint);

  return (
    <div
      className={`rounded-lg border-2 overflow-hidden ${
        isCheckpoint ? "bg-[#0f0a1e]" : "bg-[#0a1628]"
      } ${selected ? "ring-2 ring-offset-2 ring-offset-[#050d18]" : ""} ${
        selected ? (isCheckpoint ? "ring-[#a78bfa]" : "ring-[#75aafc]") : ""
      }`}
      style={{
        width: NODE_WIDTH,
        height: NODE_HEIGHT,
        borderColor: selected
          ? isCheckpoint
            ? "#a78bfa"
            : "#75aafc"
          : colors.border,
        borderStyle: data.isInstantiated ? "solid" : "dashed",
      }}
    >
      {/* Top handle for incoming edges */}
      <Handle
        type="target"
        position={Position.Top}
        className="!bg-[#75aafc] !w-3 !h-3 !border-2 !border-[#0a1628]"
      />

      {/* Status indicator bar */}
      <div className="h-1.5" style={{ backgroundColor: colors.bg }} />

      {/* Content */}
      <div className="p-2">
        <div className="flex items-center justify-between mb-1">
          <span className="font-mono text-xs text-[#dbe7ff] truncate flex-1">
            {data.name}
          </span>
          {isCheckpoint ? (
            <CheckIcon className="w-4 h-4 text-[#a78bfa] opacity-70 flex-shrink-0 ml-1" />
          ) : (
            <ChainIcon className="w-4 h-4 text-[#75aafc] opacity-50 flex-shrink-0 ml-1" />
          )}
        </div>
        <div className="flex items-center justify-between">
          <span
            className="font-mono text-[10px] uppercase px-1.5 py-0.5 rounded"
            style={{
              color: colors.bg,
              backgroundColor: `${colors.bg}20`,
            }}
          >
            {data.isInstantiated ? status : isCheckpoint ? "checkpoint" : "definition"}
          </span>
          <span className="font-mono text-[10px] text-[#8b949e]">
            {data.contractType}
          </span>
        </div>
      </div>

      {/* Bottom handle for outgoing edges */}
      <Handle
        type="source"
        position={Position.Bottom}
        className="!bg-[#f59e0b] !w-3 !h-3 !border-2 !border-[#0a1628]"
      />
    </div>
  );
}

// Custom node for contracts (active chains)
function ContractNode({ data, selected }: NodeProps) {
  const colors = getStatusColor(data.status);

  return (
    <div
      className={`rounded-lg border-2 bg-[#0a1628] overflow-hidden ${
        selected ? "ring-2 ring-[#75aafc] ring-offset-2 ring-offset-[#050d18]" : ""
      }`}
      style={{
        width: NODE_WIDTH,
        height: NODE_HEIGHT,
        borderColor: selected ? "#75aafc" : colors.border,
      }}
    >
      <Handle
        type="target"
        position={Position.Top}
        className="!bg-[#75aafc] !w-3 !h-3 !border-2 !border-[#0a1628]"
      />

      <div className="h-1.5" style={{ backgroundColor: colors.bg }} />

      <div className="p-2">
        <div className="flex items-center justify-between mb-1">
          <span className="font-mono text-xs text-[#dbe7ff] truncate flex-1">
            {data.name}
          </span>
          <ChainIcon className="w-4 h-4 text-[#75aafc] opacity-50 flex-shrink-0 ml-1" />
        </div>
        <div className="flex items-center justify-between">
          <span
            className="font-mono text-[10px] uppercase px-1.5 py-0.5 rounded"
            style={{
              color: colors.bg,
              backgroundColor: `${colors.bg}20`,
            }}
          >
            {data.status}
          </span>
          {data.phase && (
            <span className="font-mono text-[10px] text-[#8b949e]">{data.phase}</span>
          )}
        </div>
      </div>

      <Handle
        type="source"
        position={Position.Bottom}
        className="!bg-[#f59e0b] !w-3 !h-3 !border-2 !border-[#0a1628]"
      />
    </div>
  );
}

const nodeTypes = {
  definition: DefinitionNode,
  contract: ContractNode,
};

function getStatusColor(status: string, isCheckpoint = false) {
  if (isCheckpoint) {
    switch (status) {
      case "active":
        return { bg: "#a78bfa", border: "#8b5cf6", text: "#5b21b6" };
      case "completed":
        return { bg: "#818cf8", border: "#6366f1", text: "#3730a3" };
      case "pending":
        return { bg: "#c4b5fd", border: "#a78bfa", text: "#6d28d9" };
      case "failed":
        return { bg: "#ef4444", border: "#dc2626", text: "#991b1b" };
      default:
        return { bg: "#a78bfa", border: "#8b5cf6", text: "#5b21b6" };
    }
  }
  switch (status) {
    case "active":
      return { bg: "#4ade80", border: "#22c55e", text: "#166534" };
    case "completed":
      return { bg: "#60a5fa", border: "#3b82f6", text: "#1e40af" };
    case "pending":
      return { bg: "#f59e0b", border: "#d97706", text: "#92400e" };
    case "blocked":
      return { bg: "#ef4444", border: "#dc2626", text: "#991b1b" };
    default:
      return { bg: "#6b7280", border: "#4b5563", text: "#374151" };
  }
}

export function ChainEditor({
  chain,
  graph,
  loading,
  onBack,
  onRefresh,
  onContractClick,
}: ChainEditorProps) {
  const [definitions, setDefinitions] = useState<ChainContractDefinition[]>([]);
  const [definitionGraph, setDefinitionGraph] = useState<ChainDefinitionGraphResponse | null>(null);
  const [showAddDefinition, setShowAddDefinition] = useState(false);
  const [isStarting, setIsStarting] = useState(false);
  const [isStopping, setIsStopping] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);

  const [nodes, setNodes, onNodesChange] = useNodesState([]);
  const [edges, setEdges, onEdgesChange] = useEdgesState([]);

  const showDefinitions = chain.status === "pending" || chain.status === "archived";
  const canEdit = chain.status === "pending";

  // Load definitions when chain changes
  useEffect(() => {
    async function loadDefinitions() {
      try {
        const [defs, defGraph] = await Promise.all([
          listChainDefinitions(chain.id),
          getChainDefinitionGraph(chain.id),
        ]);
        setDefinitions(defs);
        setDefinitionGraph(defGraph);
      } catch (err) {
        console.error("Failed to load definitions:", err);
      }
    }
    loadDefinitions();
  }, [chain.id]);

  // Convert definitions/contracts to React Flow nodes and edges
  useEffect(() => {
    if (showDefinitions && definitionGraph) {
      const flowNodes: Node[] = definitionGraph.nodes.map((node) => ({
        id: node.id,
        type: "definition",
        position: {
          x: (node.x || 0) * GRID_SPACING_X,
          y: (node.y || 0) * GRID_SPACING_Y,
        },
        data: {
          name: node.name,
          contractType: node.contractType,
          isInstantiated: node.isInstantiated,
          contractStatus: node.contractStatus,
        },
        draggable: canEdit,
      }));

      const flowEdges: Edge[] = definitionGraph.edges.map((edge, index) => ({
        id: `${edge.from}-${edge.to}-${index}`,
        source: edge.from,
        target: edge.to,
        type: "smoothstep",
        animated: false,
        style: { stroke: "#3f6fb3", strokeWidth: 2 },
        markerEnd: { type: MarkerType.ArrowClosed, color: "#75aafc" },
      }));

      setNodes(flowNodes);
      setEdges(flowEdges);
    } else if (!showDefinitions && graph) {
      const flowNodes: Node[] = graph.nodes.map((node) => ({
        id: node.contractId,
        type: "contract",
        position: {
          x: (node.x || 0) * GRID_SPACING_X,
          y: (node.y || 0) * GRID_SPACING_Y,
        },
        data: {
          name: node.name,
          status: node.status,
          phase: node.phase,
        },
        draggable: false,
      }));

      const flowEdges: Edge[] = graph.edges.map((edge, index) => ({
        id: `${edge.from}-${edge.to}-${index}`,
        source: edge.from,
        target: edge.to,
        type: "smoothstep",
        animated: edge.from === selectedNodeId || edge.to === selectedNodeId,
        style: { stroke: "#3f6fb3", strokeWidth: 2 },
        markerEnd: { type: MarkerType.ArrowClosed, color: "#75aafc" },
      }));

      setNodes(flowNodes);
      setEdges(flowEdges);
    }
  }, [showDefinitions, definitionGraph, graph, canEdit, selectedNodeId, setNodes, setEdges]);

  // Handle node position changes (drag end)
  const handleNodesChange = useCallback(
    async (changes: NodeChange[]) => {
      onNodesChange(changes);

      // Save position changes to backend
      for (const change of changes) {
        if (change.type === "position" && change.dragging === false && change.position) {
          const gridX = Math.round(change.position.x / GRID_SPACING_X);
          const gridY = Math.round(change.position.y / GRID_SPACING_Y);

          try {
            await updateChainDefinition(chain.id, change.id, {
              editorX: gridX,
              editorY: gridY,
            });
          } catch (err) {
            console.error("Failed to save position:", err);
          }
        }
      }
    },
    [chain.id, onNodesChange]
  );

  // Handle new edge connections
  const handleConnect = useCallback(
    async (connection: Connection) => {
      if (!connection.source || !connection.target) return;

      // Find the definitions
      const sourceDef = definitions.find((d) => d.id === connection.source);
      const targetDef = definitions.find((d) => d.id === connection.target);

      if (!sourceDef || !targetDef) return;

      // Add dependency: target depends on source
      const currentDeps = targetDef.dependsOnNames || [];
      if (!currentDeps.includes(sourceDef.name)) {
        try {
          await updateChainDefinition(chain.id, connection.target, {
            dependsOn: [...currentDeps, sourceDef.name],
          });

          // Refresh
          const [defs, defGraph] = await Promise.all([
            listChainDefinitions(chain.id),
            getChainDefinitionGraph(chain.id),
          ]);
          setDefinitions(defs);
          setDefinitionGraph(defGraph);
        } catch (err) {
          console.error("Failed to create dependency:", err);
          setError(err instanceof Error ? err.message : "Failed to create dependency");
        }
      }
    },
    [chain.id, definitions]
  );

  // Handle node selection
  const handleNodeClick = useCallback(
    (_: React.MouseEvent, node: Node) => {
      setSelectedNodeId(node.id);
    },
    []
  );

  // Handle node double-click (open contract)
  const handleNodeDoubleClick = useCallback(
    (_: React.MouseEvent, node: Node) => {
      if (!showDefinitions) {
        onContractClick(node.id);
      }
    },
    [showDefinitions, onContractClick]
  );

  // Handle pane click (deselect)
  const handlePaneClick = useCallback(() => {
    setSelectedNodeId(null);
  }, []);

  // Find selected definition
  const selectedDefinition = showDefinitions && selectedNodeId
    ? definitions.find((d) => d.id === selectedNodeId)
    : null;

  // Find free position for new definition
  const findFreePosition = useCallback(() => {
    if (!definitionGraph?.nodes || definitionGraph.nodes.length === 0) {
      return { x: 0, y: 0 };
    }

    const occupied = new Set<string>();
    for (const node of definitionGraph.nodes) {
      occupied.add(`${node.x},${node.y}`);
    }

    for (let y = 0; y < 10; y++) {
      for (let x = 0; x < 10; x++) {
        if (!occupied.has(`${x},${y}`)) {
          return { x, y };
        }
      }
    }

    const maxY = Math.max(...definitionGraph.nodes.map((n) => n.y || 0));
    return { x: 0, y: maxY + 1 };
  }, [definitionGraph?.nodes]);

  const handleAddDefinition = useCallback(
    async (req: AddContractDefinitionRequest) => {
      try {
        const position = findFreePosition();
        const reqWithPosition = { ...req, editorX: position.x, editorY: position.y };
        await createChainDefinition(chain.id, reqWithPosition);

        const [defs, defGraph] = await Promise.all([
          listChainDefinitions(chain.id),
          getChainDefinitionGraph(chain.id),
        ]);
        setDefinitions(defs);
        setDefinitionGraph(defGraph);
        setShowAddDefinition(false);
      } catch (err) {
        console.error("Failed to add definition:", err);
        setError(err instanceof Error ? err.message : "Failed to add definition");
      }
    },
    [chain.id, findFreePosition]
  );

  const handleDeleteDefinition = useCallback(
    async (definitionId: string) => {
      if (!confirm("Are you sure you want to delete this definition?")) return;
      try {
        await deleteChainDefinition(chain.id, definitionId);

        const [defs, defGraph] = await Promise.all([
          listChainDefinitions(chain.id),
          getChainDefinitionGraph(chain.id),
        ]);
        setDefinitions(defs);
        setDefinitionGraph(defGraph);
        setSelectedNodeId(null);
      } catch (err) {
        console.error("Failed to delete definition:", err);
        setError(err instanceof Error ? err.message : "Failed to delete definition");
      }
    },
    [chain.id]
  );

  const handleStartChain = useCallback(async () => {
    setIsStarting(true);
    setError(null);
    try {
      await startChain(chain.id);
      onRefresh();
    } catch (err) {
      setError(err instanceof Error ? err.message : "Failed to start chain");
    } finally {
      setIsStarting(false);
    }
  }, [chain.id, onRefresh]);

  const handleStopChain = useCallback(async () => {
    if (!confirm("Are you sure you want to stop this chain?")) return;
    setIsStopping(true);
    setError(null);
    try {
      await stopChain(chain.id);
      onRefresh();
    } catch (err) {
      setError(err instanceof Error ? err.message : "Failed to stop chain");
    } finally {
      setIsStopping(false);
    }
  }, [chain.id, onRefresh]);

  const handleRemoveDependency = useCallback(
    async (nodeId: string, depName: string) => {
      const def = definitions.find((d) => d.id === nodeId);
      if (!def) return;

      const newDeps = (def.dependsOnNames || []).filter((d) => d !== depName);
      try {
        await updateChainDefinition(chain.id, nodeId, { dependsOn: newDeps });

        const [defs, defGraph] = await Promise.all([
          listChainDefinitions(chain.id),
          getChainDefinitionGraph(chain.id),
        ]);
        setDefinitions(defs);
        setDefinitionGraph(defGraph);
      } catch (err) {
        console.error("Failed to remove dependency:", err);
        setError(err instanceof Error ? err.message : "Failed to remove dependency");
      }
    },
    [chain.id, definitions]
  );

  return (
    <div className="panel h-full flex flex-col">
      {/* Header */}
      <div className="p-3 border-b border-[rgba(117,170,252,0.2)]">
        <div className="flex items-center justify-between">
          <div className="flex items-center gap-3">
            <button
              onClick={onBack}
              className="font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors"
            >
              ← Back
            </button>
            <div>
              <h2 className="font-mono text-sm text-[#dbe7ff]">{chain.name}</h2>
              {chain.description && (
                <p className="font-mono text-xs text-[#8b949e]">{chain.description}</p>
              )}
            </div>
          </div>
          <div className="flex items-center gap-2">
            <span
              className={`font-mono text-[10px] uppercase ${
                statusColors[chain.status] || "text-[#555]"
              }`}
            >
              {chain.status}
            </span>
            {chain.status === "pending" && definitions.length > 0 && (
              <button
                onClick={handleStartChain}
                disabled={isStarting}
                className="px-3 py-1 font-mono text-xs text-[#dbe7ff] bg-green-600 hover:bg-green-700 border border-green-500 transition-colors disabled:opacity-50"
              >
                {isStarting ? "Starting..." : "Start Chain"}
              </button>
            )}
            {chain.status === "active" && (
              <button
                onClick={handleStopChain}
                disabled={isStopping}
                className="px-3 py-1 font-mono text-xs text-[#dbe7ff] bg-red-600 hover:bg-red-700 border border-red-500 transition-colors disabled:opacity-50"
              >
                {isStopping ? "Stopping..." : "Stop"}
              </button>
            )}
            <button
              onClick={onRefresh}
              className="px-3 py-1 font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] border border-[#3f6fb3] hover:border-[#75aafc] transition-colors"
            >
              Refresh
            </button>
          </div>
        </div>
        {error && (
          <div className="mt-2 p-2 bg-red-400/10 border border-red-400/30 text-red-400 font-mono text-xs">
            {error}
          </div>
        )}
      </div>

      {/* Main content */}
      <div className="flex-1 flex min-h-0">
        {/* React Flow Canvas */}
        <div className="flex-1 bg-[#050d18]">
          {loading ? (
            <div className="flex items-center justify-center h-full">
              <p className="font-mono text-xs text-[#8b949e]">Loading graph...</p>
            </div>
          ) : nodes.length === 0 ? (
            <div className="flex items-center justify-center h-full">
              <div className="text-center">
                <p className="font-mono text-sm text-[#8b949e] mb-2">
                  {showDefinitions
                    ? "No contract definitions yet"
                    : "No contracts in this chain yet"}
                </p>
                <p className="font-mono text-xs text-[#556677] mb-4">
                  {showDefinitions
                    ? "Add contract definitions to build your chain"
                    : "Start the chain to create contracts from definitions"}
                </p>
                {showDefinitions && canEdit && (
                  <button
                    onClick={() => setShowAddDefinition(true)}
                    className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors"
                  >
                    + Add Definition
                  </button>
                )}
              </div>
            </div>
          ) : (
            <ReactFlow
              nodes={nodes}
              edges={edges}
              onNodesChange={handleNodesChange}
              onEdgesChange={onEdgesChange}
              onConnect={canEdit ? handleConnect : undefined}
              onNodeClick={handleNodeClick}
              onNodeDoubleClick={handleNodeDoubleClick}
              onPaneClick={handlePaneClick}
              nodeTypes={nodeTypes}
              fitView
              fitViewOptions={{ padding: 0.2 }}
              minZoom={0.5}
              maxZoom={2}
              defaultEdgeOptions={{
                type: "smoothstep",
                style: { stroke: "#3f6fb3", strokeWidth: 2 },
                markerEnd: { type: MarkerType.ArrowClosed, color: "#75aafc" },
              }}
              connectionLineStyle={{ stroke: "#f59e0b", strokeWidth: 2 }}
              proOptions={{ hideAttribution: true }}
            >
              <Background
                variant={BackgroundVariant.Dots}
                gap={20}
                size={1}
                color="#1a2744"
              />
              <Controls
                className="!bg-[#0a1628] !border-[rgba(117,170,252,0.3)] !rounded-lg"
                showInteractive={false}
              />
            </ReactFlow>
          )}
        </div>

        {/* Detail panel */}
        {selectedDefinition && (
          <DefinitionDetailPanel
            definition={selectedDefinition}
            onClose={() => setSelectedNodeId(null)}
            onDelete={handleDeleteDefinition}
            onRemoveDependency={handleRemoveDependency}
          />
        )}
      </div>

      {/* Footer with stats */}
      <div className="p-3 border-t border-[rgba(117,170,252,0.2)] bg-[#0a1628]">
        <div className="flex items-center gap-4 font-mono text-[10px] text-[#8b949e]">
          {showDefinitions ? (
            <>
              <span>{definitions.length} definitions</span>
              {canEdit && (
                <>
                  <span className="text-[#556677]">|</span>
                  <span className="text-[#556677]">Drag nodes to reposition</span>
                  <span className="text-[#556677]">|</span>
                  <span className="text-[#556677]">
                    Drag from <span className="text-[#f59e0b]">●</span> to link
                  </span>
                </>
              )}
              <span className="flex-1" />
              {canEdit && (
                <button
                  onClick={() => setShowAddDefinition(true)}
                  className="text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors"
                >
                  + Add Definition
                </button>
              )}
            </>
          ) : (
            <>
              <span>{chain.contracts.length} contracts</span>
              <span>
                {chain.contracts.filter((c) => c.contractStatus === "completed").length}{" "}
                completed
              </span>
              <span>
                {chain.contracts.filter((c) => c.contractStatus === "active").length} active
              </span>
              <span className="flex-1" />
              <span>Double-click node to open contract</span>
            </>
          )}
        </div>
      </div>

      {/* Add Definition Modal */}
      {showAddDefinition && (
        <AddDefinitionModal
          existingNames={definitions.map((d) => d.name)}
          onSubmit={handleAddDefinition}
          onCancel={() => setShowAddDefinition(false)}
        />
      )}
    </div>
  );
}

// Detail panel for definitions
interface DefinitionDetailPanelProps {
  definition: ChainContractDefinition;
  onClose: () => void;
  onDelete: (id: string) => void;
  onRemoveDependency: (nodeId: string, depName: string) => void;
}

function DefinitionDetailPanel({
  definition,
  onClose,
  onDelete,
  onRemoveDependency,
}: DefinitionDetailPanelProps) {
  const dependencies = definition.dependsOnNames || [];

  return (
    <div className="w-72 border-l border-[rgba(117,170,252,0.2)] bg-[#0a1628] overflow-y-auto">
      <div className="p-3 border-b border-[rgba(117,170,252,0.2)]">
        <div className="flex items-center justify-between mb-2">
          <h3 className="font-mono text-xs text-[#75aafc] uppercase">Definition Details</h3>
          <button
            onClick={onClose}
            className="font-mono text-xs text-[#8b949e] hover:text-[#dbe7ff]"
          >
            ✕
          </button>
        </div>
      </div>

      <div className="p-3 space-y-4">
        {/* Name */}
        <div>
          <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1">
            Name
          </label>
          <p className="font-mono text-sm text-[#dbe7ff]">{definition.name}</p>
        </div>

        {/* Description */}
        {definition.description && (
          <div>
            <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1">
              Description
            </label>
            <p className="font-mono text-xs text-[#9bc3ff]">{definition.description}</p>
          </div>
        )}

        {/* Type */}
        <div>
          <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1">
            Type
          </label>
          <p className="font-mono text-xs text-[#9bc3ff]">{definition.contractType}</p>
        </div>

        {/* Dependencies */}
        {dependencies.length > 0 && (
          <div>
            <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1">
              Dependencies
            </label>
            <div className="space-y-1">
              {dependencies.map((dep) => (
                <div
                  key={dep}
                  className="flex items-center justify-between bg-[rgba(117,170,252,0.1)] px-2 py-1 rounded"
                >
                  <span className="font-mono text-xs text-[#9bc3ff]">{dep}</span>
                  <button
                    onClick={() => onRemoveDependency(definition.id, dep)}
                    className="font-mono text-[10px] text-red-400 hover:text-red-300"
                  >
                    ✕
                  </button>
                </div>
              ))}
            </div>
          </div>
        )}

        {/* Delete button */}
        <div className="pt-4 border-t border-[rgba(117,170,252,0.2)]">
          <button
            onClick={() => onDelete(definition.id)}
            className="w-full px-3 py-2 font-mono text-xs text-red-400 border border-red-400/30 hover:bg-red-400/10 transition-colors"
          >
            Delete Definition
          </button>
        </div>
      </div>
    </div>
  );
}

// Add Definition Modal
interface AddDefinitionModalProps {
  existingNames: string[];
  onSubmit: (req: AddContractDefinitionRequest) => void;
  onCancel: () => void;
}

function AddDefinitionModal({ existingNames, onSubmit, onCancel }: AddDefinitionModalProps) {
  const [name, setName] = useState("");
  const [description, setDescription] = useState("");
  const [contractType, setContractType] = useState("simple");
  const [initialPhase, setInitialPhase] = useState("plan");
  const [dependsOn, setDependsOn] = useState<string[]>([]);

  const isCheckpoint = contractType === "checkpoint";

  const handleSubmit = () => {
    if (!name.trim()) return;
    const req: AddContractDefinitionRequest = {
      name: name.trim(),
      description: description.trim() || undefined,
      contractType,
      initialPhase: isCheckpoint ? "execute" : initialPhase,
      dependsOn: dependsOn.length > 0 ? dependsOn : undefined,
    };
    onSubmit(req);
  };

  const toggleDependency = (depName: string) => {
    setDependsOn((prev) =>
      prev.includes(depName) ? prev.filter((d) => d !== depName) : [...prev, depName]
    );
  };

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
      <div className="w-full max-w-lg p-6 bg-[#0a1628] border border-[rgba(117,170,252,0.3)]">
        <h3 className="font-mono text-sm text-[#75aafc] uppercase mb-4">
          Add Contract Definition
        </h3>

        <div className="space-y-4">
          {/* Name */}
          <div>
            <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1">
              Name *
            </label>
            <input
              type="text"
              value={name}
              onChange={(e) => setName(e.target.value)}
              className="w-full px-3 py-2 bg-[#050d18] border border-[rgba(117,170,252,0.3)] font-mono text-sm text-[#dbe7ff] focus:border-[#75aafc] focus:outline-none"
              placeholder="e.g., Research Phase"
            />
          </div>

          {/* Description */}
          <div>
            <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1">
              Description
            </label>
            <textarea
              value={description}
              onChange={(e) => setDescription(e.target.value)}
              className="w-full px-3 py-2 bg-[#050d18] border border-[rgba(117,170,252,0.3)] font-mono text-sm text-[#dbe7ff] focus:border-[#75aafc] focus:outline-none resize-none h-20"
              placeholder="Optional description..."
            />
          </div>

          {/* Type */}
          <div>
            <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1">
              Contract Type
            </label>
            <select
              value={contractType}
              onChange={(e) => setContractType(e.target.value)}
              className="w-full px-3 py-2 bg-[#050d18] border border-[rgba(117,170,252,0.3)] font-mono text-sm text-[#dbe7ff] focus:border-[#75aafc] focus:outline-none"
            >
              <option value="simple">Simple</option>
              <option value="checkpoint">Checkpoint (validation)</option>
            </select>
          </div>

          {/* Initial Phase (not for checkpoints) */}
          {!isCheckpoint && (
            <div>
              <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1">
                Initial Phase
              </label>
              <select
                value={initialPhase}
                onChange={(e) => setInitialPhase(e.target.value)}
                className="w-full px-3 py-2 bg-[#050d18] border border-[rgba(117,170,252,0.3)] font-mono text-sm text-[#dbe7ff] focus:border-[#75aafc] focus:outline-none"
              >
                <option value="plan">Plan</option>
                <option value="execute">Execute</option>
              </select>
            </div>
          )}

          {/* Dependencies */}
          {existingNames.length > 0 && (
            <div>
              <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1">
                Depends On
              </label>
              <div className="flex flex-wrap gap-2">
                {existingNames.map((depName) => (
                  <button
                    key={depName}
                    type="button"
                    onClick={() => toggleDependency(depName)}
                    className={`px-2 py-1 font-mono text-xs border transition-colors ${
                      dependsOn.includes(depName)
                        ? "bg-[#75aafc]/20 border-[#75aafc] text-[#75aafc]"
                        : "border-[rgba(117,170,252,0.3)] text-[#8b949e] hover:border-[#75aafc]"
                    }`}
                  >
                    {depName}
                  </button>
                ))}
              </div>
            </div>
          )}
        </div>

        {/* Actions */}
        <div className="flex justify-end gap-2 mt-6">
          <button
            onClick={onCancel}
            className="px-4 py-2 font-mono text-xs text-[#8b949e] border border-[rgba(117,170,252,0.3)] hover:border-[#75aafc] transition-colors"
          >
            Cancel
          </button>
          <button
            onClick={handleSubmit}
            disabled={!name.trim()}
            className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors disabled:opacity-50"
          >
            Add Definition
          </button>
        </div>
      </div>
    </div>
  );
}

// Icons
function ChainIcon({ className }: { className?: string }) {
  return (
    <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
      <path
        strokeLinecap="round"
        strokeLinejoin="round"
        d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
      />
    </svg>
  );
}

function CheckIcon({ className }: { className?: string }) {
  return (
    <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
      <path
        strokeLinecap="round"
        strokeLinejoin="round"
        d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
      />
    </svg>
  );
}