summaryrefslogblamecommitdiff
path: root/makima/frontend/src/components/chains/ChainEditor.tsx
blob: a4c8a39414a931712d7e08ed11a0be29c2814bea (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
                                                                          



                      






                               
                        



                          

                       



                                              
                             

  
























                                                                        






                                                                                                    










                                                                                                         
                                                                


                                                  

                                                      

                  



















                                                                                              
 
                                                                      
                                       
                                                                  


                                                    









                                                                                       







                                                                 
     
 
                     
                                                                              



















                                                   

                                                           


                                            



                                                      
      
                                      

    















                                                                       













                                                                     




                                                            


                                                                



                                                    
                                 





                                                                             
                            














                                                                            




























                                                                                                            

                                                                                        



                                                                                   







                                                  
                                     



                                                                                
                                                          


















                                                                                   

















                                                                                  





















                                                                               



                                              






                                                                                














































































































































                                                                                         












                                                                                                 


                                                                                       




                                                   
                                                            
                                                           

                 
                            
                   

                                                                      






                                                                                                                                                                    









                                                                                                                                                              







                                                                                                                                                          




                                                                                                          









                                                                                  
                                                                  


                                                                     


                                                       
                    



                                                                             
                    







                                                                                                                                                    










                                                


                                                                                         
                                                     










                                                         
























                                                                













                                               
                       
                                                          




























                                                                                                       




















                                                                                                       


                                




                                                           
                                                                            


                                                        
                                                                        


                                                                


                                                               



                                                                



                                                                                       


                                                                                       
                                    


                                                                                           
                                
                           




                                              

                                                                                           

                          
                                                         
                            


                                                                                          
                                  




                                                   

                                                                                  
                                                                                     











                                                                                                 




                                                                                                              








                                                                                                 
                                                                                                           






                                                                                     







                                                                                                                                                                                                      





                                                                   
 


                                                                        
 
                            
                          





















































                                                                                                            

                              

                      




                            






                                                 













                                                                                      


                                                           









                                                                                                                       

































                                                                                                  






                                     

                                                                                                            



                                                     
                                                     

          



          



                            

                                                    




                                                                
                                                             





                      
                 




                                  
                     






















                                                                               






















                                                                                                                                                              








































































                                                                                                                                                                                            








                                                                                                     




























































                                                                                               
                 












































                                                                                                                                      





















































                                                                                               

                                                                    








































































                                                                                                                                                   















                                                             






                                                                                  


                              
                                               


                                                   
                                                                                                   
                                                              










                                                       

























































                                                                                                                                                                      
                                                                         
                     




                                                                                                      

                







































































                                                                                                                                                                               












































                                                                                                                                                                                              















                                                                               
















                                                           
import { useState, useCallback, useMemo, useRef, useEffect } from "react";
import type {
  ChainWithContracts,
  ChainGraphResponse,
  ChainContractDetail,
  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
const NODE_WIDTH = 180;
const NODE_HEIGHT = 80;
const CANVAS_PADDING = 40;

export function ChainEditor({
  chain,
  graph,
  loading,
  onBack,
  onRefresh,
  onContractClick,
}: ChainEditorProps) {
  const canvasRef = useRef<HTMLDivElement>(null);
  const [selectedNode, setSelectedNode] = useState<string | null>(null);
  const [hoveredNode, setHoveredNode] = useState<string | null>(null);
  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);

  // Drag state
  const [draggedNode, setDraggedNode] = useState<string | null>(null);
  const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
  const [localPositions, setLocalPositions] = useState<Map<string, { x: number; y: number }>>(new Map());

  // Edge drawing state
  const [edgeDrawing, setEdgeDrawing] = useState<{
    fromNode: string;
    toPoint: { x: number; y: number };
  } | null>(null);

  // Context menu state (nodeId is null for canvas context menu)
  const [contextMenu, setContextMenu] = useState<{
    x: number;
    y: number;
    nodeId: string | null;
    canvasPosition?: { gridX: number; gridY: number };
  } | null>(null);

  // 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]);

  // Determine which view to show: definitions (pending chain) or contracts (active/completed)
  const showDefinitions = chain.status === "pending" || chain.status === "archived";
  const currentGraph = showDefinitions ? definitionGraph : graph;

  // Use positions from graph nodes, with local overrides for dragging
  const nodePositions = useMemo(() => {
    const positions = new Map<string, { x: number; y: number }>();

    if (showDefinitions && definitionGraph?.nodes) {
      for (const node of definitionGraph.nodes) {
        // Use local position if available (during drag), otherwise use server position
        const localPos = localPositions.get(node.id);
        if (localPos) {
          positions.set(node.id, localPos);
        } else {
          positions.set(node.id, {
            x: CANVAS_PADDING + (node.x || 0) * (NODE_WIDTH + 60),
            y: CANVAS_PADDING + (node.y || 0) * (NODE_HEIGHT + 40),
          });
        }
      }
    } else if (!showDefinitions && graph?.nodes) {
      for (const node of graph.nodes) {
        positions.set(node.contractId, {
          x: CANVAS_PADDING + (node.x || 0) * (NODE_WIDTH + 60),
          y: CANVAS_PADDING + (node.y || 0) * (NODE_HEIGHT + 40),
        });
      }
    }

    return positions;
  }, [showDefinitions, definitionGraph?.nodes, graph?.nodes, localPositions]);

  // Canvas dimensions
  const canvasDimensions = useMemo(() => {
    if (nodePositions.size === 0) {
      return { width: 600, height: 400 };
    }

    let maxX = 0;
    let maxY = 0;
    for (const pos of nodePositions.values()) {
      maxX = Math.max(maxX, pos.x + NODE_WIDTH);
      maxY = Math.max(maxY, pos.y + NODE_HEIGHT);
    }

    return {
      width: Math.max(600, maxX + CANVAS_PADDING),
      height: Math.max(400, maxY + CANVAS_PADDING),
    };
  }, [nodePositions]);

  const handleNodeClick = useCallback((nodeId: string) => {
    setSelectedNode(nodeId);
  }, []);

  const handleNodeDoubleClick = useCallback(
    (nodeId: string) => {
      // For definitions, we can't open a contract yet
      if (showDefinitions) return;
      onContractClick(nodeId);
    },
    [onContractClick, showDefinitions]
  );

  const getStatusColor = (status: string, isCheckpoint = false) => {
    // Checkpoint contracts use purple/violet colors
    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" };
    }
  };

  // Find selected item (definition or contract)
  const selectedDefinition = showDefinitions && selectedNode
    ? definitions.find((d) => d.id === selectedNode)
    : null;
  const selectedContract = !showDefinitions && selectedNode
    ? chain.contracts.find((c) => c.contractId === selectedNode)
    : null;

  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]);

  // Position for new definition (set from canvas right-click)
  const [newDefinitionPosition, setNewDefinitionPosition] = useState<{ x: number; y: number } | null>(null);

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

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

    // Find first free position by scanning row by row
    for (let y = 0; y < 10; y++) {
      for (let x = 0; x < 10; x++) {
        if (!occupied.has(`${x},${y}`)) {
          return { x, y };
        }
      }
    }

    // Fallback: place at the end of the last row
    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 {
      // Use specified position or find free space
      const position = newDefinitionPosition || findFreePosition();
      const reqWithPosition = { ...req, editorX: position.x, editorY: position.y };
      await createChainDefinition(chain.id, reqWithPosition);
      // Reload definitions
      const [defs, defGraph] = await Promise.all([
        listChainDefinitions(chain.id),
        getChainDefinitionGraph(chain.id),
      ]);
      setDefinitions(defs);
      setDefinitionGraph(defGraph);
      setShowAddDefinition(false);
      setNewDefinitionPosition(null);
    } catch (err) {
      console.error("Failed to add definition:", err);
      setError(err instanceof Error ? err.message : "Failed to add definition");
    }
  }, [chain.id, newDefinitionPosition, 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);
      // Reload definitions
      const [defs, defGraph] = await Promise.all([
        listChainDefinitions(chain.id),
        getChainDefinitionGraph(chain.id),
      ]);
      setDefinitions(defs);
      setDefinitionGraph(defGraph);
      setSelectedNode(null);
    } catch (err) {
      console.error("Failed to delete definition:", err);
      setError(err instanceof Error ? err.message : "Failed to delete definition");
    }
  }, [chain.id]);

  // Refresh definitions helper
  const refreshDefinitions = useCallback(async () => {
    const [defs, defGraph] = await Promise.all([
      listChainDefinitions(chain.id),
      getChainDefinitionGraph(chain.id),
    ]);
    setDefinitions(defs);
    setDefinitionGraph(defGraph);
    setLocalPositions(new Map()); // Clear local positions
  }, [chain.id]);

  // Context menu handlers
  const handleContextMenu = useCallback((e: React.MouseEvent, nodeId: string) => {
    e.preventDefault();
    e.stopPropagation();
    setContextMenu({ x: e.clientX, y: e.clientY, nodeId });
  }, []);

  // Canvas context menu (for creating new definitions)
  const handleCanvasContextMenu = useCallback((e: React.MouseEvent) => {
    if (!showDefinitions || chain.status !== "pending") return;
    e.preventDefault();

    const canvasRect = canvasRef.current?.getBoundingClientRect();
    if (!canvasRect) return;

    // Calculate grid position from click
    const clickX = e.clientX - canvasRect.left;
    const clickY = e.clientY - canvasRect.top;
    const gridX = Math.round((clickX - CANVAS_PADDING) / (NODE_WIDTH + 60));
    const gridY = Math.round((clickY - CANVAS_PADDING) / (NODE_HEIGHT + 40));

    setContextMenu({
      x: e.clientX,
      y: e.clientY,
      nodeId: null,
      canvasPosition: { gridX: Math.max(0, gridX), gridY: Math.max(0, gridY) },
    });
  }, [showDefinitions, chain.status]);

  const closeContextMenu = useCallback(() => {
    setContextMenu(null);
  }, []);

  // Handle creating definition at canvas position
  const handleCreateAtPosition = useCallback((gridX: number, gridY: number) => {
    setNewDefinitionPosition({ x: gridX, y: gridY });
    setShowAddDefinition(true);
    closeContextMenu();
  }, [closeContextMenu]);

  // Handle canvas click to close context menu
  const handleCanvasClick = useCallback(() => {
    if (contextMenu) {
      closeContextMenu();
    }
  }, [contextMenu, closeContextMenu]);

  // Drag handlers
  const handleDragStart = useCallback((e: React.MouseEvent, nodeId: string) => {
    if (!showDefinitions || chain.status !== "pending") return;
    e.preventDefault();
    const pos = nodePositions.get(nodeId);
    if (!pos) return;

    setDraggedNode(nodeId);
    setDragOffset({
      x: e.clientX - pos.x,
      y: e.clientY - pos.y,
    });
  }, [showDefinitions, chain.status, nodePositions]);

  const handleDragMove = useCallback((e: React.MouseEvent) => {
    if (!draggedNode) return;

    const canvasRect = canvasRef.current?.getBoundingClientRect();
    if (!canvasRect) return;

    const newX = Math.max(CANVAS_PADDING, e.clientX - dragOffset.x);
    const newY = Math.max(CANVAS_PADDING, e.clientY - dragOffset.y);

    setLocalPositions(prev => {
      const newMap = new Map(prev);
      newMap.set(draggedNode, { x: newX, y: newY });
      return newMap;
    });
  }, [draggedNode, dragOffset]);

  const handleDragEnd = useCallback(async () => {
    if (!draggedNode) return;

    const localPos = localPositions.get(draggedNode);
    if (localPos) {
      // Convert pixel position back to grid coordinates
      const gridX = Math.round((localPos.x - CANVAS_PADDING) / (NODE_WIDTH + 60));
      const gridY = Math.round((localPos.y - CANVAS_PADDING) / (NODE_HEIGHT + 40));

      try {
        await updateChainDefinition(chain.id, draggedNode, {
          editorX: gridX,
          editorY: gridY,
        });
        await refreshDefinitions();
      } catch (err) {
        console.error("Failed to update position:", err);
        setError(err instanceof Error ? err.message : "Failed to update position");
      }
    }

    setDraggedNode(null);
  }, [draggedNode, localPositions, chain.id, refreshDefinitions]);

  // Edge drawing handlers
  const handleEdgeDrawStart = useCallback((e: React.MouseEvent, nodeId: string) => {
    if (!showDefinitions || chain.status !== "pending") return;
    e.preventDefault();
    e.stopPropagation();

    const canvasRect = canvasRef.current?.getBoundingClientRect();
    if (!canvasRect) return;

    const pos = nodePositions.get(nodeId);
    if (!pos) return;

    setEdgeDrawing({
      fromNode: nodeId,
      toPoint: {
        x: pos.x + NODE_WIDTH / 2,
        y: pos.y + NODE_HEIGHT,
      },
    });
  }, [showDefinitions, chain.status, nodePositions]);

  const handleEdgeDrawMove = useCallback((e: React.MouseEvent) => {
    if (!edgeDrawing) return;

    const canvasRect = canvasRef.current?.getBoundingClientRect();
    if (!canvasRect) return;

    setEdgeDrawing(prev => prev ? {
      ...prev,
      toPoint: {
        x: e.clientX - canvasRect.left,
        y: e.clientY - canvasRect.top,
      },
    } : null);
  }, [edgeDrawing]);

  const handleEdgeDrawEnd = useCallback(async (targetNodeId: string | null) => {
    if (!edgeDrawing) return;

    if (targetNodeId && targetNodeId !== edgeDrawing.fromNode) {
      // Find the target definition and its current dependencies
      const fromDef = definitions.find(d => d.id === edgeDrawing.fromNode);
      const toDef = definitions.find(d => d.id === targetNodeId);

      if (fromDef && toDef) {
        // Add dependency: targetNodeId depends on fromNode
        const currentDeps = toDef.dependsOnNames || [];
        if (!currentDeps.includes(fromDef.name)) {
          try {
            await updateChainDefinition(chain.id, targetNodeId, {
              dependsOn: [...currentDeps, fromDef.name],
            });
            await refreshDefinitions();
          } catch (err) {
            console.error("Failed to create dependency:", err);
            setError(err instanceof Error ? err.message : "Failed to create dependency");
          }
        }
      }
    }

    setEdgeDrawing(null);
  }, [edgeDrawing, definitions, chain.id, refreshDefinitions]);

  // Handle removing a dependency via context menu
  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,
      });
      await refreshDefinitions();
      closeContextMenu();
    } catch (err) {
      console.error("Failed to remove dependency:", err);
      setError(err instanceof Error ? err.message : "Failed to remove dependency");
    }
  }, [definitions, chain.id, refreshDefinitions, closeContextMenu]);

  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 control buttons */}
            {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">
        {/* DAG Canvas */}
        <div className="flex-1 overflow-auto bg-[#050d18]">
          {loading ? (
            <div className="flex items-center justify-center h-full">
              <p className="font-mono text-xs text-[#8b949e]">Loading graph...</p>
            </div>
          ) : !currentGraph || currentGraph.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 && (
                  <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>
          ) : (
            <div
              ref={canvasRef}
              className="relative"
              style={{
                width: canvasDimensions.width,
                height: canvasDimensions.height,
                minWidth: "100%",
                minHeight: "100%",
                cursor: draggedNode ? "grabbing" : edgeDrawing ? "crosshair" : "default",
              }}
              onClick={handleCanvasClick}
              onContextMenu={handleCanvasContextMenu}
              onMouseMove={(e) => {
                if (draggedNode) handleDragMove(e);
                if (edgeDrawing) handleEdgeDrawMove(e);
              }}
              onMouseUp={() => {
                if (draggedNode) handleDragEnd();
                if (edgeDrawing) handleEdgeDrawEnd(null);
              }}
              onMouseLeave={() => {
                if (draggedNode) handleDragEnd();
                if (edgeDrawing) setEdgeDrawing(null);
              }}
            >
              {/* SVG layer for edges */}
              <svg
                className="absolute inset-0 pointer-events-none"
                style={{
                  width: canvasDimensions.width,
                  height: canvasDimensions.height,
                }}
              >
                <defs>
                  <marker
                    id="arrowhead"
                    markerWidth="10"
                    markerHeight="7"
                    refX="9"
                    refY="3.5"
                    orient="auto"
                  >
                    <polygon
                      points="0 0, 10 3.5, 0 7"
                      fill="#75aafc"
                      opacity="0.6"
                    />
                  </marker>
                  <marker
                    id="arrowhead-preview"
                    markerWidth="10"
                    markerHeight="7"
                    refX="9"
                    refY="3.5"
                    orient="auto"
                  >
                    <polygon
                      points="0 0, 10 3.5, 0 7"
                      fill="#f59e0b"
                      opacity="0.8"
                    />
                  </marker>
                </defs>
                {currentGraph.edges.map((edge, index) => {
                  const fromPos = nodePositions.get(edge.from);
                  const toPos = nodePositions.get(edge.to);
                  if (!fromPos || !toPos) return null;

                  // Calculate edge path (from bottom of source to top of target)
                  const startX = fromPos.x + NODE_WIDTH / 2;
                  const startY = fromPos.y + NODE_HEIGHT;
                  const endX = toPos.x + NODE_WIDTH / 2;
                  const endY = toPos.y;

                  // Bezier control points for smooth curves
                  const midY = (startY + endY) / 2;

                  const isHighlighted =
                    hoveredNode === edge.from || hoveredNode === edge.to;

                  return (
                    <path
                      key={`${edge.from}-${edge.to}-${index}`}
                      d={`M ${startX} ${startY} C ${startX} ${midY}, ${endX} ${midY}, ${endX} ${endY}`}
                      fill="none"
                      stroke={isHighlighted ? "#75aafc" : "#3f6fb3"}
                      strokeWidth={isHighlighted ? 2 : 1.5}
                      strokeDasharray={isHighlighted ? "none" : "4 2"}
                      markerEnd="url(#arrowhead)"
                      opacity={isHighlighted ? 1 : 0.6}
                    />
                  );
                })}
                {/* Edge drawing preview line */}
                {edgeDrawing && (() => {
                  const fromPos = nodePositions.get(edgeDrawing.fromNode);
                  if (!fromPos) return null;
                  const startX = fromPos.x + NODE_WIDTH / 2;
                  const startY = fromPos.y + NODE_HEIGHT;
                  const endX = edgeDrawing.toPoint.x;
                  const endY = edgeDrawing.toPoint.y;
                  const midY = (startY + endY) / 2;
                  return (
                    <path
                      d={`M ${startX} ${startY} C ${startX} ${midY}, ${endX} ${midY}, ${endX} ${endY}`}
                      fill="none"
                      stroke="#f59e0b"
                      strokeWidth={2}
                      strokeDasharray="6 3"
                      markerEnd="url(#arrowhead-preview)"
                      opacity={0.8}
                    />
                  );
                })()}
              </svg>

              {/* Node layer */}
              {showDefinitions && definitionGraph
                ? definitionGraph.nodes.map((node) => {
                    const pos = nodePositions.get(node.id);
                    if (!pos) return null;

                    const isCheckpoint = node.contractType === "checkpoint";
                    const status = node.isInstantiated
                      ? node.contractStatus || "pending"
                      : "pending";
                    const colors = getStatusColor(status, isCheckpoint);
                    const isSelected = selectedNode === node.id;
                    const isHovered = hoveredNode === node.id;

                    const isDragging = draggedNode === node.id;
                    const canEdit = chain.status === "pending";

                    return (
                      <div
                        key={node.id}
                        onClick={() => handleNodeClick(node.id)}
                        onContextMenu={(e) => canEdit && handleContextMenu(e, node.id)}
                        onMouseEnter={() => !draggedNode && setHoveredNode(node.id)}
                        onMouseLeave={() => !draggedNode && setHoveredNode(null)}
                        onMouseUp={() => edgeDrawing && handleEdgeDrawEnd(node.id)}
                        className={`absolute ${
                          isDragging ? "z-50 shadow-lg" : "transition-all duration-150"
                        } ${
                          isSelected
                            ? isCheckpoint
                              ? "ring-2 ring-[#a78bfa] ring-offset-2 ring-offset-[#050d18]"
                              : "ring-2 ring-[#75aafc] ring-offset-2 ring-offset-[#050d18]"
                            : ""
                        }`}
                        style={{
                          left: pos.x,
                          top: pos.y,
                          width: NODE_WIDTH,
                          height: NODE_HEIGHT,
                          transform: isHovered && !isDragging ? "scale(1.02)" : "scale(1)",
                          cursor: isDragging ? "grabbing" : canEdit ? "grab" : "pointer",
                        }}
                      >
                        {/* Drag handle (entire node) */}
                        <div
                          className={`w-full h-full rounded-lg border-2 overflow-hidden ${
                            isCheckpoint ? "bg-[#0f0a1e]" : "bg-[#0a1628]"
                          }`}
                          style={{
                            borderColor: isSelected
                              ? isCheckpoint
                                ? "#a78bfa"
                                : "#75aafc"
                              : colors.border,
                            borderStyle: node.isInstantiated ? "solid" : "dashed",
                          }}
                          onMouseDown={(e) => canEdit && handleDragStart(e, node.id)}
                        >
                          {/* 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">
                                {node.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`,
                                }}
                              >
                                {node.isInstantiated ? status : isCheckpoint ? "checkpoint" : "definition"}
                              </span>
                              <span className="font-mono text-[10px] text-[#8b949e]">
                                {node.contractType}
                              </span>
                            </div>
                          </div>
                        </div>
                        {/* Edge drawing handle (connector dot at bottom) */}
                        {canEdit && (isHovered || isSelected) && !isDragging && (
                          <div
                            className="absolute left-1/2 -translate-x-1/2 -bottom-2 w-4 h-4 rounded-full bg-[#f59e0b] border-2 border-[#0a1628] cursor-crosshair hover:scale-125 transition-transform"
                            onMouseDown={(e) => handleEdgeDrawStart(e, node.id)}
                            title="Drag to create dependency"
                          />
                        )}
                      </div>
                    );
                  })
                : graph?.nodes.map((node) => {
                    const pos = nodePositions.get(node.contractId);
                    if (!pos) return null;

                    const colors = getStatusColor(node.status);
                    const isSelected = selectedNode === node.contractId;
                    const isHovered = hoveredNode === node.contractId;

                    return (
                      <div
                        key={node.contractId}
                        onClick={() => handleNodeClick(node.contractId)}
                        onDoubleClick={() => handleNodeDoubleClick(node.contractId)}
                        onMouseEnter={() => setHoveredNode(node.contractId)}
                        onMouseLeave={() => setHoveredNode(null)}
                        className={`absolute cursor-pointer transition-all duration-150 ${
                          isSelected
                            ? "ring-2 ring-[#75aafc] ring-offset-2 ring-offset-[#050d18]"
                            : ""
                        }`}
                        style={{
                          left: pos.x,
                          top: pos.y,
                          width: NODE_WIDTH,
                          height: NODE_HEIGHT,
                          transform: isHovered ? "scale(1.02)" : "scale(1)",
                        }}
                      >
                        <div
                          className="w-full h-full rounded-lg border-2 bg-[#0a1628] overflow-hidden"
                          style={{
                            borderColor: isSelected ? "#75aafc" : colors.border,
                          }}
                        >
                          {/* 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">
                                {node.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`,
                                }}
                              >
                                {node.status}
                              </span>
                              {node.phase && (
                                <span className="font-mono text-[10px] text-[#8b949e]">
                                  {node.phase}
                                </span>
                              )}
                            </div>
                          </div>
                        </div>
                      </div>
                    );
                  })}
            </div>
          )}
        </div>

        {/* Detail panel */}
        {selectedDefinition && (
          <DefinitionDetailPanel
            definition={selectedDefinition}
            onClose={() => setSelectedNode(null)}
            onDelete={handleDeleteDefinition}
          />
        )}
        {selectedContract && (
          <ContractDetailPanel
            contract={selectedContract}
            allContracts={chain.contracts}
            onClose={() => setSelectedNode(null)}
            onSelectContract={setSelectedNode}
            onOpenContract={onContractClick}
          />
        )}
      </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>
              {chain.status === "pending" && (
                <>
                  <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="text-[#556677]">|</span>
                  <span className="text-[#556677]">Right-click for options</span>
                </>
              )}
              <span className="flex-1" />
              {chain.status === "pending" && (
                <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)}
        />
      )}

      {/* Context Menu */}
      {contextMenu && (
        <ContextMenu
          x={contextMenu.x}
          y={contextMenu.y}
          nodeId={contextMenu.nodeId}
          canvasPosition={contextMenu.canvasPosition}
          definition={contextMenu.nodeId ? definitions.find((d) => d.id === contextMenu.nodeId) : undefined}
          allDefinitions={definitions}
          onClose={closeContextMenu}
          onDelete={handleDeleteDefinition}
          onRemoveDependency={handleRemoveDependency}
          onCreateAtPosition={handleCreateAtPosition}
        />
      )}
    </div>
  );
}

// Context Menu Component
interface ContextMenuProps {
  x: number;
  y: number;
  nodeId: string | null;
  canvasPosition?: { gridX: number; gridY: number };
  definition?: ChainContractDefinition;
  allDefinitions: ChainContractDefinition[];
  onClose: () => void;
  onDelete: (id: string) => void;
  onRemoveDependency: (nodeId: string, depName: string) => void;
  onCreateAtPosition: (gridX: number, gridY: number) => void;
}

function ContextMenu({
  x,
  y,
  nodeId,
  canvasPosition,
  definition,
  allDefinitions: _allDefinitions,
  onClose,
  onDelete,
  onRemoveDependency,
  onCreateAtPosition,
}: ContextMenuProps) {
  const menuRef = useRef<HTMLDivElement>(null);

  // Close on click outside
  useEffect(() => {
    const handleClickOutside = (e: MouseEvent) => {
      if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
        onClose();
      }
    };
    document.addEventListener("mousedown", handleClickOutside);
    return () => document.removeEventListener("mousedown", handleClickOutside);
  }, [onClose]);

  // Close on escape
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key === "Escape") onClose();
    };
    document.addEventListener("keydown", handleKeyDown);
    return () => document.removeEventListener("keydown", handleKeyDown);
  }, [onClose]);

  // Canvas context menu (no node selected)
  if (!nodeId && canvasPosition) {
    return (
      <div
        ref={menuRef}
        className="fixed z-50 min-w-40 bg-[#0a1628] border border-[rgba(117,170,252,0.3)] shadow-xl rounded-lg overflow-hidden"
        style={{ left: x, top: y }}
      >
        <div className="py-1">
          <button
            onClick={() => onCreateAtPosition(canvasPosition.gridX, canvasPosition.gridY)}
            className="w-full px-3 py-2 text-left font-mono text-xs text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.1)] transition-colors flex items-center gap-2"
          >
            <PlusIcon className="w-3 h-3" />
            Create Definition Here
          </button>
        </div>
      </div>
    );
  }

  // Node context menu requires both nodeId and definition
  if (!nodeId || !definition) return null;

  const dependencies = definition.dependsOnNames || [];

  return (
    <div
      ref={menuRef}
      className="fixed z-50 min-w-48 bg-[#0a1628] border border-[rgba(117,170,252,0.3)] shadow-xl rounded-lg overflow-hidden"
      style={{ left: x, top: y }}
    >
      {/* Header */}
      <div className="px-3 py-2 border-b border-[rgba(117,170,252,0.2)] bg-[#0f1c32]">
        <span className="font-mono text-xs text-[#dbe7ff]">{definition.name}</span>
      </div>

      {/* Actions */}
      <div className="py-1">
        <button
          onClick={() => {
            onDelete(nodeId);
            onClose();
          }}
          className="w-full px-3 py-2 text-left font-mono text-xs text-red-400 hover:bg-red-400/10 transition-colors flex items-center gap-2"
        >
          <TrashIcon className="w-3 h-3" />
          Delete Definition
        </button>
      </div>

      {/* Dependencies section */}
      {dependencies.length > 0 && (
        <>
          <div className="border-t border-[rgba(117,170,252,0.2)]" />
          <div className="px-3 py-2">
            <span className="font-mono text-[10px] text-[#8b949e] uppercase">Dependencies</span>
          </div>
          <div className="pb-1">
            {dependencies.map((depName) => (
              <div
                key={depName}
                className="px-3 py-1.5 flex items-center justify-between hover:bg-[rgba(117,170,252,0.1)] group"
              >
                <span className="font-mono text-xs text-[#9bc3ff]">{depName}</span>
                <button
                  onClick={() => onRemoveDependency(nodeId, depName)}
                  className="font-mono text-[10px] text-red-400 opacity-0 group-hover:opacity-100 transition-opacity hover:underline"
                >
                  Remove
                </button>
              </div>
            ))}
          </div>
        </>
      )}

      {/* Hint */}
      <div className="border-t border-[rgba(117,170,252,0.2)] px-3 py-2">
        <span className="font-mono text-[10px] text-[#556677]">
          Drag from connector to create dependency
        </span>
      </div>
    </div>
  );
}

