diff options
Diffstat (limited to 'makima/frontend/src')
| -rw-r--r-- | makima/frontend/src/components/directives/DirectiveDetail.tsx | 180 | ||||
| -rw-r--r-- | makima/frontend/src/lib/api.ts | 25 | ||||
| -rw-r--r-- | makima/frontend/src/routes/directives.tsx | 8 |
3 files changed, 188 insertions, 25 deletions
diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx index e519b92..094cdf2 100644 --- a/makima/frontend/src/components/directives/DirectiveDetail.tsx +++ b/makima/frontend/src/components/directives/DirectiveDetail.tsx @@ -1,14 +1,19 @@ +import { useEffect, useRef } from "react"; +import { useNavigate } from "react-router"; import type { DirectiveWithChains, DirectiveStatus, - DirectiveChain, + ChainWithSteps, + ChainStep, } from "../../lib/api"; +import { getDirective } from "../../lib/api"; interface DirectiveDetailProps { directive: DirectiveWithChains; onBack: () => void; onDelete?: (id: string) => void; onStart?: (id: string) => void; + onRefresh?: (updated: DirectiveWithChains) => void; } const statusColors: Record<DirectiveStatus, string> = { @@ -21,31 +26,93 @@ const statusColors: Record<DirectiveStatus, string> = { failed: "text-red-400", }; -function ChainCard({ chain }: { chain: DirectiveChain }) { +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"; + return ( - <div className="p-3 border border-dashed border-[rgba(117,170,252,0.25)] bg-[rgba(117,170,252,0.03)]"> - <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} · {chain.status} - </span> + <div className="flex items-start gap-2 py-1 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> + {step.description && ( + <p className="font-mono text-[10px] text-[#7788aa] truncate"> + {step.description} + </p> + )} </div> - {chain.description && ( - <p className="font-mono text-[11px] text-[#7788aa] mb-1"> - {chain.description} - </p> + {step.contractId && ( + <button + onClick={() => navigate(`/contracts/${step.contractId}`)} + className="font-mono text-[9px] text-[#75aafc] hover:text-white transition-colors shrink-0" + title="View contract" + > + contract → + </button> )} - <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> + ); +} + +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} · {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> ); } @@ -81,7 +148,43 @@ export function DirectiveDetail({ onBack, onDelete, onStart, + onRefresh, }: DirectiveDetailProps) { + const navigate = useNavigate(); + + // Auto-poll when directive is in an active state + const isLive = + directive.status === "planning" || directive.status === "active"; + const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null); + + useEffect(() => { + if (!isLive) { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + return; + } + + intervalRef.current = setInterval(async () => { + try { + const updated = await getDirective(directive.id); + if (updated && onRefresh) { + onRefresh(updated); + } + } catch { + // Ignore poll errors + } + }, 5000); + + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }; + }, [isLive, directive.id, onRefresh]); + return ( <div className="panel h-full flex flex-col"> {/* Header */} @@ -100,6 +203,11 @@ export function DirectiveDetail({ > {directive.status} </span> + {isLive && ( + <span className="font-mono text-[9px] text-yellow-400/60 animate-pulse"> + polling + </span> + )} <span className="font-mono text-[10px] text-[#7788aa]"> v{directive.version} </span> @@ -129,6 +237,28 @@ export function DirectiveDetail({ {/* Content */} <div className="flex-1 overflow-y-auto p-4 space-y-4"> + {/* Orchestrator contract link */} + {directive.orchestratorContractId && ( + <div className="flex items-center gap-2 p-2 border border-dashed border-[rgba(117,170,252,0.2)] bg-[rgba(117,170,252,0.03)]"> + <span className="font-mono text-[10px] text-[#7788aa] uppercase"> + Planning Contract + </span> + <button + onClick={() => + navigate(`/contracts/${directive.orchestratorContractId}`) + } + className="font-mono text-[11px] text-[#75aafc] hover:text-white transition-colors" + > + {directive.orchestratorContractId.slice(0, 8)}... → + </button> + {directive.status === "planning" && ( + <span className="font-mono text-[9px] text-yellow-400 animate-pulse"> + planning in progress + </span> + )} + </div> + )} + {/* Goal */} <div> <h4 className="font-mono text-[10px] text-[#75aafc] uppercase tracking-wider mb-1"> @@ -196,12 +326,14 @@ export function DirectiveDetail({ </h4> {directive.chains.length === 0 ? ( <p className="font-mono text-xs text-[#7788aa]"> - No chains yet. Chains are created during planning. + {directive.status === "planning" + ? "Planning in progress... chains will appear when the planner completes." + : "No chains yet. Chains are created during planning."} </p> ) : ( <div className="space-y-2"> - {directive.chains.map((chain) => ( - <ChainCard key={chain.id} chain={chain} /> + {directive.chains.map((cws) => ( + <ChainCard key={cws.id} chainWithSteps={cws} /> ))} </div> )} diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts index 2187c34..ccc7156 100644 --- a/makima/frontend/src/lib/api.ts +++ b/makima/frontend/src/lib/api.ts @@ -3075,8 +3075,31 @@ export interface DirectiveChain { updatedAt: string; } +export interface ChainStep { + id: string; + chainId: string; + name: string; + description: string | null; + stepType: string; + contractType: string; + initialPhase: string | null; + taskPlan: string | null; + dependsOn: string[] | null; + status: string; + contractId: string | null; + supervisorTaskId: string | null; + orderIndex: number; + startedAt: string | null; + completedAt: string | null; + createdAt: string; +} + +export interface ChainWithSteps extends DirectiveChain { + steps: ChainStep[]; +} + export interface DirectiveWithChains extends Directive { - chains: DirectiveChain[]; + chains: ChainWithSteps[]; } export interface DirectiveListResponse { diff --git a/makima/frontend/src/routes/directives.tsx b/makima/frontend/src/routes/directives.tsx index 8b82f99..9437389 100644 --- a/makima/frontend/src/routes/directives.tsx +++ b/makima/frontend/src/routes/directives.tsx @@ -129,6 +129,13 @@ function DirectivesContent() { [startDirective, fetchDirective] ); + const handleRefresh = useCallback( + (updated: DirectiveWithChains) => { + setSelectedDirective(updated); + }, + [] + ); + // Detail view if (id) { if (detailLoading) { @@ -152,6 +159,7 @@ function DirectivesContent() { onBack={handleBack} onDelete={handleDelete} onStart={handleStart} + onRefresh={handleRefresh} /> </main> ); |
