summaryrefslogblamecommitdiff
path: root/makima/frontend/src/components/chains/ChainEditor.tsx
blob: 9028c3ec7f2fb3cefd7c4a2e764ce15725223c25 (plain) (tree)
1
2
3
4
5
                                                                          



                      










                               

                       



                                              
                             

  
























                                                                        


























                                                                                                    


                                                               
                                                                  














                                                                 
     
 
                     
                                                              



















                                                   

                                                           


                                            



                                                      
      
                                      
















                                                                     




                                                            


                                                                





























































                                                                                        












                                                                                                 


                                                                                       




                                                   
                                                            
                                                           

                 
                            
                   


















                                                                                                                                                                    







                                                                                                                                                          




                                                                                                          









                                                                                  
                                                                  


                                                                     


                                                       
                    



                                                                             
                    







                                                                                                                                                    




































                                                                
                                                          































                                                                                                       








































































                                                                                                            
 


                                                                        
 
                            
                          





















































                                                                                                            

                              

                      




                            






                                                 













                                                                                      





































































































                                                                                                  
                 












































                                                                                                                                      





















































                                                                                               

                                                                    








































































                                                                                                                                                   


















































































































































                                                                                                                                                                                              















                                                                               
import { useState, useCallback, useMemo, useRef, useEffect } from "react";
import type {
  ChainWithContracts,
  ChainGraphResponse,
  ChainContractDetail,
  ChainContractDefinition,
  ChainDefinitionGraphResponse,
  AddContractDefinitionRequest,
} from "../../lib/api";
import {
  listChainDefinitions,
  createChainDefinition,
  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);

  // 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 directly (x, y from server)
  const nodePositions = useMemo(() => {
    const positions = new Map<string, { x: number; y: number }>();

    if (showDefinitions && definitionGraph?.nodes) {
      for (const node of definitionGraph.nodes) {
        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]);

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

  const handleAddDefinition = useCallback(async (req: AddContractDefinitionRequest) => {
    try {
      await createChainDefinition(chain.id, req);
      // Reload definitions
      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]);

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

  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%",
              }}
            >
              {/* 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>
                </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}
                    />
                  );
                })}
              </svg>

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

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

                    return (
                      <div
                        key={node.id}
                        onClick={() => handleNodeClick(node.id)}
                        onMouseEnter={() => setHoveredNode(node.id)}
                        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,
                            borderStyle: node.isInstantiated ? "solid" : "dashed",
                          }}
                        >
                          {/* 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.isInstantiated ? status : "definition"}
                              </span>
                              <span className="font-mono text-[10px] text-[#8b949e]">
                                {node.contractType}
                              </span>
                            </div>
                          </div>
                        </div>
                      </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>
              <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)}
        />
      )}
    </div>
  );
}

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

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

  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>
            </select>
          </div>

          {/* Initial Phase */}
          <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>

          {/* 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>
  );
}