summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/directives/ChainTab.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/components/directives/ChainTab.tsx')
-rw-r--r--makima/frontend/src/components/directives/ChainTab.tsx212
1 files changed, 212 insertions, 0 deletions
diff --git a/makima/frontend/src/components/directives/ChainTab.tsx b/makima/frontend/src/components/directives/ChainTab.tsx
new file mode 100644
index 0000000..ccefe81
--- /dev/null
+++ b/makima/frontend/src/components/directives/ChainTab.tsx
@@ -0,0 +1,212 @@
+import { useState, useMemo } from "react";
+import {
+ ReactFlow,
+ Edge,
+ Controls,
+ Background,
+ BackgroundVariant,
+ MarkerType,
+} from "@xyflow/react";
+import "@xyflow/react/dist/style.css";
+import type { DirectiveWithProgress, DirectiveGraphResponse } from "../../lib/api";
+import { StepNodeComponent, stepStatusStyles } from "./StepNode";
+
+// Node types for React Flow
+const nodeTypes = {
+ step: StepNodeComponent,
+};
+
+export function ChainTab({ directive, graph }: { directive: DirectiveWithProgress; graph: DirectiveGraphResponse | null }) {
+ const [viewMode, setViewMode] = useState<"dag" | "list">("dag");
+
+ // Convert graph to React Flow nodes and edges
+ const { nodes, edges } = useMemo(() => {
+ if (!graph || !graph.nodes.length) {
+ // Fallback: generate positions from directive.steps
+ const stepNodes = directive.steps.map((step, index) => ({
+ id: step.id,
+ type: "step" as const,
+ position: {
+ x: (index % 3) * 220 + 50,
+ y: Math.floor(index / 3) * 120 + 50,
+ },
+ data: {
+ id: step.id,
+ name: step.name,
+ stepType: step.stepType,
+ status: step.status,
+ confidenceScore: step.confidenceScore,
+ confidenceLevel: step.confidenceLevel,
+ contractId: step.contractId,
+ editorX: null,
+ editorY: null,
+ },
+ }));
+
+ // Build edges from dependencies
+ const stepEdges: Edge[] = [];
+ directive.steps.forEach((step) => {
+ (step.dependsOn ?? []).forEach((depName) => {
+ const depStep = directive.steps.find((s) => s.name === depName);
+ if (depStep) {
+ stepEdges.push({
+ id: `${depStep.id}-${step.id}`,
+ source: depStep.id,
+ target: step.id,
+ type: "smoothstep",
+ markerEnd: { type: MarkerType.ArrowClosed, color: "#556677" },
+ style: { stroke: "#556677", strokeWidth: 2 },
+ });
+ }
+ });
+ });
+
+ return { nodes: stepNodes, edges: stepEdges };
+ }
+
+ // Use graph data
+ const graphNodes = graph.nodes.map((node) => ({
+ id: node.id,
+ type: "step" as const,
+ position: {
+ x: node.editorX ?? 50,
+ y: node.editorY ?? 50,
+ },
+ data: { ...node },
+ }));
+
+ const graphEdges: Edge[] = graph.edges.map((edge) => ({
+ id: `${edge.source}-${edge.target}`,
+ source: edge.source,
+ target: edge.target,
+ type: "smoothstep",
+ markerEnd: { type: MarkerType.ArrowClosed, color: "#556677" },
+ style: { stroke: "#556677", strokeWidth: 2 },
+ }));
+
+ return { nodes: graphNodes, edges: graphEdges };
+ }, [graph, directive.steps]);
+
+ if (!directive.chain) {
+ return (
+ <div className="text-center py-8">
+ <p className="font-mono text-sm text-[#556677]">
+ No chain generated yet. Start the directive to generate a chain.
+ </p>
+ </div>
+ );
+ }
+
+ return (
+ <div className="space-y-4">
+ {/* Chain info header */}
+ <div className="flex items-center justify-between p-3 bg-[rgba(117,170,252,0.05)] border border-[rgba(117,170,252,0.15)]">
+ <div>
+ <div className="font-mono text-sm text-[#dbe7ff]">{directive.chain.name}</div>
+ <div className="font-mono text-[10px] text-[#556677]">Generation {directive.chain.generation}</div>
+ </div>
+ <div className="flex items-center gap-4">
+ <div className="font-mono text-xs text-[#556677]">
+ {directive.chain.completedSteps}/{directive.chain.totalSteps} steps
+ </div>
+ {/* View toggle */}
+ <div className="flex border border-[rgba(117,170,252,0.2)]">
+ <button
+ onClick={() => setViewMode("dag")}
+ className={`px-2 py-1 font-mono text-[10px] uppercase ${
+ viewMode === "dag"
+ ? "bg-[rgba(117,170,252,0.2)] text-[#75aafc]"
+ : "text-[#556677] hover:text-[#75aafc]"
+ }`}
+ >
+ DAG
+ </button>
+ <button
+ onClick={() => setViewMode("list")}
+ className={`px-2 py-1 font-mono text-[10px] uppercase ${
+ viewMode === "list"
+ ? "bg-[rgba(117,170,252,0.2)] text-[#75aafc]"
+ : "text-[#556677] hover:text-[#75aafc]"
+ }`}
+ >
+ List
+ </button>
+ </div>
+ </div>
+ </div>
+
+ {viewMode === "dag" ? (
+ /* DAG visualization */
+ <div className="h-[400px] border border-[rgba(117,170,252,0.1)] rounded bg-[#050d18]">
+ {directive.steps.length === 0 ? (
+ <div className="flex items-center justify-center h-full">
+ <p className="font-mono text-sm text-[#556677]">No steps in chain</p>
+ </div>
+ ) : (
+ <ReactFlow
+ nodes={nodes}
+ edges={edges}
+ nodeTypes={nodeTypes}
+ fitView
+ fitViewOptions={{ padding: 0.2 }}
+ minZoom={0.5}
+ maxZoom={1.5}
+ defaultEdgeOptions={{
+ type: "smoothstep",
+ style: { stroke: "#556677", strokeWidth: 2 },
+ }}
+ >
+ <Background variant={BackgroundVariant.Dots} gap={20} size={1} color="#1a2a3a" />
+ <Controls className="!bg-[#0a1628] !border-[rgba(117,170,252,0.2)]" />
+ </ReactFlow>
+ )}
+ </div>
+ ) : (
+ /* List view */
+ <div className="space-y-2">
+ <h3 className="font-mono text-xs text-[#75aafc] uppercase">Steps</h3>
+ {directive.steps.length === 0 ? (
+ <p className="font-mono text-sm text-[#556677]">No steps in chain</p>
+ ) : (
+ directive.steps.map((step) => {
+ const styles = stepStatusStyles[step.status] || stepStatusStyles.pending;
+
+ return (
+ <div
+ key={step.id}
+ className="p-3 bg-[rgba(117,170,252,0.02)] border border-[rgba(117,170,252,0.1)]"
+ >
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-2">
+ <span className="font-mono text-sm text-[#dbe7ff]">{step.name}</span>
+ <span
+ className="px-1.5 py-0.5 font-mono text-[10px] uppercase border"
+ style={{ color: styles.text, borderColor: `${styles.border}50` }}
+ >
+ {step.status}
+ </span>
+ {step.confidenceScore !== null && (
+ <span className="font-mono text-[10px] text-[#556677]">
+ ({Math.round(step.confidenceScore * 100)}%)
+ </span>
+ )}
+ </div>
+ <div className="font-mono text-[10px] text-[#556677]">{step.stepType}</div>
+ </div>
+ {step.description && (
+ <p className="font-mono text-xs text-[#556677] mt-1">{step.description}</p>
+ )}
+ {step.dependsOn?.length > 0 && (
+ <div className="font-mono text-[10px] text-[#556677] mt-1">
+ Depends on: {step.dependsOn.join(", ")}
+ </div>
+ )}
+ </div>
+ );
+ })
+ )}
+ </div>
+ )}
+ </div>
+ );
+}