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,
ChainContractDefinition,
ChainDefinitionGraphResponse,
AddContractDefinitionRequest,
} from "../../lib/api";
import {
listChainDefinitions,
createChainDefinition,
updateChainDefinition,
deleteChainDefinition,
getChainDefinitionGraph,
startChain,
stopChain,
} from "../../lib/api";
const statusColors: Record<string, string> = {
active: "text-green-400",
completed: "text-blue-400",
archived: "text-[#555]",
pending: "text-yellow-400",
};
interface ChainEditorProps {
chain: ChainWithContracts;
graph: ChainGraphResponse | null;
loading: boolean;
onBack: () => void;
onRefresh: () => void;
onContractClick: (contractId: string) => void;
}
// Node dimensions 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 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,
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 [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(() => {
async function loadDefinitions() {
try {
const [defs, defGraph] = await Promise.all([
listChainDefinitions(chain.id),
getChainDefinitionGraph(chain.id),
]);
setDefinitions(defs);
setDefinitionGraph(defGraph);
} catch (err) {
console.error("Failed to load definitions:", err);
}
}
loadDefinitions();
}, [chain.id]);
// 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]
);
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>
{/* 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)}
/>
)}
</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>
);
}