diff options
Diffstat (limited to 'makima/frontend/src/components/chains')
| -rw-r--r-- | makima/frontend/src/components/chains/ChainEditor.tsx | 1278 | ||||
| -rw-r--r-- | makima/frontend/src/components/chains/ChainList.tsx | 191 |
2 files changed, 0 insertions, 1469 deletions
diff --git a/makima/frontend/src/components/chains/ChainEditor.tsx b/makima/frontend/src/components/chains/ChainEditor.tsx deleted file mode 100644 index 6b9aa70..0000000 --- a/makima/frontend/src/components/chains/ChainEditor.tsx +++ /dev/null @@ -1,1278 +0,0 @@ -import { useState, useCallback, useEffect } from "react"; -import { - ReactFlow, - Node, - Edge, - Controls, - Background, - useNodesState, - useEdgesState, - Connection, - Handle, - Position, - BackgroundVariant, - NodeChange, - MarkerType, -} from "@xyflow/react"; -import "@xyflow/react/dist/style.css"; - -import type { - ChainWithContracts, - ChainGraphResponse, - ChainContractDefinition, - ChainDefinitionGraphResponse, - AddContractDefinitionRequest, - ChainRepository, - AddChainRepositoryRequest, -} from "../../lib/api"; -import { - listChainDefinitions, - createChainDefinition, - updateChainDefinition, - deleteChainDefinition, - getChainDefinitionGraph, - startChain, - stopChain, - listChainRepositories, - addChainRepository, - deleteChainRepository, - setChainRepositoryPrimary, -} from "../../lib/api"; - -const statusColors: Record<string, string> = { - active: "text-green-400", - completed: "text-blue-400", - archived: "text-[#555]", - pending: "text-yellow-400", -}; - -interface ChainEditorProps { - chain: ChainWithContracts; - graph: ChainGraphResponse | null; - loading: boolean; - onBack: () => void; - onRefresh: () => void; - onContractClick: (contractId: string) => void; -} - -// Node dimensions for layout -const NODE_WIDTH = 200; -const NODE_HEIGHT = 80; -const GRID_SPACING_X = 280; -const GRID_SPACING_Y = 120; - -// Custom node component for definitions -function DefinitionNodeComponent({ - data, - selected, -}: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data: any; - selected?: boolean; -}) { - const isCheckpoint = data.contractType === "checkpoint"; - const status = data.isInstantiated ? data.contractStatus || "pending" : "pending"; - const colors = getStatusColor(status, isCheckpoint); - - return ( - <div - className={`rounded-lg border-2 overflow-hidden ${ - isCheckpoint ? "bg-[#0f0a1e]" : "bg-[#0a1628]" - } ${selected ? "ring-2 ring-offset-2 ring-offset-[#050d18]" : ""} ${ - selected ? (isCheckpoint ? "ring-[#a78bfa]" : "ring-[#75aafc]") : "" - }`} - style={{ - width: NODE_WIDTH, - height: NODE_HEIGHT, - borderColor: selected - ? isCheckpoint - ? "#a78bfa" - : "#75aafc" - : colors.border, - borderStyle: data.isInstantiated ? "solid" : "dashed", - }} - > - {/* Top handle for incoming edges */} - <Handle - type="target" - position={Position.Top} - className="!bg-[#75aafc] !w-3 !h-3 !border-2 !border-[#0a1628]" - /> - - {/* Status indicator bar */} - <div className="h-1.5" style={{ backgroundColor: colors.bg }} /> - - {/* Content */} - <div className="p-2"> - <div className="flex items-center justify-between mb-1"> - <span className="font-mono text-xs text-[#dbe7ff] truncate flex-1"> - {data.name} - </span> - {isCheckpoint ? ( - <CheckIcon className="w-4 h-4 text-[#a78bfa] opacity-70 flex-shrink-0 ml-1" /> - ) : ( - <ChainIcon className="w-4 h-4 text-[#75aafc] opacity-50 flex-shrink-0 ml-1" /> - )} - </div> - <div className="flex items-center justify-between"> - <span - className="font-mono text-[10px] uppercase px-1.5 py-0.5 rounded" - style={{ - color: colors.bg, - backgroundColor: `${colors.bg}20`, - }} - > - {data.isInstantiated ? status : isCheckpoint ? "checkpoint" : "definition"} - </span> - <span className="font-mono text-[10px] text-[#8b949e]"> - {data.contractType} - </span> - </div> - </div> - - {/* Bottom handle for outgoing edges */} - <Handle - type="source" - position={Position.Bottom} - className="!bg-[#f59e0b] !w-3 !h-3 !border-2 !border-[#0a1628]" - /> - </div> - ); -} - -// Custom node for contracts (active chains) -function ContractNodeComponent({ - data, - selected, -}: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data: any; - selected?: boolean; -}) { - const colors = getStatusColor(data.status); - - return ( - <div - className={`rounded-lg border-2 bg-[#0a1628] overflow-hidden ${ - selected ? "ring-2 ring-[#75aafc] ring-offset-2 ring-offset-[#050d18]" : "" - }`} - style={{ - width: NODE_WIDTH, - height: NODE_HEIGHT, - borderColor: selected ? "#75aafc" : colors.border, - }} - > - <Handle - type="target" - position={Position.Top} - className="!bg-[#75aafc] !w-3 !h-3 !border-2 !border-[#0a1628]" - /> - - <div className="h-1.5" style={{ backgroundColor: colors.bg }} /> - - <div className="p-2"> - <div className="flex items-center justify-between mb-1"> - <span className="font-mono text-xs text-[#dbe7ff] truncate flex-1"> - {data.name} - </span> - <ChainIcon className="w-4 h-4 text-[#75aafc] opacity-50 flex-shrink-0 ml-1" /> - </div> - <div className="flex items-center justify-between"> - <span - className="font-mono text-[10px] uppercase px-1.5 py-0.5 rounded" - style={{ - color: colors.bg, - backgroundColor: `${colors.bg}20`, - }} - > - {data.status} - </span> - {data.phase && ( - <span className="font-mono text-[10px] text-[#8b949e]">{data.phase}</span> - )} - </div> - </div> - - <Handle - type="source" - position={Position.Bottom} - className="!bg-[#f59e0b] !w-3 !h-3 !border-2 !border-[#0a1628]" - /> - </div> - ); -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const nodeTypes: Record<string, any> = { - definition: DefinitionNodeComponent, - contract: ContractNodeComponent, -}; - -function getStatusColor(status: string, isCheckpoint = false) { - if (isCheckpoint) { - switch (status) { - case "active": - return { bg: "#a78bfa", border: "#8b5cf6", text: "#5b21b6" }; - case "completed": - return { bg: "#818cf8", border: "#6366f1", text: "#3730a3" }; - case "pending": - return { bg: "#c4b5fd", border: "#a78bfa", text: "#6d28d9" }; - case "failed": - return { bg: "#ef4444", border: "#dc2626", text: "#991b1b" }; - default: - return { bg: "#a78bfa", border: "#8b5cf6", text: "#5b21b6" }; - } - } - switch (status) { - case "active": - return { bg: "#4ade80", border: "#22c55e", text: "#166534" }; - case "completed": - return { bg: "#60a5fa", border: "#3b82f6", text: "#1e40af" }; - case "pending": - return { bg: "#f59e0b", border: "#d97706", text: "#92400e" }; - case "blocked": - return { bg: "#ef4444", border: "#dc2626", text: "#991b1b" }; - default: - return { bg: "#6b7280", border: "#4b5563", text: "#374151" }; - } -} - -export function ChainEditor({ - chain, - graph, - loading, - onBack, - onRefresh, - onContractClick, -}: ChainEditorProps) { - const [definitions, setDefinitions] = useState<ChainContractDefinition[]>([]); - const [definitionGraph, setDefinitionGraph] = useState<ChainDefinitionGraphResponse | null>(null); - const [showAddDefinition, setShowAddDefinition] = useState(false); - const [isStarting, setIsStarting] = useState(false); - const [isStopping, setIsStopping] = useState(false); - const [error, setError] = useState<string | null>(null); - const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null); - const [repositories, setRepositories] = useState<ChainRepository[]>([]); - const [showAddRepo, setShowAddRepo] = useState(false); - - const [nodes, setNodes, onNodesChange] = useNodesState([] as Node[]); - const [edges, setEdges, onEdgesChange] = useEdgesState([] as Edge[]); - - const showDefinitions = chain.status === "pending" || chain.status === "archived"; - const canEdit = chain.status === "pending"; - - // Load definitions and repositories when chain changes - useEffect(() => { - async function loadData() { - try { - const [defs, defGraph, repos] = await Promise.all([ - listChainDefinitions(chain.id), - getChainDefinitionGraph(chain.id), - listChainRepositories(chain.id), - ]); - setDefinitions(defs); - setDefinitionGraph(defGraph); - setRepositories(repos); - } catch (err) { - console.error("Failed to load data:", err); - } - } - loadData(); - }, [chain.id]); - - // Convert definitions/contracts to React Flow nodes and edges - useEffect(() => { - if (showDefinitions && definitionGraph) { - const flowNodes: Node[] = definitionGraph.nodes.map((node) => ({ - id: node.id, - type: "definition", - position: { - x: (node.x || 0) * GRID_SPACING_X, - y: (node.y || 0) * GRID_SPACING_Y, - }, - data: { - name: node.name, - contractType: node.contractType, - isInstantiated: node.isInstantiated, - contractStatus: node.contractStatus, - }, - draggable: canEdit, - })); - - const flowEdges: Edge[] = definitionGraph.edges.map((edge, index) => ({ - id: `${edge.from}-${edge.to}-${index}`, - source: edge.from, - target: edge.to, - type: "smoothstep", - animated: false, - style: { stroke: "#3f6fb3", strokeWidth: 2 }, - markerEnd: { type: MarkerType.ArrowClosed, color: "#75aafc" }, - })); - - setNodes(flowNodes); - setEdges(flowEdges); - } else if (!showDefinitions && graph) { - const flowNodes: Node[] = graph.nodes.map((node) => ({ - id: node.contractId, - type: "contract", - position: { - x: (node.x || 0) * GRID_SPACING_X, - y: (node.y || 0) * GRID_SPACING_Y, - }, - data: { - name: node.name, - status: node.status, - phase: node.phase, - }, - draggable: false, - })); - - const flowEdges: Edge[] = graph.edges.map((edge, index) => ({ - id: `${edge.from}-${edge.to}-${index}`, - source: edge.from, - target: edge.to, - type: "smoothstep", - animated: edge.from === selectedNodeId || edge.to === selectedNodeId, - style: { stroke: "#3f6fb3", strokeWidth: 2 }, - markerEnd: { type: MarkerType.ArrowClosed, color: "#75aafc" }, - })); - - setNodes(flowNodes); - setEdges(flowEdges); - } - }, [showDefinitions, definitionGraph, graph, canEdit, selectedNodeId, setNodes, setEdges]); - - // Handle node position changes (drag end) - const handleNodesChange = useCallback( - async (changes: NodeChange[]) => { - onNodesChange(changes); - - // Save position changes to backend - for (const change of changes) { - if (change.type === "position" && change.dragging === false && change.position) { - const gridX = Math.round(change.position.x / GRID_SPACING_X); - const gridY = Math.round(change.position.y / GRID_SPACING_Y); - - try { - await updateChainDefinition(chain.id, change.id, { - editorX: gridX, - editorY: gridY, - }); - } catch (err) { - console.error("Failed to save position:", err); - } - } - } - }, - [chain.id, onNodesChange] - ); - - // Handle new edge connections - const handleConnect = useCallback( - async (connection: Connection) => { - if (!connection.source || !connection.target) return; - - // Find the definitions - const sourceDef = definitions.find((d) => d.id === connection.source); - const targetDef = definitions.find((d) => d.id === connection.target); - - if (!sourceDef || !targetDef) return; - - // Add dependency: target depends on source - const currentDeps = targetDef.dependsOnNames || []; - if (!currentDeps.includes(sourceDef.name)) { - try { - await updateChainDefinition(chain.id, connection.target, { - dependsOn: [...currentDeps, sourceDef.name], - }); - - // Refresh - const [defs, defGraph] = await Promise.all([ - listChainDefinitions(chain.id), - getChainDefinitionGraph(chain.id), - ]); - setDefinitions(defs); - setDefinitionGraph(defGraph); - } catch (err) { - console.error("Failed to create dependency:", err); - setError(err instanceof Error ? err.message : "Failed to create dependency"); - } - } - }, - [chain.id, definitions] - ); - - // Handle node selection - const handleNodeClick = useCallback( - (_: React.MouseEvent, node: Node) => { - setSelectedNodeId(node.id); - }, - [] - ); - - // Handle node double-click (open contract) - const handleNodeDoubleClick = useCallback( - (_: React.MouseEvent, node: Node) => { - if (!showDefinitions) { - onContractClick(node.id); - } - }, - [showDefinitions, onContractClick] - ); - - // Handle pane click (deselect) - const handlePaneClick = useCallback(() => { - setSelectedNodeId(null); - }, []); - - // Find selected definition - const selectedDefinition = showDefinitions && selectedNodeId - ? definitions.find((d) => d.id === selectedNodeId) - : null; - - // Find free position for new definition - const findFreePosition = useCallback(() => { - if (!definitionGraph?.nodes || definitionGraph.nodes.length === 0) { - return { x: 0, y: 0 }; - } - - const occupied = new Set<string>(); - for (const node of definitionGraph.nodes) { - occupied.add(`${node.x},${node.y}`); - } - - for (let y = 0; y < 10; y++) { - for (let x = 0; x < 10; x++) { - if (!occupied.has(`${x},${y}`)) { - return { x, y }; - } - } - } - - const maxY = Math.max(...definitionGraph.nodes.map((n) => n.y || 0)); - return { x: 0, y: maxY + 1 }; - }, [definitionGraph?.nodes]); - - const handleAddDefinition = useCallback( - async (req: AddContractDefinitionRequest) => { - try { - const position = findFreePosition(); - const reqWithPosition = { ...req, editorX: position.x, editorY: position.y }; - await createChainDefinition(chain.id, reqWithPosition); - - const [defs, defGraph] = await Promise.all([ - listChainDefinitions(chain.id), - getChainDefinitionGraph(chain.id), - ]); - setDefinitions(defs); - setDefinitionGraph(defGraph); - setShowAddDefinition(false); - } catch (err) { - console.error("Failed to add definition:", err); - setError(err instanceof Error ? err.message : "Failed to add definition"); - } - }, - [chain.id, findFreePosition] - ); - - const handleDeleteDefinition = useCallback( - async (definitionId: string) => { - if (!confirm("Are you sure you want to delete this definition?")) return; - try { - await deleteChainDefinition(chain.id, definitionId); - - const [defs, defGraph] = await Promise.all([ - listChainDefinitions(chain.id), - getChainDefinitionGraph(chain.id), - ]); - setDefinitions(defs); - setDefinitionGraph(defGraph); - setSelectedNodeId(null); - } catch (err) { - console.error("Failed to delete definition:", err); - setError(err instanceof Error ? err.message : "Failed to delete definition"); - } - }, - [chain.id] - ); - - const handleStartChain = useCallback(async () => { - setIsStarting(true); - setError(null); - try { - await startChain(chain.id); - onRefresh(); - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to start chain"); - } finally { - setIsStarting(false); - } - }, [chain.id, onRefresh]); - - const handleStopChain = useCallback(async () => { - if (!confirm("Are you sure you want to stop this chain?")) return; - setIsStopping(true); - setError(null); - try { - await stopChain(chain.id); - onRefresh(); - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to stop chain"); - } finally { - setIsStopping(false); - } - }, [chain.id, onRefresh]); - - const handleRemoveDependency = useCallback( - async (nodeId: string, depName: string) => { - const def = definitions.find((d) => d.id === nodeId); - if (!def) return; - - const newDeps = (def.dependsOnNames || []).filter((d) => d !== depName); - try { - await updateChainDefinition(chain.id, nodeId, { dependsOn: newDeps }); - - const [defs, defGraph] = await Promise.all([ - listChainDefinitions(chain.id), - getChainDefinitionGraph(chain.id), - ]); - setDefinitions(defs); - setDefinitionGraph(defGraph); - } catch (err) { - console.error("Failed to remove dependency:", err); - setError(err instanceof Error ? err.message : "Failed to remove dependency"); - } - }, - [chain.id, definitions] - ); - - // Repository handlers - const handleAddRepository = useCallback( - async (req: AddChainRepositoryRequest) => { - try { - await addChainRepository(chain.id, req); - const repos = await listChainRepositories(chain.id); - setRepositories(repos); - setShowAddRepo(false); - } catch (err) { - console.error("Failed to add repository:", err); - setError(err instanceof Error ? err.message : "Failed to add repository"); - } - }, - [chain.id] - ); - - const handleDeleteRepository = useCallback( - async (repoId: string) => { - if (!confirm("Remove this repository from the chain?")) return; - try { - await deleteChainRepository(chain.id, repoId); - const repos = await listChainRepositories(chain.id); - setRepositories(repos); - } catch (err) { - console.error("Failed to delete repository:", err); - setError(err instanceof Error ? err.message : "Failed to delete repository"); - } - }, - [chain.id] - ); - - const handleSetPrimary = useCallback( - async (repoId: string) => { - try { - await setChainRepositoryPrimary(chain.id, repoId); - const repos = await listChainRepositories(chain.id); - setRepositories(repos); - } catch (err) { - console.error("Failed to set primary:", err); - setError(err instanceof Error ? err.message : "Failed to set primary repository"); - } - }, - [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.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> - - {/* Repository section */} - <div className="px-3 py-2 border-b border-[rgba(117,170,252,0.2)] bg-[#0a1628]/50"> - <div className="flex items-center justify-between"> - <span className="font-mono text-[10px] text-[#8b949e] uppercase"> - Repositories ({repositories.length}) - </span> - {canEdit && ( - <button - onClick={() => setShowAddRepo(true)} - className="font-mono text-[10px] text-[#9bc3ff] hover:text-[#dbe7ff]" - > - + Add - </button> - )} - </div> - {repositories.length > 0 && ( - <div className="flex flex-wrap gap-2 mt-2"> - {repositories.map((repo) => ( - <div - key={repo.id} - className={`flex items-center gap-1 px-2 py-1 rounded font-mono text-xs ${ - repo.isPrimary - ? "bg-[#75aafc]/20 border border-[#75aafc]/50 text-[#75aafc]" - : "bg-[#1a2744] border border-[rgba(117,170,252,0.2)] text-[#9bc3ff]" - }`} - > - <RepoIcon className="w-3 h-3" /> - <span className="truncate max-w-[150px]" title={repo.name}> - {repo.name} - </span> - {repo.isPrimary && ( - <span className="text-[8px] uppercase ml-1">primary</span> - )} - {canEdit && !repo.isPrimary && ( - <button - onClick={() => handleSetPrimary(repo.id)} - className="ml-1 text-[8px] text-[#556677] hover:text-[#9bc3ff]" - title="Set as primary" - > - ★ - </button> - )} - {canEdit && ( - <button - onClick={() => handleDeleteRepository(repo.id)} - className="ml-1 text-[10px] text-[#556677] hover:text-red-400" - > - ✕ - </button> - )} - </div> - ))} - </div> - )} - {repositories.length === 0 && ( - <p className="font-mono text-[10px] text-[#556677] mt-1"> - No repositories attached. {canEdit && "Add one to use with contracts."} - </p> - )} - </div> - - {/* Main content */} - <div className="flex-1 flex min-h-0"> - {/* React Flow Canvas */} - <div className="flex-1 bg-[#050d18]"> - {loading ? ( - <div className="flex items-center justify-center h-full"> - <p className="font-mono text-xs text-[#8b949e]">Loading graph...</p> - </div> - ) : nodes.length === 0 ? ( - <div className="flex items-center justify-center h-full"> - <div className="text-center"> - <p className="font-mono text-sm text-[#8b949e] mb-2"> - {showDefinitions - ? "No contract definitions yet" - : "No contracts in this chain yet"} - </p> - <p className="font-mono text-xs text-[#556677] mb-4"> - {showDefinitions - ? "Add contract definitions to build your chain" - : "Start the chain to create contracts from definitions"} - </p> - {showDefinitions && canEdit && ( - <button - onClick={() => setShowAddDefinition(true)} - className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors" - > - + Add Definition - </button> - )} - </div> - </div> - ) : ( - <ReactFlow - nodes={nodes} - edges={edges} - onNodesChange={handleNodesChange} - onEdgesChange={onEdgesChange} - onConnect={canEdit ? handleConnect : undefined} - onNodeClick={handleNodeClick} - onNodeDoubleClick={handleNodeDoubleClick} - onPaneClick={handlePaneClick} - nodeTypes={nodeTypes} - fitView - fitViewOptions={{ padding: 0.2 }} - minZoom={0.5} - maxZoom={2} - defaultEdgeOptions={{ - type: "smoothstep", - style: { stroke: "#3f6fb3", strokeWidth: 2 }, - markerEnd: { type: MarkerType.ArrowClosed, color: "#75aafc" }, - }} - connectionLineStyle={{ stroke: "#f59e0b", strokeWidth: 2 }} - proOptions={{ hideAttribution: true }} - > - <Background - variant={BackgroundVariant.Dots} - gap={20} - size={1} - color="#1a2744" - /> - <Controls - className="!bg-[#0a1628] !border-[rgba(117,170,252,0.3)] !rounded-lg" - showInteractive={false} - /> - </ReactFlow> - )} - </div> - - {/* Detail panel */} - {selectedDefinition && ( - <DefinitionDetailPanel - definition={selectedDefinition} - onClose={() => setSelectedNodeId(null)} - onDelete={handleDeleteDefinition} - onRemoveDependency={handleRemoveDependency} - /> - )} - </div> - - {/* Footer with stats */} - <div className="p-3 border-t border-[rgba(117,170,252,0.2)] bg-[#0a1628]"> - <div className="flex items-center gap-4 font-mono text-[10px] text-[#8b949e]"> - {showDefinitions ? ( - <> - <span>{definitions.length} definitions</span> - {canEdit && ( - <> - <span className="text-[#556677]">|</span> - <span className="text-[#556677]">Drag nodes to reposition</span> - <span className="text-[#556677]">|</span> - <span className="text-[#556677]"> - Drag from <span className="text-[#f59e0b]">●</span> to link - </span> - </> - )} - <span className="flex-1" /> - {canEdit && ( - <button - onClick={() => setShowAddDefinition(true)} - className="text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors" - > - + Add Definition - </button> - )} - </> - ) : ( - <> - <span>{chain.contracts.length} contracts</span> - <span> - {chain.contracts.filter((c) => c.contractStatus === "completed").length}{" "} - completed - </span> - <span> - {chain.contracts.filter((c) => c.contractStatus === "active").length} active - </span> - <span className="flex-1" /> - <span>Double-click node to open contract</span> - </> - )} - </div> - </div> - - {/* Add Definition Modal */} - {showAddDefinition && ( - <AddDefinitionModal - existingNames={definitions.map((d) => d.name)} - onSubmit={handleAddDefinition} - onCancel={() => setShowAddDefinition(false)} - /> - )} - - {/* Add Repository Modal */} - {showAddRepo && ( - <AddRepositoryModal - onSubmit={handleAddRepository} - onCancel={() => setShowAddRepo(false)} - /> - )} - </div> - ); -} - -// Detail panel for definitions -interface DefinitionDetailPanelProps { - definition: ChainContractDefinition; - onClose: () => void; - onDelete: (id: string) => void; - onRemoveDependency: (nodeId: string, depName: string) => void; -} - -function DefinitionDetailPanel({ - definition, - onClose, - onDelete, - onRemoveDependency, -}: DefinitionDetailPanelProps) { - const dependencies = definition.dependsOnNames || []; - - return ( - <div className="w-72 border-l border-[rgba(117,170,252,0.2)] bg-[#0a1628] overflow-y-auto"> - <div className="p-3 border-b border-[rgba(117,170,252,0.2)]"> - <div className="flex items-center justify-between mb-2"> - <h3 className="font-mono text-xs text-[#75aafc] uppercase">Definition Details</h3> - <button - onClick={onClose} - className="font-mono text-xs text-[#8b949e] hover:text-[#dbe7ff]" - > - ✕ - </button> - </div> - </div> - - <div className="p-3 space-y-4"> - {/* Name */} - <div> - <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1"> - Name - </label> - <p className="font-mono text-sm text-[#dbe7ff]">{definition.name}</p> - </div> - - {/* Description */} - {definition.description && ( - <div> - <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1"> - Description - </label> - <p className="font-mono text-xs text-[#9bc3ff]">{definition.description}</p> - </div> - )} - - {/* Type */} - <div> - <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1"> - Type - </label> - <p className="font-mono text-xs text-[#9bc3ff]">{definition.contractType}</p> - </div> - - {/* Dependencies */} - {dependencies.length > 0 && ( - <div> - <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1"> - Dependencies - </label> - <div className="space-y-1"> - {dependencies.map((dep) => ( - <div - key={dep} - className="flex items-center justify-between bg-[rgba(117,170,252,0.1)] px-2 py-1 rounded" - > - <span className="font-mono text-xs text-[#9bc3ff]">{dep}</span> - <button - onClick={() => onRemoveDependency(definition.id, dep)} - className="font-mono text-[10px] text-red-400 hover:text-red-300" - > - ✕ - </button> - </div> - ))} - </div> - </div> - )} - - {/* Delete button */} - <div className="pt-4 border-t border-[rgba(117,170,252,0.2)]"> - <button - onClick={() => onDelete(definition.id)} - className="w-full px-3 py-2 font-mono text-xs text-red-400 border border-red-400/30 hover:bg-red-400/10 transition-colors" - > - Delete Definition - </button> - </div> - </div> - </div> - ); -} - -// Add Definition Modal -interface AddDefinitionModalProps { - existingNames: string[]; - onSubmit: (req: AddContractDefinitionRequest) => void; - onCancel: () => void; -} - -function AddDefinitionModal({ existingNames, onSubmit, onCancel }: AddDefinitionModalProps) { - const [name, setName] = useState(""); - const [description, setDescription] = useState(""); - const [contractType, setContractType] = useState("simple"); - const [initialPhase, setInitialPhase] = useState("plan"); - const [dependsOn, setDependsOn] = useState<string[]>([]); - - const isCheckpoint = contractType === "checkpoint"; - - const handleSubmit = () => { - if (!name.trim()) return; - const req: AddContractDefinitionRequest = { - name: name.trim(), - description: description.trim() || undefined, - contractType, - initialPhase: isCheckpoint ? "execute" : initialPhase, - dependsOn: dependsOn.length > 0 ? dependsOn : undefined, - }; - onSubmit(req); - }; - - const toggleDependency = (depName: string) => { - setDependsOn((prev) => - prev.includes(depName) ? prev.filter((d) => d !== depName) : [...prev, depName] - ); - }; - - return ( - <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"> - <div className="w-full max-w-lg p-6 bg-[#0a1628] border border-[rgba(117,170,252,0.3)]"> - <h3 className="font-mono text-sm text-[#75aafc] uppercase mb-4"> - Add Contract Definition - </h3> - - <div className="space-y-4"> - {/* Name */} - <div> - <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1"> - Name * - </label> - <input - type="text" - value={name} - onChange={(e) => setName(e.target.value)} - className="w-full px-3 py-2 bg-[#050d18] border border-[rgba(117,170,252,0.3)] font-mono text-sm text-[#dbe7ff] focus:border-[#75aafc] focus:outline-none" - placeholder="e.g., Research Phase" - /> - </div> - - {/* Description */} - <div> - <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1"> - Description - </label> - <textarea - value={description} - onChange={(e) => setDescription(e.target.value)} - className="w-full px-3 py-2 bg-[#050d18] border border-[rgba(117,170,252,0.3)] font-mono text-sm text-[#dbe7ff] focus:border-[#75aafc] focus:outline-none resize-none h-20" - placeholder="Optional description..." - /> - </div> - - {/* Type */} - <div> - <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1"> - Contract Type - </label> - <select - value={contractType} - onChange={(e) => setContractType(e.target.value)} - className="w-full px-3 py-2 bg-[#050d18] border border-[rgba(117,170,252,0.3)] font-mono text-sm text-[#dbe7ff] focus:border-[#75aafc] focus:outline-none" - > - <option value="simple">Simple</option> - <option value="checkpoint">Checkpoint (validation)</option> - </select> - </div> - - {/* Initial Phase (not for checkpoints) */} - {!isCheckpoint && ( - <div> - <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1"> - Initial Phase - </label> - <select - value={initialPhase} - onChange={(e) => setInitialPhase(e.target.value)} - className="w-full px-3 py-2 bg-[#050d18] border border-[rgba(117,170,252,0.3)] font-mono text-sm text-[#dbe7ff] focus:border-[#75aafc] focus:outline-none" - > - <option value="plan">Plan</option> - <option value="execute">Execute</option> - </select> - </div> - )} - - {/* Dependencies */} - {existingNames.length > 0 && ( - <div> - <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1"> - Depends On - </label> - <div className="flex flex-wrap gap-2"> - {existingNames.map((depName) => ( - <button - key={depName} - type="button" - onClick={() => toggleDependency(depName)} - className={`px-2 py-1 font-mono text-xs border transition-colors ${ - dependsOn.includes(depName) - ? "bg-[#75aafc]/20 border-[#75aafc] text-[#75aafc]" - : "border-[rgba(117,170,252,0.3)] text-[#8b949e] hover:border-[#75aafc]" - }`} - > - {depName} - </button> - ))} - </div> - </div> - )} - </div> - - {/* Actions */} - <div className="flex justify-end gap-2 mt-6"> - <button - onClick={onCancel} - className="px-4 py-2 font-mono text-xs text-[#8b949e] border border-[rgba(117,170,252,0.3)] hover:border-[#75aafc] transition-colors" - > - Cancel - </button> - <button - onClick={handleSubmit} - disabled={!name.trim()} - className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors disabled:opacity-50" - > - Add Definition - </button> - </div> - </div> - </div> - ); -} - -// Icons -function ChainIcon({ className }: { className?: string }) { - return ( - <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" - /> - </svg> - ); -} - -function CheckIcon({ className }: { className?: string }) { - return ( - <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - /> - </svg> - ); -} - -function RepoIcon({ className }: { className?: string }) { - return ( - <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" - /> - </svg> - ); -} - -// Add Repository Modal -interface AddRepositoryModalProps { - onSubmit: (req: AddChainRepositoryRequest) => void; - onCancel: () => void; -} - -function AddRepositoryModal({ onSubmit, onCancel }: AddRepositoryModalProps) { - const [name, setName] = useState(""); - const [repositoryUrl, setRepositoryUrl] = useState(""); - const [localPath, setLocalPath] = useState(""); - const [sourceType, setSourceType] = useState<"remote" | "local">("remote"); - const [isPrimary, setIsPrimary] = useState(false); - - const handleSubmit = () => { - if (!name.trim()) return; - if (sourceType === "remote" && !repositoryUrl.trim()) return; - if (sourceType === "local" && !localPath.trim()) return; - - const req: AddChainRepositoryRequest = { - name: name.trim(), - sourceType, - isPrimary, - ...(sourceType === "remote" - ? { repositoryUrl: repositoryUrl.trim() } - : { localPath: localPath.trim() }), - }; - onSubmit(req); - }; - - 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 Repository</h3> - - <div className="space-y-4"> - {/* Name */} - <div> - <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1"> - Name * - </label> - <input - type="text" - value={name} - onChange={(e) => setName(e.target.value)} - className="w-full px-3 py-2 bg-[#050d18] border border-[rgba(117,170,252,0.3)] font-mono text-sm text-[#dbe7ff] focus:border-[#75aafc] focus:outline-none" - placeholder="e.g., Main Repository" - /> - </div> - - {/* Source Type */} - <div> - <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1"> - Source Type - </label> - <div className="flex gap-4"> - <label className="flex items-center gap-2 font-mono text-sm text-[#dbe7ff]"> - <input - type="radio" - checked={sourceType === "remote"} - onChange={() => setSourceType("remote")} - className="accent-[#75aafc]" - /> - Remote (URL) - </label> - <label className="flex items-center gap-2 font-mono text-sm text-[#dbe7ff]"> - <input - type="radio" - checked={sourceType === "local"} - onChange={() => setSourceType("local")} - className="accent-[#75aafc]" - /> - Local Path - </label> - </div> - </div> - - {/* Repository URL or Local Path */} - {sourceType === "remote" ? ( - <div> - <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1"> - Repository URL * - </label> - <input - type="text" - value={repositoryUrl} - onChange={(e) => setRepositoryUrl(e.target.value)} - className="w-full px-3 py-2 bg-[#050d18] border border-[rgba(117,170,252,0.3)] font-mono text-sm text-[#dbe7ff] focus:border-[#75aafc] focus:outline-none" - placeholder="https://github.com/user/repo" - /> - </div> - ) : ( - <div> - <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1"> - Local Path * - </label> - <input - type="text" - value={localPath} - onChange={(e) => setLocalPath(e.target.value)} - className="w-full px-3 py-2 bg-[#050d18] border border-[rgba(117,170,252,0.3)] font-mono text-sm text-[#dbe7ff] focus:border-[#75aafc] focus:outline-none" - placeholder="/path/to/local/repo" - /> - </div> - )} - - {/* Primary checkbox */} - <label className="flex items-center gap-2 font-mono text-sm text-[#dbe7ff]"> - <input - type="checkbox" - checked={isPrimary} - onChange={(e) => setIsPrimary(e.target.checked)} - className="accent-[#75aafc]" - /> - Set as primary repository - </label> - </div> - - {/* Actions */} - <div className="flex justify-end gap-2 mt-6"> - <button - onClick={onCancel} - className="px-4 py-2 font-mono text-xs text-[#8b949e] border border-[rgba(117,170,252,0.3)] hover:border-[#75aafc] transition-colors" - > - Cancel - </button> - <button - onClick={handleSubmit} - disabled={ - !name.trim() || - (sourceType === "remote" && !repositoryUrl.trim()) || - (sourceType === "local" && !localPath.trim()) - } - className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors disabled:opacity-50" - > - Add Repository - </button> - </div> - </div> - </div> - ); -} diff --git a/makima/frontend/src/components/chains/ChainList.tsx b/makima/frontend/src/components/chains/ChainList.tsx deleted file mode 100644 index e185efc..0000000 --- a/makima/frontend/src/components/chains/ChainList.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import { useState, useCallback } from "react"; -import type { ChainSummary, ChainStatus } from "../../lib/api"; - -interface ChainListProps { - chains: ChainSummary[]; - loading: boolean; - onSelect: (chainId: string) => void; - onCreate: () => void; - selectedId?: string; - onArchive: (chain: ChainSummary) => void; -} - -const statusColors: Record<ChainStatus, string> = { - pending: "text-yellow-400", - active: "text-green-400", - completed: "text-blue-400", - archived: "text-[#555]", -}; - -export function ChainList({ - chains, - loading, - onSelect, - onCreate, - selectedId, - onArchive, -}: ChainListProps) { - const [filter, setFilter] = useState<ChainStatus | "all">("all"); - const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null); - const [contextMenuChain, setContextMenuChain] = useState<ChainSummary | null>(null); - - const filteredChains = - filter === "all" - ? chains - : chains.filter((c) => c.status === filter); - - const handleContextMenu = useCallback( - (e: React.MouseEvent, chain: ChainSummary) => { - e.preventDefault(); - setContextMenuPosition({ x: e.clientX, y: e.clientY }); - setContextMenuChain(chain); - }, - [] - ); - - const closeContextMenu = useCallback(() => { - setContextMenuPosition(null); - setContextMenuChain(null); - }, []); - - const handleArchive = useCallback(() => { - if (contextMenuChain) { - onArchive(contextMenuChain); - closeContextMenu(); - } - }, [contextMenuChain, onArchive, closeContextMenu]); - - if (loading) { - return ( - <div className="panel h-full flex items-center justify-center"> - <div className="font-mono text-[#9bc3ff] text-sm">Loading...</div> - </div> - ); - } - - return ( - <div className="panel h-full flex flex-col" onClick={closeContextMenu}> - {/* Header */} - <div className="p-4 border-b border-dashed border-[rgba(117,170,252,0.35)]"> - <div className="flex items-center justify-between mb-3"> - <h2 className="font-mono text-sm text-[#75aafc] uppercase tracking-wider"> - Chains - </h2> - <button - onClick={onCreate} - className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase" - > - + New - </button> - </div> - - {/* Filter tabs */} - <div className="flex gap-1"> - {(["all", "active", "completed", "archived"] as const).map((status) => ( - <button - key={status} - onClick={() => setFilter(status)} - className={` - px-2 py-1 font-mono text-[10px] uppercase tracking-wider transition-colors - ${ - filter === status - ? "bg-[rgba(117,170,252,0.1)] text-[#9bc3ff] border border-[rgba(117,170,252,0.3)]" - : "text-[#555] hover:text-[#75aafc]" - } - `} - > - {status} - </button> - ))} - </div> - </div> - - {/* Chain list */} - <div className="flex-1 overflow-y-auto"> - {filteredChains.length === 0 ? ( - <div className="p-4 text-center"> - <p className="font-mono text-sm text-[#555]"> - {filter === "all" - ? "No chains yet" - : `No ${filter} chains`} - </p> - </div> - ) : ( - <div className="divide-y divide-[rgba(117,170,252,0.15)]"> - {filteredChains.map((chain) => ( - <button - key={chain.id} - onClick={() => onSelect(chain.id)} - onContextMenu={(e) => handleContextMenu(e, chain)} - className={` - w-full text-left p-4 transition-colors - ${ - selectedId === chain.id - ? "bg-[rgba(117,170,252,0.1)]" - : "hover:bg-[rgba(117,170,252,0.05)]" - } - `} - > - <div className="flex items-start justify-between gap-2 mb-2"> - <h3 className="font-mono text-sm text-[#dbe7ff] truncate"> - {chain.name} - </h3> - <span - className={`text-[10px] font-mono uppercase shrink-0 ${ - statusColors[chain.status] - }`} - > - {chain.status} - </span> - </div> - - {chain.description && ( - <p className="font-mono text-xs text-[#555] mb-2 line-clamp-2"> - {chain.description} - </p> - )} - - <div className="flex items-center gap-4 text-[10px] font-mono text-[#555]"> - <span>{chain.contractCount} contracts</span> - <span>{chain.completedContractCount} completed</span> - {chain.loopEnabled && ( - <span className="text-amber-400"> - loop {chain.loopCurrentIteration || 0}/{chain.loopMaxIterations || "∞"} - </span> - )} - </div> - </button> - ))} - </div> - )} - </div> - - {/* Context Menu */} - {contextMenuPosition && contextMenuChain && ( - <div - className="fixed z-50 bg-[#0a1628] border border-[rgba(117,170,252,0.3)] shadow-lg py-1 min-w-[150px]" - style={{ top: contextMenuPosition.y, left: contextMenuPosition.x }} - onClick={(e) => e.stopPropagation()} - > - <button - onClick={() => { - onSelect(contextMenuChain.id); - closeContextMenu(); - }} - className="w-full px-4 py-2 text-left font-mono text-xs text-[#dbe7ff] hover:bg-[rgba(117,170,252,0.1)] transition-colors" - > - View Details - </button> - {contextMenuChain.status !== "archived" && ( - <button - onClick={handleArchive} - className="w-full px-4 py-2 text-left font-mono text-xs text-red-400 hover:bg-red-400/10 transition-colors" - > - Archive - </button> - )} - </div> - )} - </div> - ); -} |
