diff options
Diffstat (limited to 'makima/frontend/src/components/directives/StepDiagram.tsx')
| -rw-r--r-- | makima/frontend/src/components/directives/StepDiagram.tsx | 281 |
1 files changed, 195 insertions, 86 deletions
diff --git a/makima/frontend/src/components/directives/StepDiagram.tsx b/makima/frontend/src/components/directives/StepDiagram.tsx index 5c65ae1..91a3438 100644 --- a/makima/frontend/src/components/directives/StepDiagram.tsx +++ b/makima/frontend/src/components/directives/StepDiagram.tsx @@ -6,23 +6,49 @@ interface StepDiagramProps { steps: ChainStep[]; } -const statusBorderColors: Record<string, string> = { - pending: "border-[#555]", - running: "border-yellow-400", - passed: "border-green-400", - failed: "border-red-400", +const statusColors: Record<string, { border: string; dot: string; bg: string; glow: string }> = { + 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 statusDotColors: Record<string, string> = { - pending: "bg-[#555]", - running: "bg-yellow-400", - passed: "bg-green-400", - failed: "bg-red-400", +const statusLabels: Record<string, string> = { + pending: "Pending", + running: "Running", + evaluating: "Evaluating", + passed: "Passed", + failed: "Failed", }; /** - * Assign depth to each step via topological sort. - * Steps with no dependsOn = depth 0. Steps depending only on depth-0 = depth 1. Etc. + * Assign depth to each step via topological sort based on dependsOn UUIDs. */ function assignDepths(steps: ChainStep[]): Map<string, number> { const depths = new Map<string, number>(); @@ -50,9 +76,108 @@ function assignDepths(steps: ChainStep[]): Map<string, number> { return depths; } -export function StepDiagram({ steps }: StepDiagramProps) { +function StepCard({ step }: { step: ChainStep }) { const navigate = useNavigate(); + const colors = statusColors[step.status] || statusColors.pending; + const summary = step.contractSummary; + const hasContract = !!step.contractId; + + return ( + <div + className={` + border ${colors.border} ${colors.bg} ${colors.glow} + p-3 w-[220px] transition-all duration-200 + ${hasContract ? "cursor-pointer hover:brightness-125" : ""} + `} + onClick={() => { + if (hasContract) navigate(`/contracts/${step.contractId}`); + }} + title={hasContract ? "View contract" : undefined} + > + {/* Status header */} + <div className="flex items-center justify-between mb-2"> + <div className="flex items-center gap-2 min-w-0 flex-1"> + <div className={`w-2 h-2 rounded-full ${colors.dot} shrink-0 ${ + step.status === "running" ? "animate-pulse" : "" + }`} /> + <span className="font-mono text-[11px] text-[#dbe7ff] truncate font-medium"> + {step.name} + </span> + </div> + <span className={`font-mono text-[9px] uppercase tracking-wider shrink-0 ml-2 ${ + step.status === "passed" ? "text-green-400" : + step.status === "failed" ? "text-red-400" : + step.status === "running" ? "text-yellow-400" : + step.status === "evaluating" ? "text-blue-400" : + "text-[#555]" + }`}> + {statusLabels[step.status] || step.status} + </span> + </div> + + {/* Description */} + {step.description && ( + <p className="font-mono text-[10px] text-[#7788aa] mb-2 line-clamp-2 leading-relaxed"> + {step.description} + </p> + )} + + {/* Contract progress */} + {summary && ( + <div className="border-t border-[rgba(117,170,252,0.1)] pt-2 mt-1"> + <div className="mb-1.5"> + <PhaseProgressBarCompact + currentPhase={summary.phase as ContractPhase} + /> + </div> + <div className="flex items-center gap-3 font-mono text-[9px]"> + <span className="text-[#7788aa]"> + {summary.tasksDone}/{summary.taskCount} tasks + </span> + {summary.tasksRunning > 0 && ( + <span className="text-yellow-400"> + {summary.tasksRunning} running + </span> + )} + {summary.tasksFailed > 0 && ( + <span className="text-red-400"> + {summary.tasksFailed} failed + </span> + )} + </div> + </div> + )} + {/* Contract link arrow */} + {hasContract && !summary && ( + <div className="border-t border-[rgba(117,170,252,0.1)] pt-1.5 mt-1"> + <span className="font-mono text-[9px] text-[#75aafc]"> + view contract → + </span> + </div> + )} + </div> + ); +} + +/** Vertical connector between levels */ +function LevelConnector({ count }: { count: number }) { + return ( + <div className="flex justify-center py-1"> + <div className="flex items-center gap-3"> + {Array.from({ length: count }).map((_, i) => ( + <div key={i} className="flex flex-col items-center"> + <div className="w-px h-4 bg-[rgba(117,170,252,0.25)]" /> + <div className="text-[rgba(117,170,252,0.4)] text-[10px] leading-none">↓</div> + <div className="w-px h-4 bg-[rgba(117,170,252,0.25)]" /> + </div> + ))} + </div> + </div> + ); +} + +export function StepDiagram({ steps }: StepDiagramProps) { if (steps.length === 0) { return ( <p className="font-mono text-xs text-[#7788aa]">No steps to display.</p> @@ -62,7 +187,7 @@ export function StepDiagram({ steps }: StepDiagramProps) { const depths = assignDepths(steps); const maxDepth = Math.max(...Array.from(depths.values())); - // Group steps by depth + // Group steps by depth level const levels: ChainStep[][] = []; for (let d = 0; d <= maxDepth; d++) { levels.push( @@ -72,81 +197,65 @@ export function StepDiagram({ steps }: StepDiagramProps) { ); } - // Build position map for connectors - const stepPositions = new Map<string, { level: number; index: number }>(); - levels.forEach((level, li) => { - level.forEach((step, si) => { - stepPositions.set(step.id, { level: li, index: si }); - }); - }); + // 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 ( - <div className="space-y-3"> - {levels.map((level, li) => ( - <div key={li} className="flex items-start gap-2 flex-wrap"> - {li > 0 && ( - <div className="w-full flex justify-center mb-1"> - <div className="w-px h-3 bg-[rgba(117,170,252,0.3)]" /> - </div> - )} - {level.map((step) => { - const borderColor = - statusBorderColors[step.status] || "border-[#555]"; - const dotColor = statusDotColors[step.status] || "bg-[#555]"; - const summary = step.contractSummary; - const hasContract = !!step.contractId; - - return ( - <div - key={step.id} - className={` - border ${borderColor} bg-[rgba(0,0,0,0.2)] p-2 min-w-[180px] max-w-[220px] - ${hasContract ? "cursor-pointer hover:bg-[rgba(117,170,252,0.05)]" : ""} - transition-colors - `} - onClick={() => { - if (hasContract) navigate(`/contracts/${step.contractId}`); - }} - title={hasContract ? "View contract" : undefined} - > - <div className="flex items-center gap-1.5 mb-1"> - <div className={`w-1.5 h-1.5 rounded-full ${dotColor}`} /> - <span className="font-mono text-[11px] text-[#dbe7ff] truncate flex-1"> - {step.name} - </span> - {hasContract && ( - <span className="font-mono text-[9px] text-[#75aafc] shrink-0"> - → - </span> - )} - </div> - {summary && ( - <> - <div className="mb-1"> - <PhaseProgressBarCompact - currentPhase={summary.phase as ContractPhase} - /> - </div> - <div className="font-mono text-[9px] text-[#7788aa]"> - {summary.tasksDone}/{summary.taskCount} tasks - {summary.tasksRunning > 0 && ( - <span className="text-yellow-400 ml-1"> - {summary.tasksRunning} running - </span> - )} - {summary.tasksFailed > 0 && ( - <span className="text-red-400 ml-1"> - {summary.tasksFailed} failed - </span> - )} - </div> - </> - )} - </div> - ); - })} + <div> + {/* Progress summary */} + <div className="flex items-center gap-4 mb-4 font-mono text-[10px]"> + <span className="text-[#7788aa]"> + {levels.length} level{levels.length !== 1 ? "s" : ""} · {steps.length} step{steps.length !== 1 ? "s" : ""} + </span> + {passedCount > 0 && ( + <span className="text-green-400">{passedCount} passed</span> + )} + {runningCount > 0 && ( + <span className="text-yellow-400">{runningCount} active</span> + )} + {failedCount > 0 && ( + <span className="text-red-400">{failedCount} failed</span> + )} + <div className="flex-1 h-1 bg-[rgba(117,170,252,0.1)] rounded-full overflow-hidden"> + <div + className="h-full bg-green-400/60 rounded-full transition-all duration-500" + style={{ width: `${(passedCount / steps.length) * 100}%` }} + /> </div> - ))} + <span className="text-[#7788aa]"> + {Math.round((passedCount / steps.length) * 100)}% + </span> + </div> + + {/* Chain flow */} + <div className="flex flex-col items-center"> + {levels.map((level, li) => ( + <div key={li}> + {/* Level label */} + {levels.length > 1 && ( + <div className="flex justify-center mb-2"> + <span className="font-mono text-[9px] text-[#555] uppercase tracking-widest px-2 py-0.5 border border-[rgba(117,170,252,0.1)] bg-[rgba(0,0,0,0.3)]"> + {li === 0 ? "Start" : li === maxDepth ? "Final" : `Level ${li}`} + </span> + </div> + )} + + {/* Steps at this level */} + <div className="flex items-start justify-center gap-3 flex-wrap"> + {level.map((step) => ( + <StepCard key={step.id} step={step} /> + ))} + </div> + + {/* Connector to next level */} + {li < maxDepth && ( + <LevelConnector count={Math.min(level.length, 3)} /> + )} + </div> + ))} + </div> </div> ); } |
