summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--makima/frontend/package-lock.json168
-rw-r--r--makima/frontend/package.json1
-rw-r--r--makima/frontend/src/components/chains/ChainEditor.tsx1696
3 files changed, 707 insertions, 1158 deletions
diff --git a/makima/frontend/package-lock.json b/makima/frontend/package-lock.json
index cc842b1..38adfc4 100644
--- a/makima/frontend/package-lock.json
+++ b/makima/frontend/package-lock.json
@@ -9,6 +9,7 @@
"version": "0.1.0",
"dependencies": {
"@supabase/supabase-js": "^2.90.1",
+ "@xyflow/react": "^12.10.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router": "^7.1.0",
@@ -1791,6 +1792,15 @@
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="
},
+ "node_modules/@types/d3-drag": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+ "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
"node_modules/@types/d3-ease": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
@@ -1817,6 +1827,12 @@
"@types/d3-time": "*"
}
},
+ "node_modules/@types/d3-selection": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
+ "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
+ "license": "MIT"
+ },
"node_modules/@types/d3-shape": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
@@ -1835,6 +1851,25 @@
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="
},
+ "node_modules/@types/d3-transition": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
+ "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-zoom": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+ "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-interpolate": "*",
+ "@types/d3-selection": "*"
+ }
+ },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -1915,6 +1950,38 @@
"node": ">=0.10.0"
}
},
+ "node_modules/@xyflow/react": {
+ "version": "12.10.0",
+ "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.0.tgz",
+ "integrity": "sha512-eOtz3whDMWrB4KWVatIBrKuxECHqip6PfA8fTpaS2RUGVpiEAe+nqDKsLqkViVWxDGreq0lWX71Xth/SPAzXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "@xyflow/system": "0.0.74",
+ "classcat": "^5.0.3",
+ "zustand": "^4.4.0"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@xyflow/system": {
+ "version": "0.0.74",
+ "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.74.tgz",
+ "integrity": "sha512-7v7B/PkiVrkdZzSbL+inGAo6tkR/WQHHG0/jhSvLQToCsfa8YubOGmBYd1s08tpKpihdHDZFwzQZeR69QSBb4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-drag": "^3.0.7",
+ "@types/d3-interpolate": "^3.0.4",
+ "@types/d3-selection": "^3.0.10",
+ "@types/d3-transition": "^3.0.8",
+ "@types/d3-zoom": "^3.0.8",
+ "d3-drag": "^3.0.0",
+ "d3-interpolate": "^3.0.1",
+ "d3-selection": "^3.0.0",
+ "d3-zoom": "^3.0.0"
+ }
+ },
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
@@ -2035,6 +2102,12 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/classcat": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
+ "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
+ "license": "MIT"
+ },
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -2092,6 +2165,28 @@
"node": ">=12"
}
},
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
@@ -2142,6 +2237,16 @@
"node": ">=12"
}
},
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "license": "ISC",
+ "peer": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/d3-shape": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
@@ -2183,6 +2288,41 @@
"node": ">=12"
}
},
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -3341,6 +3481,34 @@
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true
+ },
+ "node_modules/zustand": {
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+ "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.2.2"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0.6",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
}
}
}
diff --git a/makima/frontend/package.json b/makima/frontend/package.json
index 9293f65..3b908aa 100644
--- a/makima/frontend/package.json
+++ b/makima/frontend/package.json
@@ -12,6 +12,7 @@
},
"dependencies": {
"@supabase/supabase-js": "^2.90.1",
+ "@xyflow/react": "^12.10.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router": "^7.1.0",
diff --git a/makima/frontend/src/components/chains/ChainEditor.tsx b/makima/frontend/src/components/chains/ChainEditor.tsx
index a4c8a39..5b77170 100644
--- a/makima/frontend/src/components/chains/ChainEditor.tsx
+++ b/makima/frontend/src/components/chains/ChainEditor.tsx
@@ -1,8 +1,26 @@
-import { useState, useCallback, useMemo, useRef, useEffect } from "react";
+import { useState, useCallback, useEffect, useMemo } from "react";
+import {
+ ReactFlow,
+ Node,
+ Edge,
+ Controls,
+ Background,
+ useNodesState,
+ useEdgesState,
+ addEdge,
+ Connection,
+ NodeProps,
+ Handle,
+ Position,
+ BackgroundVariant,
+ NodeChange,
+ MarkerType,
+} from "@xyflow/react";
+import "@xyflow/react/dist/style.css";
+
import type {
ChainWithContracts,
ChainGraphResponse,
- ChainContractDetail,
ChainContractDefinition,
ChainDefinitionGraphResponse,
AddContractDefinitionRequest,
@@ -33,10 +51,172 @@ interface ChainEditorProps {
onContractClick: (contractId: string) => void;
}
-// Node dimensions
-const NODE_WIDTH = 180;
+// Node dimensions for layout
+const NODE_WIDTH = 200;
const NODE_HEIGHT = 80;
-const CANVAS_PADDING = 40;
+const GRID_SPACING_X = 280;
+const GRID_SPACING_Y = 120;
+
+// Custom node component for definitions
+function DefinitionNode({ data, selected }: NodeProps) {
+ const isCheckpoint = data.contractType === "checkpoint";
+ const status = data.isInstantiated ? data.contractStatus || "pending" : "pending";
+ const colors = getStatusColor(status, isCheckpoint);
+
+ return (
+ <div
+ className={`rounded-lg border-2 overflow-hidden ${
+ isCheckpoint ? "bg-[#0f0a1e]" : "bg-[#0a1628]"
+ } ${selected ? "ring-2 ring-offset-2 ring-offset-[#050d18]" : ""} ${
+ selected ? (isCheckpoint ? "ring-[#a78bfa]" : "ring-[#75aafc]") : ""
+ }`}
+ style={{
+ width: NODE_WIDTH,
+ height: NODE_HEIGHT,
+ borderColor: selected
+ ? isCheckpoint
+ ? "#a78bfa"
+ : "#75aafc"
+ : colors.border,
+ borderStyle: data.isInstantiated ? "solid" : "dashed",
+ }}
+ >
+ {/* Top handle for incoming edges */}
+ <Handle
+ type="target"
+ position={Position.Top}
+ className="!bg-[#75aafc] !w-3 !h-3 !border-2 !border-[#0a1628]"
+ />
+
+ {/* Status indicator bar */}
+ <div className="h-1.5" style={{ backgroundColor: colors.bg }} />
+
+ {/* Content */}
+ <div className="p-2">
+ <div className="flex items-center justify-between mb-1">
+ <span className="font-mono text-xs text-[#dbe7ff] truncate flex-1">
+ {data.name}
+ </span>
+ {isCheckpoint ? (
+ <CheckIcon className="w-4 h-4 text-[#a78bfa] opacity-70 flex-shrink-0 ml-1" />
+ ) : (
+ <ChainIcon className="w-4 h-4 text-[#75aafc] opacity-50 flex-shrink-0 ml-1" />
+ )}
+ </div>
+ <div className="flex items-center justify-between">
+ <span
+ className="font-mono text-[10px] uppercase px-1.5 py-0.5 rounded"
+ style={{
+ color: colors.bg,
+ backgroundColor: `${colors.bg}20`,
+ }}
+ >
+ {data.isInstantiated ? status : isCheckpoint ? "checkpoint" : "definition"}
+ </span>
+ <span className="font-mono text-[10px] text-[#8b949e]">
+ {data.contractType}
+ </span>
+ </div>
+ </div>
+
+ {/* Bottom handle for outgoing edges */}
+ <Handle
+ type="source"
+ position={Position.Bottom}
+ className="!bg-[#f59e0b] !w-3 !h-3 !border-2 !border-[#0a1628]"
+ />
+ </div>
+ );
+}
+
+// Custom node for contracts (active chains)
+function ContractNode({ data, selected }: NodeProps) {
+ const colors = getStatusColor(data.status);
+
+ return (
+ <div
+ className={`rounded-lg border-2 bg-[#0a1628] overflow-hidden ${
+ selected ? "ring-2 ring-[#75aafc] ring-offset-2 ring-offset-[#050d18]" : ""
+ }`}
+ style={{
+ width: NODE_WIDTH,
+ height: NODE_HEIGHT,
+ borderColor: selected ? "#75aafc" : colors.border,
+ }}
+ >
+ <Handle
+ type="target"
+ position={Position.Top}
+ className="!bg-[#75aafc] !w-3 !h-3 !border-2 !border-[#0a1628]"
+ />
+
+ <div className="h-1.5" style={{ backgroundColor: colors.bg }} />
+
+ <div className="p-2">
+ <div className="flex items-center justify-between mb-1">
+ <span className="font-mono text-xs text-[#dbe7ff] truncate flex-1">
+ {data.name}
+ </span>
+ <ChainIcon className="w-4 h-4 text-[#75aafc] opacity-50 flex-shrink-0 ml-1" />
+ </div>
+ <div className="flex items-center justify-between">
+ <span
+ className="font-mono text-[10px] uppercase px-1.5 py-0.5 rounded"
+ style={{
+ color: colors.bg,
+ backgroundColor: `${colors.bg}20`,
+ }}
+ >
+ {data.status}
+ </span>
+ {data.phase && (
+ <span className="font-mono text-[10px] text-[#8b949e]">{data.phase}</span>
+ )}
+ </div>
+ </div>
+
+ <Handle
+ type="source"
+ position={Position.Bottom}
+ className="!bg-[#f59e0b] !w-3 !h-3 !border-2 !border-[#0a1628]"
+ />
+ </div>
+ );
+}
+
+const nodeTypes = {
+ definition: DefinitionNode,
+ contract: ContractNode,
+};
+
+function getStatusColor(status: string, isCheckpoint = false) {
+ if (isCheckpoint) {
+ switch (status) {
+ case "active":
+ return { bg: "#a78bfa", border: "#8b5cf6", text: "#5b21b6" };
+ case "completed":
+ return { bg: "#818cf8", border: "#6366f1", text: "#3730a3" };
+ case "pending":
+ return { bg: "#c4b5fd", border: "#a78bfa", text: "#6d28d9" };
+ case "failed":
+ return { bg: "#ef4444", border: "#dc2626", text: "#991b1b" };
+ default:
+ return { bg: "#a78bfa", border: "#8b5cf6", text: "#5b21b6" };
+ }
+ }
+ switch (status) {
+ case "active":
+ return { bg: "#4ade80", border: "#22c55e", text: "#166534" };
+ case "completed":
+ return { bg: "#60a5fa", border: "#3b82f6", text: "#1e40af" };
+ case "pending":
+ return { bg: "#f59e0b", border: "#d97706", text: "#92400e" };
+ case "blocked":
+ return { bg: "#ef4444", border: "#dc2626", text: "#991b1b" };
+ default:
+ return { bg: "#6b7280", border: "#4b5563", text: "#374151" };
+ }
+}
export function ChainEditor({
chain,
@@ -46,34 +226,19 @@ export function ChainEditor({
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);
+ const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
- // Drag state
- const [draggedNode, setDraggedNode] = useState<string | null>(null);
- const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
- const [localPositions, setLocalPositions] = useState<Map<string, { x: number; y: number }>>(new Map());
-
- // Edge drawing state
- const [edgeDrawing, setEdgeDrawing] = useState<{
- fromNode: string;
- toPoint: { x: number; y: number };
- } | null>(null);
-
- // Context menu state (nodeId is null for canvas context menu)
- const [contextMenu, setContextMenu] = useState<{
- x: number;
- y: number;
- nodeId: string | null;
- canvasPosition?: { gridX: number; gridY: number };
- } | null>(null);
+ const [nodes, setNodes, onNodesChange] = useNodesState([]);
+ const [edges, setEdges, onEdgesChange] = useEdgesState([]);
+
+ const showDefinitions = chain.status === "pending" || chain.status === "archived";
+ const canEdit = chain.status === "pending";
// Load definitions when chain changes
useEffect(() => {
@@ -92,152 +257,167 @@ export function ChainEditor({
loadDefinitions();
}, [chain.id]);
- // Determine which view to show: definitions (pending chain) or contracts (active/completed)
- const showDefinitions = chain.status === "pending" || chain.status === "archived";
- const currentGraph = showDefinitions ? definitionGraph : graph;
-
- // Use positions from graph nodes, with local overrides for dragging
- const nodePositions = useMemo(() => {
- const positions = new Map<string, { x: number; y: number }>();
-
- if (showDefinitions && definitionGraph?.nodes) {
- for (const node of definitionGraph.nodes) {
- // Use local position if available (during drag), otherwise use server position
- const localPos = localPositions.get(node.id);
- if (localPos) {
- positions.set(node.id, localPos);
- } else {
- positions.set(node.id, {
- x: CANVAS_PADDING + (node.x || 0) * (NODE_WIDTH + 60),
- y: CANVAS_PADDING + (node.y || 0) * (NODE_HEIGHT + 40),
- });
+ // 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);
+ }
}
}
- } 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),
- });
- }
- }
+ },
+ [chain.id, onNodesChange]
+ );
- return positions;
- }, [showDefinitions, definitionGraph?.nodes, graph?.nodes, localPositions]);
+ // Handle new edge connections
+ const handleConnect = useCallback(
+ async (connection: Connection) => {
+ if (!connection.source || !connection.target) return;
- // Canvas dimensions
- const canvasDimensions = useMemo(() => {
- if (nodePositions.size === 0) {
- return { width: 600, height: 400 };
- }
+ // Find the definitions
+ const sourceDef = definitions.find((d) => d.id === connection.source);
+ const targetDef = definitions.find((d) => d.id === connection.target);
- 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);
- }
+ if (!sourceDef || !targetDef) return;
- return {
- width: Math.max(600, maxX + CANVAS_PADDING),
- height: Math.max(400, maxY + CANVAS_PADDING),
- };
- }, [nodePositions]);
+ // 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],
+ });
- const handleNodeClick = useCallback((nodeId: string) => {
- setSelectedNode(nodeId);
- }, []);
+ // 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]
+ );
- const handleNodeDoubleClick = useCallback(
- (nodeId: string) => {
- // For definitions, we can't open a contract yet
- if (showDefinitions) return;
- onContractClick(nodeId);
+ // Handle node selection
+ const handleNodeClick = useCallback(
+ (_: React.MouseEvent, node: Node) => {
+ setSelectedNodeId(node.id);
},
- [onContractClick, showDefinitions]
+ []
);
- const getStatusColor = (status: string, isCheckpoint = false) => {
- // Checkpoint contracts use purple/violet colors
- if (isCheckpoint) {
- switch (status) {
- case "active":
- return { bg: "#a78bfa", border: "#8b5cf6", text: "#5b21b6" };
- case "completed":
- return { bg: "#818cf8", border: "#6366f1", text: "#3730a3" };
- case "pending":
- return { bg: "#c4b5fd", border: "#a78bfa", text: "#6d28d9" };
- case "failed":
- return { bg: "#ef4444", border: "#dc2626", text: "#991b1b" };
- default:
- return { bg: "#a78bfa", border: "#8b5cf6", text: "#5b21b6" };
+ // Handle node double-click (open contract)
+ const handleNodeDoubleClick = useCallback(
+ (_: React.MouseEvent, node: Node) => {
+ if (!showDefinitions) {
+ onContractClick(node.id);
}
- }
- 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]);
+ },
+ [showDefinitions, onContractClick]
+ );
- 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]);
+ // Handle pane click (deselect)
+ const handlePaneClick = useCallback(() => {
+ setSelectedNodeId(null);
+ }, []);
- // Position for new definition (set from canvas right-click)
- const [newDefinitionPosition, setNewDefinitionPosition] = useState<{ x: number; y: number } | null>(null);
+ // Find selected definition
+ const selectedDefinition = showDefinitions && selectedNodeId
+ ? definitions.find((d) => d.id === selectedNodeId)
+ : null;
- // Find free space on the grid for new definitions
+ // Find free position for new definition
const findFreePosition = useCallback(() => {
if (!definitionGraph?.nodes || definitionGraph.nodes.length === 0) {
return { x: 0, y: 0 };
}
- // Get all occupied positions
const occupied = new Set<string>();
for (const node of definitionGraph.nodes) {
occupied.add(`${node.x},${node.y}`);
}
- // Find first free position by scanning row by row
for (let y = 0; y < 10; y++) {
for (let x = 0; x < 10; x++) {
if (!occupied.has(`${x},${y}`)) {
@@ -246,243 +426,102 @@ export function ChainEditor({
}
}
- // Fallback: place at the end of the last row
const maxY = Math.max(...definitionGraph.nodes.map((n) => n.y || 0));
return { x: 0, y: maxY + 1 };
}, [definitionGraph?.nodes]);
- const handleAddDefinition = useCallback(async (req: AddContractDefinitionRequest) => {
+ 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 {
- // Use specified position or find free space
- const position = newDefinitionPosition || findFreePosition();
- const reqWithPosition = { ...req, editorX: position.x, editorY: position.y };
- await createChainDefinition(chain.id, reqWithPosition);
- // Reload definitions
- const [defs, defGraph] = await Promise.all([
- listChainDefinitions(chain.id),
- getChainDefinitionGraph(chain.id),
- ]);
- setDefinitions(defs);
- setDefinitionGraph(defGraph);
- setShowAddDefinition(false);
- setNewDefinitionPosition(null);
+ await startChain(chain.id);
+ onRefresh();
} catch (err) {
- console.error("Failed to add definition:", err);
- setError(err instanceof Error ? err.message : "Failed to add definition");
+ setError(err instanceof Error ? err.message : "Failed to start chain");
+ } finally {
+ setIsStarting(false);
}
- }, [chain.id, newDefinitionPosition, findFreePosition]);
+ }, [chain.id, onRefresh]);
- const handleDeleteDefinition = useCallback(async (definitionId: string) => {
- if (!confirm("Are you sure you want to delete this definition?")) return;
+ const handleStopChain = useCallback(async () => {
+ if (!confirm("Are you sure you want to stop this chain?")) return;
+ setIsStopping(true);
+ setError(null);
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);
+ await stopChain(chain.id);
+ onRefresh();
} catch (err) {
- console.error("Failed to delete definition:", err);
- setError(err instanceof Error ? err.message : "Failed to delete definition");
+ setError(err instanceof Error ? err.message : "Failed to stop chain");
+ } finally {
+ setIsStopping(false);
}
- }, [chain.id]);
-
- // Refresh definitions helper
- const refreshDefinitions = useCallback(async () => {
- const [defs, defGraph] = await Promise.all([
- listChainDefinitions(chain.id),
- getChainDefinitionGraph(chain.id),
- ]);
- setDefinitions(defs);
- setDefinitionGraph(defGraph);
- setLocalPositions(new Map()); // Clear local positions
- }, [chain.id]);
-
- // Context menu handlers
- const handleContextMenu = useCallback((e: React.MouseEvent, nodeId: string) => {
- e.preventDefault();
- e.stopPropagation();
- setContextMenu({ x: e.clientX, y: e.clientY, nodeId });
- }, []);
-
- // Canvas context menu (for creating new definitions)
- const handleCanvasContextMenu = useCallback((e: React.MouseEvent) => {
- if (!showDefinitions || chain.status !== "pending") return;
- e.preventDefault();
-
- const canvasRect = canvasRef.current?.getBoundingClientRect();
- if (!canvasRect) return;
-
- // Calculate grid position from click
- const clickX = e.clientX - canvasRect.left;
- const clickY = e.clientY - canvasRect.top;
- const gridX = Math.round((clickX - CANVAS_PADDING) / (NODE_WIDTH + 60));
- const gridY = Math.round((clickY - CANVAS_PADDING) / (NODE_HEIGHT + 40));
-
- setContextMenu({
- x: e.clientX,
- y: e.clientY,
- nodeId: null,
- canvasPosition: { gridX: Math.max(0, gridX), gridY: Math.max(0, gridY) },
- });
- }, [showDefinitions, chain.status]);
-
- const closeContextMenu = useCallback(() => {
- setContextMenu(null);
- }, []);
+ }, [chain.id, onRefresh]);
- // Handle creating definition at canvas position
- const handleCreateAtPosition = useCallback((gridX: number, gridY: number) => {
- setNewDefinitionPosition({ x: gridX, y: gridY });
- setShowAddDefinition(true);
- closeContextMenu();
- }, [closeContextMenu]);
-
- // Handle canvas click to close context menu
- const handleCanvasClick = useCallback(() => {
- if (contextMenu) {
- closeContextMenu();
- }
- }, [contextMenu, closeContextMenu]);
-
- // Drag handlers
- const handleDragStart = useCallback((e: React.MouseEvent, nodeId: string) => {
- if (!showDefinitions || chain.status !== "pending") return;
- e.preventDefault();
- const pos = nodePositions.get(nodeId);
- if (!pos) return;
-
- setDraggedNode(nodeId);
- setDragOffset({
- x: e.clientX - pos.x,
- y: e.clientY - pos.y,
- });
- }, [showDefinitions, chain.status, nodePositions]);
-
- const handleDragMove = useCallback((e: React.MouseEvent) => {
- if (!draggedNode) return;
-
- const canvasRect = canvasRef.current?.getBoundingClientRect();
- if (!canvasRect) return;
-
- const newX = Math.max(CANVAS_PADDING, e.clientX - dragOffset.x);
- const newY = Math.max(CANVAS_PADDING, e.clientY - dragOffset.y);
-
- setLocalPositions(prev => {
- const newMap = new Map(prev);
- newMap.set(draggedNode, { x: newX, y: newY });
- return newMap;
- });
- }, [draggedNode, dragOffset]);
-
- const handleDragEnd = useCallback(async () => {
- if (!draggedNode) return;
-
- const localPos = localPositions.get(draggedNode);
- if (localPos) {
- // Convert pixel position back to grid coordinates
- const gridX = Math.round((localPos.x - CANVAS_PADDING) / (NODE_WIDTH + 60));
- const gridY = Math.round((localPos.y - CANVAS_PADDING) / (NODE_HEIGHT + 40));
+ 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, draggedNode, {
- editorX: gridX,
- editorY: gridY,
- });
- await refreshDefinitions();
- } catch (err) {
- console.error("Failed to update position:", err);
- setError(err instanceof Error ? err.message : "Failed to update position");
- }
- }
+ await updateChainDefinition(chain.id, nodeId, { dependsOn: newDeps });
- setDraggedNode(null);
- }, [draggedNode, localPositions, chain.id, refreshDefinitions]);
-
- // Edge drawing handlers
- const handleEdgeDrawStart = useCallback((e: React.MouseEvent, nodeId: string) => {
- if (!showDefinitions || chain.status !== "pending") return;
- e.preventDefault();
- e.stopPropagation();
-
- const canvasRect = canvasRef.current?.getBoundingClientRect();
- if (!canvasRect) return;
-
- const pos = nodePositions.get(nodeId);
- if (!pos) return;
-
- setEdgeDrawing({
- fromNode: nodeId,
- toPoint: {
- x: pos.x + NODE_WIDTH / 2,
- y: pos.y + NODE_HEIGHT,
- },
- });
- }, [showDefinitions, chain.status, nodePositions]);
-
- const handleEdgeDrawMove = useCallback((e: React.MouseEvent) => {
- if (!edgeDrawing) return;
-
- const canvasRect = canvasRef.current?.getBoundingClientRect();
- if (!canvasRect) return;
-
- setEdgeDrawing(prev => prev ? {
- ...prev,
- toPoint: {
- x: e.clientX - canvasRect.left,
- y: e.clientY - canvasRect.top,
- },
- } : null);
- }, [edgeDrawing]);
-
- const handleEdgeDrawEnd = useCallback(async (targetNodeId: string | null) => {
- if (!edgeDrawing) return;
-
- if (targetNodeId && targetNodeId !== edgeDrawing.fromNode) {
- // Find the target definition and its current dependencies
- const fromDef = definitions.find(d => d.id === edgeDrawing.fromNode);
- const toDef = definitions.find(d => d.id === targetNodeId);
-
- if (fromDef && toDef) {
- // Add dependency: targetNodeId depends on fromNode
- const currentDeps = toDef.dependsOnNames || [];
- if (!currentDeps.includes(fromDef.name)) {
- try {
- await updateChainDefinition(chain.id, targetNodeId, {
- dependsOn: [...currentDeps, fromDef.name],
- });
- await refreshDefinitions();
- } catch (err) {
- console.error("Failed to create dependency:", err);
- setError(err instanceof Error ? err.message : "Failed to create dependency");
- }
- }
+ 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");
}
- }
-
- setEdgeDrawing(null);
- }, [edgeDrawing, definitions, chain.id, refreshDefinitions]);
-
- // Handle removing a dependency via context menu
- const handleRemoveDependency = useCallback(async (nodeId: string, depName: string) => {
- const def = definitions.find(d => d.id === nodeId);
- if (!def) return;
-
- const newDeps = (def.dependsOnNames || []).filter(d => d !== depName);
- try {
- await updateChainDefinition(chain.id, nodeId, {
- dependsOn: newDeps,
- });
- await refreshDefinitions();
- closeContextMenu();
- } catch (err) {
- console.error("Failed to remove dependency:", err);
- setError(err instanceof Error ? err.message : "Failed to remove dependency");
- }
- }, [definitions, chain.id, refreshDefinitions, closeContextMenu]);
+ },
+ [chain.id, definitions]
+ );
return (
<div className="panel h-full flex flex-col">
@@ -494,7 +533,7 @@ export function ChainEditor({
onClick={onBack}
className="font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors"
>
- Back
+ ← Back
</button>
<div>
<h2 className="font-mono text-sm text-[#dbe7ff]">{chain.name}</h2>
@@ -511,7 +550,6 @@ export function ChainEditor({
>
{chain.status}
</span>
- {/* Chain control buttons */}
{chain.status === "pending" && definitions.length > 0 && (
<button
onClick={handleStartChain}
@@ -547,13 +585,13 @@ export function ChainEditor({
{/* Main content */}
<div className="flex-1 flex min-h-0">
- {/* DAG Canvas */}
- <div className="flex-1 overflow-auto bg-[#050d18]">
+ {/* 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>
- ) : !currentGraph || currentGraph.nodes.length === 0 ? (
+ ) : 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">
@@ -566,7 +604,7 @@ export function ChainEditor({
? "Add contract definitions to build your chain"
: "Start the chain to create contracts from definitions"}
</p>
- {showDefinitions && (
+ {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"
@@ -577,293 +615,39 @@ export function ChainEditor({
</div>
</div>
) : (
- <div
- ref={canvasRef}
- className="relative"
- style={{
- width: canvasDimensions.width,
- height: canvasDimensions.height,
- minWidth: "100%",
- minHeight: "100%",
- cursor: draggedNode ? "grabbing" : edgeDrawing ? "crosshair" : "default",
- }}
- onClick={handleCanvasClick}
- onContextMenu={handleCanvasContextMenu}
- onMouseMove={(e) => {
- if (draggedNode) handleDragMove(e);
- if (edgeDrawing) handleEdgeDrawMove(e);
- }}
- onMouseUp={() => {
- if (draggedNode) handleDragEnd();
- if (edgeDrawing) handleEdgeDrawEnd(null);
- }}
- onMouseLeave={() => {
- if (draggedNode) handleDragEnd();
- if (edgeDrawing) setEdgeDrawing(null);
+ <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 }}
>
- {/* SVG layer for edges */}
- <svg
- className="absolute inset-0 pointer-events-none"
- style={{
- width: canvasDimensions.width,
- height: canvasDimensions.height,
- }}
- >
- <defs>
- <marker
- id="arrowhead"
- markerWidth="10"
- markerHeight="7"
- refX="9"
- refY="3.5"
- orient="auto"
- >
- <polygon
- points="0 0, 10 3.5, 0 7"
- fill="#75aafc"
- opacity="0.6"
- />
- </marker>
- <marker
- id="arrowhead-preview"
- markerWidth="10"
- markerHeight="7"
- refX="9"
- refY="3.5"
- orient="auto"
- >
- <polygon
- points="0 0, 10 3.5, 0 7"
- fill="#f59e0b"
- opacity="0.8"
- />
- </marker>
- </defs>
- {currentGraph.edges.map((edge, index) => {
- const fromPos = nodePositions.get(edge.from);
- const toPos = nodePositions.get(edge.to);
- if (!fromPos || !toPos) return null;
-
- // Calculate edge path (from bottom of source to top of target)
- const startX = fromPos.x + NODE_WIDTH / 2;
- const startY = fromPos.y + NODE_HEIGHT;
- const endX = toPos.x + NODE_WIDTH / 2;
- const endY = toPos.y;
-
- // Bezier control points for smooth curves
- const midY = (startY + endY) / 2;
-
- const isHighlighted =
- hoveredNode === edge.from || hoveredNode === edge.to;
-
- return (
- <path
- key={`${edge.from}-${edge.to}-${index}`}
- d={`M ${startX} ${startY} C ${startX} ${midY}, ${endX} ${midY}, ${endX} ${endY}`}
- fill="none"
- stroke={isHighlighted ? "#75aafc" : "#3f6fb3"}
- strokeWidth={isHighlighted ? 2 : 1.5}
- strokeDasharray={isHighlighted ? "none" : "4 2"}
- markerEnd="url(#arrowhead)"
- opacity={isHighlighted ? 1 : 0.6}
- />
- );
- })}
- {/* Edge drawing preview line */}
- {edgeDrawing && (() => {
- const fromPos = nodePositions.get(edgeDrawing.fromNode);
- if (!fromPos) return null;
- const startX = fromPos.x + NODE_WIDTH / 2;
- const startY = fromPos.y + NODE_HEIGHT;
- const endX = edgeDrawing.toPoint.x;
- const endY = edgeDrawing.toPoint.y;
- const midY = (startY + endY) / 2;
- return (
- <path
- d={`M ${startX} ${startY} C ${startX} ${midY}, ${endX} ${midY}, ${endX} ${endY}`}
- fill="none"
- stroke="#f59e0b"
- strokeWidth={2}
- strokeDasharray="6 3"
- markerEnd="url(#arrowhead-preview)"
- opacity={0.8}
- />
- );
- })()}
- </svg>
-
- {/* Node layer */}
- {showDefinitions && definitionGraph
- ? definitionGraph.nodes.map((node) => {
- const pos = nodePositions.get(node.id);
- if (!pos) return null;
-
- const isCheckpoint = node.contractType === "checkpoint";
- const status = node.isInstantiated
- ? node.contractStatus || "pending"
- : "pending";
- const colors = getStatusColor(status, isCheckpoint);
- const isSelected = selectedNode === node.id;
- const isHovered = hoveredNode === node.id;
-
- const isDragging = draggedNode === node.id;
- const canEdit = chain.status === "pending";
-
- return (
- <div
- key={node.id}
- onClick={() => handleNodeClick(node.id)}
- onContextMenu={(e) => canEdit && handleContextMenu(e, node.id)}
- onMouseEnter={() => !draggedNode && setHoveredNode(node.id)}
- onMouseLeave={() => !draggedNode && setHoveredNode(null)}
- onMouseUp={() => edgeDrawing && handleEdgeDrawEnd(node.id)}
- className={`absolute ${
- isDragging ? "z-50 shadow-lg" : "transition-all duration-150"
- } ${
- isSelected
- ? isCheckpoint
- ? "ring-2 ring-[#a78bfa] ring-offset-2 ring-offset-[#050d18]"
- : "ring-2 ring-[#75aafc] ring-offset-2 ring-offset-[#050d18]"
- : ""
- }`}
- style={{
- left: pos.x,
- top: pos.y,
- width: NODE_WIDTH,
- height: NODE_HEIGHT,
- transform: isHovered && !isDragging ? "scale(1.02)" : "scale(1)",
- cursor: isDragging ? "grabbing" : canEdit ? "grab" : "pointer",
- }}
- >
- {/* Drag handle (entire node) */}
- <div
- className={`w-full h-full rounded-lg border-2 overflow-hidden ${
- isCheckpoint ? "bg-[#0f0a1e]" : "bg-[#0a1628]"
- }`}
- style={{
- borderColor: isSelected
- ? isCheckpoint
- ? "#a78bfa"
- : "#75aafc"
- : colors.border,
- borderStyle: node.isInstantiated ? "solid" : "dashed",
- }}
- onMouseDown={(e) => canEdit && handleDragStart(e, node.id)}
- >
- {/* Status indicator bar */}
- <div
- className="h-1.5"
- style={{ backgroundColor: colors.bg }}
- />
- {/* Content */}
- <div className="p-2">
- <div className="flex items-center justify-between mb-1">
- <span className="font-mono text-xs text-[#dbe7ff] truncate flex-1">
- {node.name}
- </span>
- {isCheckpoint ? (
- <CheckIcon className="w-4 h-4 text-[#a78bfa] opacity-70 flex-shrink-0 ml-1" />
- ) : (
- <ChainIcon className="w-4 h-4 text-[#75aafc] opacity-50 flex-shrink-0 ml-1" />
- )}
- </div>
- <div className="flex items-center justify-between">
- <span
- className="font-mono text-[10px] uppercase px-1.5 py-0.5 rounded"
- style={{
- color: colors.bg,
- backgroundColor: `${colors.bg}20`,
- }}
- >
- {node.isInstantiated ? status : isCheckpoint ? "checkpoint" : "definition"}
- </span>
- <span className="font-mono text-[10px] text-[#8b949e]">
- {node.contractType}
- </span>
- </div>
- </div>
- </div>
- {/* Edge drawing handle (connector dot at bottom) */}
- {canEdit && (isHovered || isSelected) && !isDragging && (
- <div
- className="absolute left-1/2 -translate-x-1/2 -bottom-2 w-4 h-4 rounded-full bg-[#f59e0b] border-2 border-[#0a1628] cursor-crosshair hover:scale-125 transition-transform"
- onMouseDown={(e) => handleEdgeDrawStart(e, node.id)}
- title="Drag to create dependency"
- />
- )}
- </div>
- );
- })
- : graph?.nodes.map((node) => {
- const pos = nodePositions.get(node.contractId);
- if (!pos) return null;
-
- const colors = getStatusColor(node.status);
- const isSelected = selectedNode === node.contractId;
- const isHovered = hoveredNode === node.contractId;
-
- return (
- <div
- key={node.contractId}
- onClick={() => handleNodeClick(node.contractId)}
- onDoubleClick={() => handleNodeDoubleClick(node.contractId)}
- onMouseEnter={() => setHoveredNode(node.contractId)}
- onMouseLeave={() => setHoveredNode(null)}
- className={`absolute cursor-pointer transition-all duration-150 ${
- isSelected
- ? "ring-2 ring-[#75aafc] ring-offset-2 ring-offset-[#050d18]"
- : ""
- }`}
- style={{
- left: pos.x,
- top: pos.y,
- width: NODE_WIDTH,
- height: NODE_HEIGHT,
- transform: isHovered ? "scale(1.02)" : "scale(1)",
- }}
- >
- <div
- className="w-full h-full rounded-lg border-2 bg-[#0a1628] overflow-hidden"
- style={{
- borderColor: isSelected ? "#75aafc" : colors.border,
- }}
- >
- {/* Status indicator bar */}
- <div
- className="h-1.5"
- style={{ backgroundColor: colors.bg }}
- />
- {/* Content */}
- <div className="p-2">
- <div className="flex items-center justify-between mb-1">
- <span className="font-mono text-xs text-[#dbe7ff] truncate flex-1">
- {node.name}
- </span>
- <ChainIcon className="w-4 h-4 text-[#75aafc] opacity-50 flex-shrink-0 ml-1" />
- </div>
- <div className="flex items-center justify-between">
- <span
- className="font-mono text-[10px] uppercase px-1.5 py-0.5 rounded"
- style={{
- color: colors.bg,
- backgroundColor: `${colors.bg}20`,
- }}
- >
- {node.status}
- </span>
- {node.phase && (
- <span className="font-mono text-[10px] text-[#8b949e]">
- {node.phase}
- </span>
- )}
- </div>
- </div>
- </div>
- </div>
- );
- })}
- </div>
+ <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>
@@ -871,17 +655,9 @@ export function ChainEditor({
{selectedDefinition && (
<DefinitionDetailPanel
definition={selectedDefinition}
- onClose={() => setSelectedNode(null)}
+ onClose={() => setSelectedNodeId(null)}
onDelete={handleDeleteDefinition}
- />
- )}
- {selectedContract && (
- <ContractDetailPanel
- contract={selectedContract}
- allContracts={chain.contracts}
- onClose={() => setSelectedNode(null)}
- onSelectContract={setSelectedNode}
- onOpenContract={onContractClick}
+ onRemoveDependency={handleRemoveDependency}
/>
)}
</div>
@@ -892,18 +668,18 @@ export function ChainEditor({
{showDefinitions ? (
<>
<span>{definitions.length} definitions</span>
- {chain.status === "pending" && (
+ {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="text-[#556677]">|</span>
- <span className="text-[#556677]">Right-click for options</span>
+ <span className="text-[#556677]">
+ Drag from <span className="text-[#f59e0b]">●</span> to link
+ </span>
</>
)}
<span className="flex-1" />
- {chain.status === "pending" && (
+ {canEdit && (
<button
onClick={() => setShowAddDefinition(true)}
className="text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors"
@@ -916,7 +692,8 @@ export function ChainEditor({
<>
<span>{chain.contracts.length} contracts</span>
<span>
- {chain.contracts.filter((c) => c.contractStatus === "completed").length} completed
+ {chain.contracts.filter((c) => c.contractStatus === "completed").length}{" "}
+ completed
</span>
<span>
{chain.contracts.filter((c) => c.contractStatus === "active").length} active
@@ -936,202 +713,36 @@ export function ChainEditor({
onCancel={() => setShowAddDefinition(false)}
/>
)}
-
- {/* Context Menu */}
- {contextMenu && (
- <ContextMenu
- x={contextMenu.x}
- y={contextMenu.y}
- nodeId={contextMenu.nodeId}
- canvasPosition={contextMenu.canvasPosition}
- definition={contextMenu.nodeId ? definitions.find((d) => d.id === contextMenu.nodeId) : undefined}
- allDefinitions={definitions}
- onClose={closeContextMenu}
- onDelete={handleDeleteDefinition}
- onRemoveDependency={handleRemoveDependency}
- onCreateAtPosition={handleCreateAtPosition}
- />
- )}
- </div>
- );
-}
-
-// Context Menu Component
-interface ContextMenuProps {
- x: number;
- y: number;
- nodeId: string | null;
- canvasPosition?: { gridX: number; gridY: number };
- definition?: ChainContractDefinition;
- allDefinitions: ChainContractDefinition[];
- onClose: () => void;
- onDelete: (id: string) => void;
- onRemoveDependency: (nodeId: string, depName: string) => void;
- onCreateAtPosition: (gridX: number, gridY: number) => void;
-}
-
-function ContextMenu({
- x,
- y,
- nodeId,
- canvasPosition,
- definition,
- allDefinitions: _allDefinitions,
- onClose,
- onDelete,
- onRemoveDependency,
- onCreateAtPosition,
-}: ContextMenuProps) {
- const menuRef = useRef<HTMLDivElement>(null);
-
- // Close on click outside
- useEffect(() => {
- const handleClickOutside = (e: MouseEvent) => {
- if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
- onClose();
- }
- };
- document.addEventListener("mousedown", handleClickOutside);
- return () => document.removeEventListener("mousedown", handleClickOutside);
- }, [onClose]);
-
- // Close on escape
- useEffect(() => {
- const handleKeyDown = (e: KeyboardEvent) => {
- if (e.key === "Escape") onClose();
- };
- document.addEventListener("keydown", handleKeyDown);
- return () => document.removeEventListener("keydown", handleKeyDown);
- }, [onClose]);
-
- // Canvas context menu (no node selected)
- if (!nodeId && canvasPosition) {
- return (
- <div
- ref={menuRef}
- className="fixed z-50 min-w-40 bg-[#0a1628] border border-[rgba(117,170,252,0.3)] shadow-xl rounded-lg overflow-hidden"
- style={{ left: x, top: y }}
- >
- <div className="py-1">
- <button
- onClick={() => onCreateAtPosition(canvasPosition.gridX, canvasPosition.gridY)}
- className="w-full px-3 py-2 text-left font-mono text-xs text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.1)] transition-colors flex items-center gap-2"
- >
- <PlusIcon className="w-3 h-3" />
- Create Definition Here
- </button>
- </div>
- </div>
- );
- }
-
- // Node context menu requires both nodeId and definition
- if (!nodeId || !definition) return null;
-
- const dependencies = definition.dependsOnNames || [];
-
- return (
- <div
- ref={menuRef}
- className="fixed z-50 min-w-48 bg-[#0a1628] border border-[rgba(117,170,252,0.3)] shadow-xl rounded-lg overflow-hidden"
- style={{ left: x, top: y }}
- >
- {/* Header */}
- <div className="px-3 py-2 border-b border-[rgba(117,170,252,0.2)] bg-[#0f1c32]">
- <span className="font-mono text-xs text-[#dbe7ff]">{definition.name}</span>
- </div>
-
- {/* Actions */}
- <div className="py-1">
- <button
- onClick={() => {
- onDelete(nodeId);
- onClose();
- }}
- className="w-full px-3 py-2 text-left font-mono text-xs text-red-400 hover:bg-red-400/10 transition-colors flex items-center gap-2"
- >
- <TrashIcon className="w-3 h-3" />
- Delete Definition
- </button>
- </div>
-
- {/* Dependencies section */}
- {dependencies.length > 0 && (
- <>
- <div className="border-t border-[rgba(117,170,252,0.2)]" />
- <div className="px-3 py-2">
- <span className="font-mono text-[10px] text-[#8b949e] uppercase">Dependencies</span>
- </div>
- <div className="pb-1">
- {dependencies.map((depName) => (
- <div
- key={depName}
- className="px-3 py-1.5 flex items-center justify-between hover:bg-[rgba(117,170,252,0.1)] group"
- >
- <span className="font-mono text-xs text-[#9bc3ff]">{depName}</span>
- <button
- onClick={() => onRemoveDependency(nodeId, depName)}
- className="font-mono text-[10px] text-red-400 opacity-0 group-hover:opacity-100 transition-opacity hover:underline"
- >
- Remove
- </button>
- </div>
- ))}
- </div>
- </>
- )}
-
- {/* Hint */}
- <div className="border-t border-[rgba(117,170,252,0.2)] px-3 py-2">
- <span className="font-mono text-[10px] text-[#556677]">
- Drag from connector to create dependency
- </span>
- </div>
</div>
);
}
-// Trash Icon
-function TrashIcon({ className }: { className?: string }) {
- return (
- <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
- <path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
- </svg>
- );
-}
-
-// Plus Icon
-function PlusIcon({ className }: { className?: string }) {
- return (
- <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
- <path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
- </svg>
- );
-}
-
+// 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>
+ <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>
@@ -1151,65 +762,45 @@ function DefinitionDetailPanel({
<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>
+ <p className="font-mono text-xs text-[#9bc3ff]">{definition.description}</p>
</div>
)}
- {/* Contract Type */}
+ {/* Type */}
<div>
<label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1">
- Contract Type
+ 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>
+ <p className="font-mono text-xs text-[#9bc3ff]">{definition.contractType}</p>
</div>
{/* Dependencies */}
- {definition.dependsOnNames && definition.dependsOnNames.length > 0 && (
+ {dependencies.length > 0 && (
<div>
<label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1">
- Depends On
+ Dependencies
</label>
<div className="space-y-1">
- {definition.dependsOnNames.map((depName) => (
- <span
- key={depName}
- className="block font-mono text-xs text-[#9bc3ff]"
+ {dependencies.map((dep) => (
+ <div
+ key={dep}
+ className="flex items-center justify-between bg-[rgba(117,170,252,0.1)] px-2 py-1 rounded"
>
- {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}
+ <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>
)}
- {/* Actions */}
- <div className="pt-2 border-t border-[rgba(117,170,252,0.2)]">
+ {/* 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"
@@ -1222,150 +813,19 @@ function DefinitionDetailPanel({
);
}
-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>
- );
-}
-
+// Add Definition Modal
interface AddDefinitionModalProps {
existingNames: string[];
onSubmit: (req: AddContractDefinitionRequest) => void;
onCancel: () => void;
}
-function AddDefinitionModal({
- existingNames,
- onSubmit,
- onCancel,
-}: AddDefinitionModalProps) {
+function AddDefinitionModal({ existingNames, onSubmit, onCancel }: AddDefinitionModalProps) {
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [contractType, setContractType] = useState("simple");
const [initialPhase, setInitialPhase] = useState("plan");
const [dependsOn, setDependsOn] = useState<string[]>([]);
- // Checkpoint validation options
- const [checkDeliverables, setCheckDeliverables] = useState(true);
- const [runTests, setRunTests] = useState(false);
- const [checkContent, setCheckContent] = useState("");
- const [onFailure, setOnFailure] = useState<"block" | "retry" | "warn">("block");
const isCheckpoint = contractType === "checkpoint";
@@ -1375,18 +835,9 @@ function AddDefinitionModal({
name: name.trim(),
description: description.trim() || undefined,
contractType,
- initialPhase: isCheckpoint ? "execute" : initialPhase, // Checkpoints always start in execute
+ initialPhase: isCheckpoint ? "execute" : initialPhase,
dependsOn: dependsOn.length > 0 ? dependsOn : undefined,
};
- // Add validation config for checkpoint contracts
- if (isCheckpoint) {
- req.validation = {
- checkDeliverables,
- runTests,
- checkContent: checkContent.trim() || undefined,
- onFailure,
- };
- }
onSubmit(req);
};
@@ -1406,202 +857,131 @@ function AddDefinitionModal({
<div className="space-y-4">
{/* Name */}
<div>
- <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1">
- Name
+ <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"
- 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 className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1">
+ Description
</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"
+ 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>
- {/* Contract Type */}
+ {/* Type */}
<div>
- <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1">
+ <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-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]"
+ 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="specification">Specification</option>
- <option value="execute">Execute</option>
- <option value="checkpoint">Checkpoint (Validation)</option>
+ <option value="checkpoint">Checkpoint (validation)</option>
</select>
- {isCheckpoint && (
- <p className="mt-1 font-mono text-[10px] text-[#a78bfa]">
- Checkpoint contracts validate outputs before allowing downstream contracts to proceed.
- </p>
- )}
</div>
- {/* Initial Phase - hidden for checkpoints */}
+ {/* Initial Phase (not for checkpoints) */}
{!isCheckpoint && (
<div>
- <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1">
+ <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-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]"
+ 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>
- <option value="review">Review</option>
</select>
</div>
)}
- {/* Checkpoint Validation Options */}
- {isCheckpoint && (
- <div className="p-3 bg-[#0f0a1e] border border-[#6366f1]/30 space-y-3">
- <h4 className="font-mono text-xs text-[#a78bfa] uppercase">Validation Options</h4>
-
- <label className="flex items-center gap-2 cursor-pointer">
- <input
- type="checkbox"
- checked={checkDeliverables}
- onChange={(e) => setCheckDeliverables(e.target.checked)}
- className="accent-[#a78bfa]"
- />
- <span className="font-mono text-xs text-[#dbe7ff]">Check required deliverables exist</span>
- </label>
-
- <label className="flex items-center gap-2 cursor-pointer">
- <input
- type="checkbox"
- checked={runTests}
- onChange={(e) => setRunTests(e.target.checked)}
- className="accent-[#a78bfa]"
- />
- <span className="font-mono text-xs text-[#dbe7ff]">Run test suite</span>
- </label>
-
- <div>
- <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1">
- Custom Validation Instructions (optional)
- </label>
- <textarea
- value={checkContent}
- onChange={(e) => setCheckContent(e.target.value)}
- placeholder="Additional criteria for Claude to check..."
- rows={3}
- className="w-full px-2 py-1.5 bg-[#0d1b2d] border border-[#6366f1]/50 text-[#dbe7ff] font-mono text-xs focus:outline-none focus:border-[#a78bfa] resize-none"
- />
- </div>
-
- <div>
- <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1">
- On Failure
- </label>
- <select
- value={onFailure}
- onChange={(e) => setOnFailure(e.target.value as "block" | "retry" | "warn")}
- className="w-full px-2 py-1.5 bg-[#0d1b2d] border border-[#6366f1]/50 text-[#dbe7ff] font-mono text-xs focus:outline-none focus:border-[#a78bfa]"
- >
- <option value="block">Block - Stop chain until fixed</option>
- <option value="retry">Retry - Retry upstream contracts</option>
- <option value="warn">Warn - Log warning but continue</option>
- </select>
- </div>
- </div>
- )}
-
{/* Dependencies */}
{existingNames.length > 0 && (
<div>
- <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1">
+ <label className="block font-mono text-[10px] text-[#8b949e] uppercase mb-1">
Depends On
</label>
- <div className="space-y-1 max-h-32 overflow-y-auto">
+ <div className="flex flex-wrap gap-2">
{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>
+ <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 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>
+ {/* 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}
- 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 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}
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- strokeWidth="2"
- strokeLinecap="round"
- strokeLinejoin="round"
- >
- <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
- <polyline points="22 4 12 14.01 9 11.01" />
+ <svg 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>
);
}