// Trash Icon
function TrashIcon({ className }: { className?: string }) {
  return (
    <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
      <path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
    </svg>
  );
}

// Plus Icon
function PlusIcon({ className }: { className?: string }) {
  return (
    <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
      <path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
    </svg>
  );
}

interface DefinitionDetailPanelProps {
  definition: ChainContractDefinition;
  onClose: () => void;
  onDelete: (id: string) => void;
}

function DefinitionDetailPanel({
  definition,
  onClose,
  onDelete,
}: DefinitionDetailPanelProps) {
  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]"
          >
            Close
          </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-[#dbe7ff]">{definition.description}</p>
          </div>
        )}

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

        {/* Initial Phase */}
        <div>
          <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1">
            Initial Phase
          </label>
          <span className="font-mono text-xs text-[#dbe7ff]">
            {definition.initialPhase || "plan"}
          </span>
        </div>

        {/* Dependencies */}
        {definition.dependsOnNames && definition.dependsOnNames.length > 0 && (
          <div>
            <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1">
              Depends On
            </label>
            <div className="space-y-1">
              {definition.dependsOnNames.map((depName) => (
                <span
                  key={depName}
                  className="block font-mono text-xs text-[#9bc3ff]"
                >
                  {depName}
                </span>
              ))}
            </div>
          </div>
        )}

        {/* Tasks */}
        {definition.tasks && definition.tasks.length > 0 && (
          <div>
            <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1">
              Tasks ({definition.tasks.length})
            </label>
            <div className="space-y-1">
              {definition.tasks.map((task, i) => (
                <div key={i} className="font-mono text-xs text-[#dbe7ff]">
                  {task.name}
                </div>
              ))}
            </div>
          </div>
        )}

        {/* Actions */}
        <div className="pt-2 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>
  );
}

