summaryrefslogtreecommitdiff
path: root/makima/frontend
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-08 16:18:13 +0000
committersoryu <soryu@soryu.co>2026-02-08 16:18:13 +0000
commitc0f220582cd61f0d88e42dfbc29d55b3be1e3b19 (patch)
tree5105fe5ffb6e80d3e82f806411628b0587c0685c /makima/frontend
parent62d411f61893486680ded5921a8b86b483ee1144 (diff)
downloadsoryu-c0f220582cd61f0d88e42dfbc29d55b3be1e3b19.tar.gz
soryu-c0f220582cd61f0d88e42dfbc29d55b3be1e3b19.zip
Fix directive deletion and stop local only on contracts
Diffstat (limited to 'makima/frontend')
-rw-r--r--makima/frontend/src/components/directives/DirectiveDetail.tsx188
-rw-r--r--makima/frontend/src/components/directives/StepDiagram.tsx281
2 files changed, 237 insertions, 232 deletions
diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx
index 06d8ba2..6bdf5aa 100644
--- a/makima/frontend/src/components/directives/DirectiveDetail.tsx
+++ b/makima/frontend/src/components/directives/DirectiveDetail.tsx
@@ -3,8 +3,6 @@ import { useNavigate } from "react-router";
import type {
DirectiveWithChains,
DirectiveStatus,
- ChainWithSteps,
- ChainStep,
ContractPhase,
} from "../../lib/api";
import { getDirective } from "../../lib/api";
@@ -32,122 +30,6 @@ const statusColors: Record<DirectiveStatus, string> = {
failed: "text-red-400",
};
-const stepStatusColors: Record<string, string> = {
- pending: "text-[#888]",
- running: "text-yellow-400",
- passed: "text-green-400",
- failed: "text-red-400",
-};
-
-const stepStatusIcons: Record<string, string> = {
- pending: "\u25CB", // ○
- running: "\u25D4", // ◔
- passed: "\u25CF", // ●
- failed: "\u2715", // ✕
-};
-
-function StepRow({ step }: { step: ChainStep }) {
- const navigate = useNavigate();
- const color = stepStatusColors[step.status] || "text-[#888]";
- const icon = stepStatusIcons[step.status] || "\u25CB";
- const summary = step.contractSummary;
-
- return (
- <div className="flex items-start gap-2 py-1.5 px-2 hover:bg-[rgba(117,170,252,0.05)]">
- <span className={`font-mono text-[11px] ${color} mt-px`}>{icon}</span>
- <div className="flex-1 min-w-0">
- <div className="flex items-center gap-2">
- <span className="font-mono text-[11px] text-[#dbe7ff] truncate">
- {step.name}
- </span>
- <span className={`font-mono text-[9px] uppercase ${color}`}>
- {step.status}
- </span>
- </div>
- {summary && (
- <div className="flex items-center gap-2 mt-0.5">
- <PhaseProgressBarCompact
- currentPhase={summary.phase as ContractPhase}
- />
- <span className="font-mono text-[9px] text-[#7788aa]">
- {summary.tasksDone}/{summary.taskCount} tasks
- </span>
- {step.contractId && (
- <button
- onClick={(e) => {
- e.stopPropagation();
- navigate(`/contracts/${step.contractId}`);
- }}
- className="font-mono text-[9px] text-[#75aafc] hover:text-white transition-colors"
- >
- contract &rarr;
- </button>
- )}
- </div>
- )}
- {!summary && step.contractId && (
- <button
- onClick={() => navigate(`/contracts/${step.contractId}`)}
- className="font-mono text-[9px] text-[#75aafc] hover:text-white transition-colors mt-0.5"
- >
- contract &rarr;
- </button>
- )}
- {step.description && (
- <p className="font-mono text-[10px] text-[#7788aa] truncate mt-0.5">
- {step.description}
- </p>
- )}
- </div>
- </div>
- );
-}
-
-function ChainCard({ chainWithSteps }: { chainWithSteps: ChainWithSteps }) {
- const chain = chainWithSteps;
- const steps = chainWithSteps.steps || [];
-
- return (
- <div className="border border-dashed border-[rgba(117,170,252,0.25)] bg-[rgba(117,170,252,0.03)]">
- <div className="p-3">
- <div className="flex items-center justify-between mb-1">
- <span className="font-mono text-xs text-[#dbe7ff]">
- {chain.name}
- </span>
- <span className="font-mono text-[10px] text-[#7788aa] uppercase">
- gen {chain.generation} &middot; {chain.status}
- </span>
- </div>
- {chain.description && (
- <p className="font-mono text-[11px] text-[#7788aa] mb-1">
- {chain.description}
- </p>
- )}
- <div className="flex gap-3 font-mono text-[10px] text-[#7788aa]">
- <span>
- {chain.completedSteps}/{chain.totalSteps} steps
- </span>
- {chain.failedSteps > 0 && (
- <span className="text-red-400">{chain.failedSteps} failed</span>
- )}
- {chain.currentConfidence != null && (
- <span>
- confidence: {(chain.currentConfidence * 100).toFixed(0)}%
- </span>
- )}
- </div>
- </div>
- {steps.length > 0 && (
- <div className="border-t border-dashed border-[rgba(117,170,252,0.15)]">
- {steps.map((step) => (
- <StepRow key={step.id} step={step} />
- ))}
- </div>
- )}
- </div>
- );
-}
-
function JsonSection({
label,
data,
@@ -484,39 +366,53 @@ export function DirectiveDetail({
{activeTab === "chain" && (
<div className="space-y-4">
- {/* Step diagram */}
- {directive.chains.length > 0 && (
- <div>
- <h4 className="font-mono text-[10px] text-[#75aafc] uppercase tracking-wider mb-2">
- Step Dependencies
- </h4>
- <StepDiagram
- steps={directive.chains.flatMap((c) => c.steps)}
- />
- </div>
- )}
-
- {/* Chain cards */}
- <div>
- <h4 className="font-mono text-[10px] text-[#75aafc] uppercase tracking-wider mb-2">
- Chains ({directive.chains.length})
- </h4>
- {directive.chains.length === 0 ? (
- <p className="font-mono text-xs text-[#7788aa]">
+ {directive.chains.length === 0 ? (
+ <div className="flex flex-col items-center justify-center py-12">
+ <div className="font-mono text-[40px] text-[#333] mb-3">
+ {directive.status === "planning" ? "\u2699" : "\u25CB"}
+ </div>
+ <p className="font-mono text-xs text-[#7788aa] text-center max-w-[300px]">
{directive.status === "planning"
- ? "Planning in progress... chains will appear when the planner completes."
+ ? "Planning in progress... the chain will appear when the planner submits a plan."
: directive.status === "draft"
? "No chains yet. Start the directive to begin planning."
: "No chains created for this directive."}
</p>
- ) : (
- <div className="space-y-2">
- {directive.chains.map((cws) => (
- <ChainCard key={cws.id} chainWithSteps={cws} />
- ))}
- </div>
- )}
- </div>
+ </div>
+ ) : (
+ <>
+ {/* Chain metadata header */}
+ {directive.chains.map((chain) => (
+ <div key={chain.id} className="space-y-3">
+ <div className="flex items-center gap-3 pb-2 border-b border-dashed border-[rgba(117,170,252,0.15)]">
+ <span className="font-mono text-xs text-[#dbe7ff]">
+ {chain.name}
+ </span>
+ <span className="font-mono text-[10px] text-[#7788aa] uppercase">
+ gen {chain.generation}
+ </span>
+ <span className={`font-mono text-[10px] uppercase ${
+ chain.status === "completed" ? "text-green-400" :
+ chain.status === "failed" ? "text-red-400" :
+ chain.status === "running" ? "text-yellow-400" :
+ "text-[#7788aa]"
+ }`}>
+ {chain.status}
+ </span>
+ <span className="font-mono text-[10px] text-[#7788aa] ml-auto">
+ {chain.completedSteps}/{chain.totalSteps} steps
+ {chain.failedSteps > 0 && (
+ <span className="text-red-400 ml-1">
+ ({chain.failedSteps} failed)
+ </span>
+ )}
+ </span>
+ </div>
+ <StepDiagram steps={chain.steps} />
+ </div>
+ ))}
+ </>
+ )}
</div>
)}
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 &rarr;
+ </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">&darr;</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">
- &rarr;
- </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" : ""} &middot; {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>
);
}