import { useNavigate } from "react-router"; import type { ChainStep, ContractPhase } from "../../lib/api"; import { PhaseProgressBarCompact } from "../contracts/PhaseProgressBar"; interface StepDiagramProps { steps: ChainStep[]; } const statusColors: Record = { pending: { border: "border-[#444]", dot: "bg-[#555]", bg: "bg-[rgba(40,40,50,0.6)]", glow: "", }, running: { border: "border-yellow-400/60", dot: "bg-yellow-400", bg: "bg-[rgba(80,70,20,0.3)]", glow: "shadow-[0_0_8px_rgba(250,204,21,0.15)]", }, evaluating: { border: "border-blue-400/60", dot: "bg-blue-400", bg: "bg-[rgba(20,50,80,0.3)]", glow: "shadow-[0_0_8px_rgba(96,165,250,0.15)]", }, passed: { border: "border-green-400/60", dot: "bg-green-400", bg: "bg-[rgba(20,60,30,0.3)]", glow: "", }, failed: { border: "border-red-400/60", dot: "bg-red-400", bg: "bg-[rgba(60,20,20,0.3)]", glow: "", }, }; const statusLabels: Record = { pending: "Pending", running: "Running", evaluating: "Evaluating", passed: "Passed", failed: "Failed", }; /** * Assign depth to each step via topological sort based on dependsOn UUIDs. */ function assignDepths(steps: ChainStep[]): Map { const depths = new Map(); const stepMap = new Map(steps.map((s) => [s.id, s])); function getDepth(id: string): number { if (depths.has(id)) return depths.get(id)!; const step = stepMap.get(id); if (!step || !step.dependsOn || step.dependsOn.length === 0) { depths.set(id, 0); return 0; } const maxParent = Math.max( ...step.dependsOn.map((depId) => getDepth(depId)) ); const d = maxParent + 1; depths.set(id, d); return d; } for (const step of steps) { getDepth(step.id); } return depths; } function StepCard({ step }: { step: ChainStep }) { const navigate = useNavigate(); const colors = statusColors[step.status] || statusColors.pending; const summary = step.contractSummary; const hasContract = !!step.contractId; return (
{ if (hasContract) navigate(`/contracts/${step.contractId}`); }} title={hasContract ? "View contract" : undefined} > {/* Status header */}
{step.name}
{statusLabels[step.status] || step.status}
{/* Description */} {step.description && (

{step.description}

)} {/* Contract progress */} {summary && (
{summary.tasksDone}/{summary.taskCount} tasks {summary.tasksRunning > 0 && ( {summary.tasksRunning} running )} {summary.tasksFailed > 0 && ( {summary.tasksFailed} failed )}
)} {/* Contract link arrow */} {hasContract && !summary && (
view contract →
)}
); } /** Vertical connector between levels */ function LevelConnector({ count }: { count: number }) { return (
{Array.from({ length: count }).map((_, i) => (
))}
); } export function StepDiagram({ steps }: StepDiagramProps) { if (steps.length === 0) { return (

No steps to display.

); } const depths = assignDepths(steps); const maxDepth = Math.max(...Array.from(depths.values())); // Group steps by depth level const levels: ChainStep[][] = []; for (let d = 0; d <= maxDepth; d++) { levels.push( steps .filter((s) => depths.get(s.id) === d) .sort((a, b) => a.orderIndex - b.orderIndex) ); } // Compute overall progress const passedCount = steps.filter(s => s.status === "passed").length; const failedCount = steps.filter(s => s.status === "failed").length; const runningCount = steps.filter(s => s.status === "running" || s.status === "evaluating").length; return (
{/* Progress summary */}
{levels.length} level{levels.length !== 1 ? "s" : ""} · {steps.length} step{steps.length !== 1 ? "s" : ""} {passedCount > 0 && ( {passedCount} passed )} {runningCount > 0 && ( {runningCount} active )} {failedCount > 0 && ( {failedCount} failed )}
{Math.round((passedCount / steps.length) * 100)}%
{/* Chain flow */}
{levels.map((level, li) => (
{/* Level label */} {levels.length > 1 && (
{li === 0 ? "Start" : li === maxDepth ? "Final" : `Level ${li}`}
)} {/* Steps at this level */}
{level.map((step) => ( ))}
{/* Connector to next level */} {li < maxDepth && ( )}
))}
); }