interface ContractDetailPanelProps {
  contract: ChainContractDetail;
  allContracts: ChainContractDetail[];
  onClose: () => void;
  onSelectContract: (contractId: string) => void;
  onOpenContract: (contractId: string) => void;
}

function ContractDetailPanel({
  contract,
  allContracts,
  onClose,
  onSelectContract,
  onOpenContract,
}: ContractDetailPanelProps) {
  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">
            Contract Details
          </h3>
          <button
            onClick={onClose}
            className="font-mono text-xs text-[#8b949e] hover:text-[#dbe7ff]"
          >
            Close
          </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]">
            {contract.contractName}
          </p>
        </div>

        {/* Status */}
        <div>
          <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1">
            Status
          </label>
          <span
            className={`font-mono text-xs uppercase ${
              statusColors[contract.contractStatus] || "text-[#555]"
            }`}
          >
            {contract.contractStatus}
          </span>
        </div>

        {/* Phase */}
        <div>
          <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1">
            Phase
          </label>
          <span className="font-mono text-sm text-[#dbe7ff]">
            {contract.contractPhase}
          </span>
        </div>

        {/* Dependencies */}
        {contract.dependsOn && contract.dependsOn.length > 0 && (
          <div>
            <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1">
              Depends On
            </label>
            <div className="space-y-1">
              {contract.dependsOn.map((depId) => {
                const dep = allContracts.find((c) => c.contractId === depId);
                return (
                  <button
                    key={depId}
                    onClick={() => onSelectContract(depId)}
                    className="block w-full text-left font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] truncate"
                  >
                    {dep?.contractName || depId}
                  </button>
                );
              })}
            </div>
          </div>
        )}

        {/* Order Index */}
        <div>
          <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1">
            Order Index
          </label>
          <p className="font-mono text-xs text-[#dbe7ff]">
            {contract.orderIndex}
          </p>
        </div>

        {/* Created */}
        <div>
          <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1">
            Created
          </label>
          <p className="font-mono text-xs text-[#dbe7ff]">
            {new Date(contract.createdAt).toLocaleString()}
          </p>
        </div>

        {/* Actions */}
        <div className="pt-2 border-t border-[rgba(117,170,252,0.2)]">
          <button
            onClick={() => onOpenContract(contract.contractId)}
            className="w-full px-3 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors"
          >
            Open Contract
          </button>
        </div>
      </div>
    </div>
  );
}

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[]>([]);
  // Checkpoint validation options
  const [checkDeliverables, setCheckDeliverables] = useState(true);
  const [runTests, setRunTests] = useState(false);
  const [checkContent, setCheckContent] = useState("");
  const [onFailure, setOnFailure] = useState<"block" | "retry" | "warn">("block");

  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, // Checkpoints always start in execute
      dependsOn: dependsOn.length > 0 ? dependsOn : undefined,
    };
    // Add validation config for checkpoint contracts
    if (isCheckpoint) {
      req.validation = {
        checkDeliverables,
        runTests,
        checkContent: checkContent.trim() || undefined,
        onFailure,
      };
    }
    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-xs text-[#8b949e] uppercase mb-1">
              Name
            </label>
            <input
              type="text"
              value={name}
              onChange={(e) => setName(e.target.value)}
              placeholder="e.g., Research Phase"
              className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]"
              autoFocus
            />
          </div>

          {/* Description */}
          <div>
            <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1">
              Description (optional)
            </label>
            <textarea
              value={description}
              onChange={(e) => setDescription(e.target.value)}
              placeholder="What does this contract accomplish?"
              rows={2}
              className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc] resize-none"
            />
          </div>

          {/* Contract Type */}
          <div>
            <label className="block font-mono text-xs 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-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]"
            >
              <option value="simple">Simple</option>
              <option value="specification">Specification</option>
              <option value="execute">Execute</option>
              <option value="checkpoint">Checkpoint (Validation)</option>
            </select>
            {isCheckpoint && (
              <p className="mt-1 font-mono text-[10px] text-[#a78bfa]">
                Checkpoint contracts validate outputs before allowing downstream contracts to proceed.
              </p>
            )}
          </div>

          {/* Initial Phase - hidden for checkpoints */}
          {!isCheckpoint && (
            <div>
              <label className="block font-mono text-xs 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-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]"
              >
                <option value="plan">Plan</option>
                <option value="execute">Execute</option>
                <option value="review">Review</option>
              </select>
            </div>
          )}

          {/* Checkpoint Validation Options */}
          {isCheckpoint && (
            <div className="p-3 bg-[#0f0a1e] border border-[#6366f1]/30 space-y-3">
              <h4 className="font-mono text-xs text-[#a78bfa] uppercase">Validation Options</h4>

              <label className="flex items-center gap-2 cursor-pointer">
                <input
                  type="checkbox"
                  checked={checkDeliverables}
                  onChange={(e) => setCheckDeliverables(e.target.checked)}
                  className="accent-[#a78bfa]"
                />
                <span className="font-mono text-xs text-[#dbe7ff]">Check required deliverables exist</span>
              </label>

              <label className="flex items-center gap-2 cursor-pointer">
                <input
                  type="checkbox"
                  checked={runTests}
                  onChange={(e) => setRunTests(e.target.checked)}
                  className="accent-[#a78bfa]"
                />
                <span className="font-mono text-xs text-[#dbe7ff]">Run test suite</span>
              </label>

              <div>
                <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1">
                  Custom Validation Instructions (optional)
                </label>
                <textarea
                  value={checkContent}
                  onChange={(e) => setCheckContent(e.target.value)}
                  placeholder="Additional criteria for Claude to check..."
                  rows={3}
                  className="w-full px-2 py-1.5 bg-[#0d1b2d] border border-[#6366f1]/50 text-[#dbe7ff] font-mono text-xs focus:outline-none focus:border-[#a78bfa] resize-none"
                />
              </div>

              <div>
                <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1">
                  On Failure
                </label>
                <select
                  value={onFailure}
                  onChange={(e) => setOnFailure(e.target.value as "block" | "retry" | "warn")}
                  className="w-full px-2 py-1.5 bg-[#0d1b2d] border border-[#6366f1]/50 text-[#dbe7ff] font-mono text-xs focus:outline-none focus:border-[#a78bfa]"
                >
                  <option value="block">Block - Stop chain until fixed</option>
                  <option value="retry">Retry - Retry upstream contracts</option>
                  <option value="warn">Warn - Log warning but continue</option>
                </select>
              </div>
            </div>
          )}

          {/* Dependencies */}
          {existingNames.length > 0 && (
            <div>
              <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1">
                Depends On
              </label>
              <div className="space-y-1 max-h-32 overflow-y-auto">
                {existingNames.map((depName) => (
                  <label key={depName} className="flex items-center gap-2 cursor-pointer">
                    <input
                      type="checkbox"
                      checked={dependsOn.includes(depName)}
                      onChange={() => toggleDependency(depName)}
                      className="accent-[#75aafc]"
                    />
                    <span className="font-mono text-xs text-[#dbe7ff]">{depName}</span>
                  </label>
                ))}
              </div>
            </div>
          )}

          {/* Actions */}
          <div className="flex gap-2 justify-end pt-2">
            <button
              onClick={onCancel}
              className="px-4 py-2 font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] 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 disabled:cursor-not-allowed"
            >
              Add Definition
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

function ChainIcon({ className }: { className?: string }) {
  return (
    <svg
      className={className}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
    >
      <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
      <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
    </svg>
  );
}

function CheckIcon({ className }: { className?: string }) {
  return (
    <svg
      className={className}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
    >
      <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
      <polyline points="22 4 12 14.01 9 11.01" />
    </svg>
  );
}