summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/chains
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/components/chains')
-rw-r--r--makima/frontend/src/components/chains/ChainEditor.tsx1278
-rw-r--r--makima/frontend/src/components/chains/ChainList.tsx191
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>
- );
-}