summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/directives
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/components/directives')
-rw-r--r--makima/frontend/src/components/directives/DirectiveContractsTab.tsx148
-rw-r--r--makima/frontend/src/components/directives/DirectiveDetail.tsx425
-rw-r--r--makima/frontend/src/components/directives/DirectiveList.tsx143
-rw-r--r--makima/frontend/src/components/directives/StepDiagram.tsx313
4 files changed, 0 insertions, 1029 deletions
diff --git a/makima/frontend/src/components/directives/DirectiveContractsTab.tsx b/makima/frontend/src/components/directives/DirectiveContractsTab.tsx
deleted file mode 100644
index 28da117..0000000
--- a/makima/frontend/src/components/directives/DirectiveContractsTab.tsx
+++ /dev/null
@@ -1,148 +0,0 @@
-import { useNavigate } from "react-router";
-import type {
- DirectiveWithChains,
- StepContractSummary,
- ContractPhase,
-} from "../../lib/api";
-import { PhaseProgressBarCompact } from "../contracts/PhaseProgressBar";
-
-interface DirectiveContractsTabProps {
- directive: DirectiveWithChains;
-}
-
-const statusColors: Record<string, string> = {
- active: "text-green-400",
- completed: "text-blue-400",
- archived: "text-[#555]",
-};
-
-function ContractCard({
- summary,
- label,
-}: {
- summary: StepContractSummary;
- label: string;
-}) {
- const navigate = useNavigate();
-
- const progressPct =
- summary.taskCount > 0
- ? Math.round((summary.tasksDone / summary.taskCount) * 100)
- : 0;
-
- return (
- <div
- className="border border-dashed border-[rgba(117,170,252,0.25)] bg-[rgba(117,170,252,0.03)] p-3 cursor-pointer hover:bg-[rgba(117,170,252,0.06)] transition-colors"
- onClick={() => navigate(`/contracts/${summary.id}`)}
- >
- <div className="flex items-center justify-between mb-1.5">
- <div className="flex items-center gap-2 min-w-0">
- <span className="font-mono text-[11px] text-[#dbe7ff] truncate">
- {summary.name}
- </span>
- <span className="font-mono text-[9px] text-[#7788aa] uppercase shrink-0">
- {summary.contractType}
- </span>
- </div>
- <div className="flex items-center gap-2 shrink-0">
- <span
- className={`font-mono text-[9px] uppercase ${statusColors[summary.status] || "text-[#888]"}`}
- >
- {summary.status}
- </span>
- <span className="font-mono text-[9px] text-[#75aafc]">&rarr;</span>
- </div>
- </div>
-
- <div className="flex items-center gap-2 mb-1.5">
- <span className="font-mono text-[9px] text-[#7788aa] uppercase shrink-0">
- {label}
- </span>
- <PhaseProgressBarCompact
- currentPhase={summary.phase as ContractPhase}
- />
- </div>
-
- {/* Task progress bar */}
- <div className="flex items-center gap-2">
- <div className="flex-1 h-1 bg-[rgba(117,170,252,0.1)] rounded-full overflow-hidden">
- <div
- className="h-full bg-[#3f6fb3] rounded-full transition-all"
- style={{ width: `${progressPct}%` }}
- />
- </div>
- <span className="font-mono text-[9px] text-[#7788aa] shrink-0">
- {summary.tasksDone}/{summary.taskCount} tasks
- </span>
- </div>
- </div>
- );
-}
-
-export function DirectiveContractsTab({
- directive,
-}: DirectiveContractsTabProps) {
- // Collect all contract summaries
- const contracts: { summary: StepContractSummary; label: string }[] = [];
-
- if (directive.orchestratorContractSummary) {
- contracts.push({
- summary: directive.orchestratorContractSummary,
- label: "Planning",
- });
- }
-
- for (const chain of directive.chains) {
- for (const step of chain.steps) {
- if (step.contractSummary) {
- contracts.push({
- summary: step.contractSummary,
- label: step.name,
- });
- }
- // Show monitoring/evaluation contracts
- if (step.monitoringContractId) {
- contracts.push({
- summary: {
- id: step.monitoringContractId,
- name: `${step.name} - Evaluation`,
- contractType: "monitoring",
- status: step.status === "evaluating" ? "active" : "completed",
- phase: "plan",
- taskCount: 1,
- tasksDone: step.status === "evaluating" ? 0 : 1,
- tasksRunning: step.status === "evaluating" ? 1 : 0,
- tasksFailed: 0,
- },
- label: `${step.name} eval`,
- });
- }
- }
- }
-
- if (contracts.length === 0) {
- return (
- <div className="text-center py-8">
- <p className="font-mono text-xs text-[#7788aa]">
- {directive.status === "draft"
- ? "No contracts yet. Start the directive to begin planning."
- : directive.status === "planning"
- ? "Planning in progress... contracts will appear when steps are created."
- : "No contracts associated with this directive."}
- </p>
- </div>
- );
- }
-
- return (
- <div className="space-y-2">
- {contracts.map((c) => (
- <ContractCard
- key={c.summary.id}
- summary={c.summary}
- label={c.label}
- />
- ))}
- </div>
- );
-}
diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx
deleted file mode 100644
index 6bdf5aa..0000000
--- a/makima/frontend/src/components/directives/DirectiveDetail.tsx
+++ /dev/null
@@ -1,425 +0,0 @@
-import { useState, useEffect, useRef } from "react";
-import { useNavigate } from "react-router";
-import type {
- DirectiveWithChains,
- DirectiveStatus,
- ContractPhase,
-} from "../../lib/api";
-import { getDirective } from "../../lib/api";
-import { PhaseProgressBarCompact } from "../contracts/PhaseProgressBar";
-import { StepDiagram } from "./StepDiagram";
-import { DirectiveContractsTab } from "./DirectiveContractsTab";
-
-interface DirectiveDetailProps {
- directive: DirectiveWithChains;
- onBack: () => void;
- onDelete?: (id: string) => void;
- onStart?: (id: string) => void;
- onRefresh?: (updated: DirectiveWithChains) => void;
-}
-
-type Tab = "overview" | "chain" | "contracts";
-
-const statusColors: Record<DirectiveStatus, string> = {
- draft: "text-[#888]",
- planning: "text-yellow-400",
- active: "text-green-400",
- paused: "text-orange-400",
- completed: "text-blue-400",
- archived: "text-[#555]",
- failed: "text-red-400",
-};
-
-function JsonSection({
- label,
- data,
-}: {
- label: string;
- data: unknown[] | unknown;
-}) {
- const items = Array.isArray(data) ? data : [];
- if (items.length === 0) return null;
-
- return (
- <div>
- <h4 className="font-mono text-[10px] text-[#75aafc] uppercase tracking-wider mb-1">
- {label}
- </h4>
- <div className="font-mono text-xs text-[#9bb8d8] bg-[rgba(0,0,0,0.2)] p-2 max-h-32 overflow-y-auto">
- {items.map((item, i) => (
- <div key={i} className="mb-0.5">
- {typeof item === "string" ? item : JSON.stringify(item)}
- </div>
- ))}
- </div>
- </div>
- );
-}
-
-export function DirectiveDetail({
- directive,
- onBack,
- onDelete,
- onStart,
- onRefresh,
-}: DirectiveDetailProps) {
- const navigate = useNavigate();
- const [activeTab, setActiveTab] = useState<Tab>("overview");
-
- // 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]);
-
- // Count total steps and completed steps across all chains
- const totalSteps = directive.chains.reduce(
- (sum, c) => sum + c.totalSteps,
- 0
- );
- const completedSteps = directive.chains.reduce(
- (sum, c) => sum + c.completedSteps,
- 0
- );
-
- // Count contracts
- const contractCount =
- (directive.orchestratorContractSummary ? 1 : 0) +
- directive.chains.reduce(
- (sum, c) =>
- sum + c.steps.filter((s) => s.contractSummary != null).length,
- 0
- );
-
- const tabs: { key: Tab; label: string; count?: number }[] = [
- { key: "overview", label: "Overview" },
- { key: "chain", label: "Chain", count: totalSteps },
- { key: "contracts", label: "Contracts", count: contractCount },
- ];
-
- return (
- <div className="panel h-full flex flex-col">
- {/* Header */}
- <div className="p-4 border-b border-dashed border-[rgba(117,170,252,0.35)]">
- <div className="flex items-center justify-between mb-3">
- <button
- onClick={onBack}
- className="font-mono text-xs text-[#75aafc] hover:text-[#9bc3ff] transition-colors"
- >
- &larr; Back to list
- </button>
- <div className="flex items-center gap-2">
- {onStart && directive.status === "draft" && (
- <button
- onClick={() => onStart(directive.id)}
- className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase"
- >
- Start
- </button>
- )}
- {onDelete && (
- <button
- onClick={() => onDelete(directive.id)}
- className="px-3 py-1.5 font-mono text-xs text-red-400 border border-red-400/30 hover:border-red-400/50 transition-colors uppercase"
- >
- Delete
- </button>
- )}
- </div>
- </div>
- <div className="flex items-center gap-3 mb-2">
- <h2 className="font-mono text-lg text-[#dbe7ff]">
- {directive.title}
- </h2>
- <span
- className={`font-mono text-xs uppercase ${
- statusColors[directive.status as DirectiveStatus] || "text-[#888]"
- }`}
- >
- {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>
- </div>
- </div>
-
- {/* Tabs */}
- <div className="flex border-b border-[rgba(117,170,252,0.2)]">
- {tabs.map((tab) => (
- <button
- key={tab.key}
- onClick={() => setActiveTab(tab.key)}
- className={`
- px-4 py-2 font-mono text-xs uppercase tracking-wider transition-colors
- ${
- activeTab === tab.key
- ? "text-[#dbe7ff] border-b-2 border-[#75aafc]"
- : "text-[#555] hover:text-[#9bc3ff]"
- }
- `}
- >
- {tab.label}
- {tab.count != null && tab.count > 0 && (
- <span className="ml-1 text-[10px] text-[#7788aa]">
- ({tab.count})
- </span>
- )}
- </button>
- ))}
- </div>
-
- {/* Tab content */}
- <div className="flex-1 overflow-y-auto p-4">
- {activeTab === "overview" && (
- <div className="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>
- {directive.orchestratorContractSummary && (
- <PhaseProgressBarCompact
- currentPhase={
- directive.orchestratorContractSummary
- .phase as ContractPhase
- }
- />
- )}
- <button
- onClick={() =>
- navigate(
- `/contracts/${directive.orchestratorContractId}`
- )
- }
- className="font-mono text-[11px] text-[#75aafc] hover:text-white transition-colors"
- >
- {directive.orchestratorContractSummary?.name ||
- directive.orchestratorContractId.slice(0, 8) + "..."}{" "}
- &rarr;
- </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">
- Goal
- </h4>
- <p className="font-mono text-xs text-[#9bb8d8] whitespace-pre-wrap">
- {directive.goal}
- </p>
- </div>
-
- {/* Config grid */}
- <div className="grid grid-cols-3 gap-2">
- <div>
- <span className="font-mono text-[10px] text-[#7788aa] uppercase">
- Autonomy
- </span>
- <div className="font-mono text-xs text-[#dbe7ff]">
- {directive.autonomyLevel}
- </div>
- </div>
- <div>
- <span className="font-mono text-[10px] text-[#7788aa] uppercase">
- Chains
- </span>
- <div className="font-mono text-xs text-[#dbe7ff]">
- {directive.chainGenerationCount} generated
- </div>
- </div>
- <div>
- <span className="font-mono text-[10px] text-[#7788aa] uppercase">
- Cost
- </span>
- <div className="font-mono text-xs text-[#dbe7ff]">
- ${directive.totalCostUsd.toFixed(2)}
- </div>
- </div>
- {directive.repositoryUrl && (
- <div className="col-span-3">
- <span className="font-mono text-[10px] text-[#7788aa] uppercase">
- Repository
- </span>
- <div className="font-mono text-xs text-[#dbe7ff] truncate">
- {directive.repositoryUrl}
- </div>
- </div>
- )}
- </div>
-
- {/* Stat cards */}
- <div className="grid grid-cols-3 gap-2">
- <div className="border border-dashed border-[rgba(117,170,252,0.2)] p-2 text-center">
- <div className="font-mono text-lg text-[#dbe7ff]">
- {totalSteps}
- </div>
- <div className="font-mono text-[9px] text-[#7788aa] uppercase">
- Total Steps
- </div>
- </div>
- <div className="border border-dashed border-[rgba(117,170,252,0.2)] p-2 text-center">
- <div className="font-mono text-lg text-green-400">
- {completedSteps}
- </div>
- <div className="font-mono text-[9px] text-[#7788aa] uppercase">
- Completed
- </div>
- </div>
- <div className="border border-dashed border-[rgba(117,170,252,0.2)] p-2 text-center">
- <div className="font-mono text-lg text-[#dbe7ff]">
- ${directive.totalCostUsd.toFixed(2)}
- </div>
- <div className="font-mono text-[9px] text-[#7788aa] uppercase">
- Cost
- </div>
- </div>
- </div>
-
- {/* Structured sections */}
- <JsonSection label="Requirements" data={directive.requirements} />
- <JsonSection
- label="Acceptance Criteria"
- data={directive.acceptanceCriteria}
- />
- <JsonSection label="Constraints" data={directive.constraints} />
- <JsonSection
- label="External Dependencies"
- data={directive.externalDependencies}
- />
-
- {/* Metadata */}
- <div>
- <h4 className="font-mono text-[10px] text-[#75aafc] uppercase tracking-wider mb-1">
- Metadata
- </h4>
- <div className="grid grid-cols-2 gap-1 font-mono text-[10px]">
- <span className="text-[#7788aa]">Created</span>
- <span className="text-[#9bb8d8]">
- {new Date(directive.createdAt).toLocaleString()}
- </span>
- <span className="text-[#7788aa]">Updated</span>
- <span className="text-[#9bb8d8]">
- {new Date(directive.updatedAt).toLocaleString()}
- </span>
- {directive.startedAt && (
- <>
- <span className="text-[#7788aa]">Started</span>
- <span className="text-[#9bb8d8]">
- {new Date(directive.startedAt).toLocaleString()}
- </span>
- </>
- )}
- {directive.completedAt && (
- <>
- <span className="text-[#7788aa]">Completed</span>
- <span className="text-[#9bb8d8]">
- {new Date(directive.completedAt).toLocaleString()}
- </span>
- </>
- )}
- <span className="text-[#7788aa]">Version</span>
- <span className="text-[#9bb8d8]">{directive.version}</span>
- </div>
- </div>
- </div>
- )}
-
- {activeTab === "chain" && (
- <div className="space-y-4">
- {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... 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>
- ) : (
- <>
- {/* 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>
- )}
-
- {activeTab === "contracts" && (
- <DirectiveContractsTab directive={directive} />
- )}
- </div>
- </div>
- );
-}
diff --git a/makima/frontend/src/components/directives/DirectiveList.tsx b/makima/frontend/src/components/directives/DirectiveList.tsx
deleted file mode 100644
index 5afa36e..0000000
--- a/makima/frontend/src/components/directives/DirectiveList.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-import { useState } from "react";
-import type { DirectiveSummary, DirectiveStatus } from "../../lib/api";
-
-interface DirectiveListProps {
- directives: DirectiveSummary[];
- loading: boolean;
- onSelect: (id: string) => void;
- onCreate: () => void;
- onDelete?: (directive: DirectiveSummary) => void;
- selectedId?: string;
-}
-
-const statusColors: Record<DirectiveStatus, string> = {
- draft: "text-[#888]",
- planning: "text-yellow-400",
- active: "text-green-400",
- paused: "text-orange-400",
- completed: "text-blue-400",
- archived: "text-[#555]",
- failed: "text-red-400",
-};
-
-export function DirectiveList({
- directives,
- loading,
- onSelect,
- onCreate,
- selectedId,
-}: DirectiveListProps) {
- const [filter, setFilter] = useState<DirectiveStatus | "all">("all");
-
- const filteredDirectives =
- filter === "all"
- ? directives
- : directives.filter((d) => d.status === filter);
-
- if (loading) {
- return (
- <div className="panel h-full flex items-center justify-center">
- <div className="font-mono text-[#9bc3ff] text-sm">Loading...</div>
- </div>
- );
- }
-
- return (
- <div className="panel h-full flex flex-col">
- {/* Header */}
- <div className="p-4 border-b border-dashed border-[rgba(117,170,252,0.35)]">
- <div className="flex items-center justify-between mb-3">
- <h2 className="font-mono text-sm text-[#75aafc] uppercase tracking-wider">
- Directives
- </h2>
- <button
- onClick={onCreate}
- className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase"
- >
- + New
- </button>
- </div>
-
- {/* Filter tabs */}
- <div className="flex gap-1 flex-wrap">
- {(["all", "draft", "planning", "active", "paused", "completed", "failed"] as const).map(
- (status) => (
- <button
- key={status}
- onClick={() => setFilter(status)}
- className={`
- px-2 py-1 font-mono text-[10px] uppercase tracking-wider transition-colors
- ${
- filter === status
- ? "bg-[rgba(117,170,252,0.1)] text-[#9bc3ff] border border-[rgba(117,170,252,0.3)]"
- : "text-[#555] hover:text-[#75aafc]"
- }
- `}
- >
- {status}
- </button>
- )
- )}
- </div>
- </div>
-
- {/* List */}
- <div className="flex-1 overflow-y-auto">
- {filteredDirectives.length === 0 ? (
- <div className="p-4 text-center">
- <p className="font-mono text-sm text-[#555]">
- {filter === "all"
- ? "No directives yet"
- : `No ${filter} directives`}
- </p>
- </div>
- ) : (
- <div className="divide-y divide-[rgba(117,170,252,0.15)]">
- {filteredDirectives.map((directive) => (
- <button
- key={directive.id}
- onClick={() => onSelect(directive.id)}
- className={`
- w-full text-left p-4 transition-colors
- ${
- selectedId === directive.id
- ? "bg-[rgba(117,170,252,0.1)]"
- : "hover:bg-[rgba(117,170,252,0.05)]"
- }
- `}
- >
- <div className="flex items-start justify-between gap-2 mb-2">
- <h3 className="font-mono text-sm text-[#dbe7ff] truncate">
- {directive.title}
- </h3>
- <span
- className={`text-[10px] font-mono uppercase shrink-0 ${
- statusColors[directive.status] || "text-[#888]"
- }`}
- >
- {directive.status}
- </span>
- </div>
-
- {directive.goal && (
- <p className="font-mono text-xs text-[#555] mb-2 line-clamp-2">
- {directive.goal}
- </p>
- )}
-
- <div className="flex items-center gap-3 text-[10px] font-mono text-[#555]">
- {directive.chainCount > 0 && (
- <span>{directive.chainCount} chains</span>
- )}
- {directive.stepCount > 0 && (
- <span>{directive.stepCount} steps</span>
- )}
- </div>
- </button>
- ))}
- </div>
- )}
- </div>
- </div>
- );
-}
diff --git a/makima/frontend/src/components/directives/StepDiagram.tsx b/makima/frontend/src/components/directives/StepDiagram.tsx
deleted file mode 100644
index 33892e0..0000000
--- a/makima/frontend/src/components/directives/StepDiagram.tsx
+++ /dev/null
@@ -1,313 +0,0 @@
-import { useNavigate } from "react-router";
-import type { ChainStep, ContractPhase } from "../../lib/api";
-import { PhaseProgressBarCompact } from "../contracts/PhaseProgressBar";
-
-interface StepDiagramProps {
- steps: ChainStep[];
-}
-
-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 statusLabels: Record<string, string> = {
- pending: "Pending",
- ready: "Ready",
- running: "Running",
- evaluating: "Evaluating",
- passed: "Passed",
- failed: "Failed",
- rework: "Rework",
- skipped: "Skipped",
- blocked: "Blocked",
-};
-
-const confidenceColors: Record<string, string> = {
- green: "text-green-400",
- yellow: "text-yellow-400",
- red: "text-red-400",
-};
-
-/**
- * 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>();
- 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 (
- <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>
- )}
-
- {/* Evaluation info */}
- {(step.confidenceScore != null || step.evaluationCount > 0 || step.reworkCount > 0) && (
- <div className="border-t border-[rgba(117,170,252,0.1)] pt-2 mt-1">
- <div className="flex items-center gap-2 font-mono text-[9px]">
- {step.confidenceScore != null && (
- <span className={confidenceColors[step.confidenceLevel || ""] || "text-[#7788aa]"}>
- {Math.round(step.confidenceScore * 100)}% confidence
- </span>
- )}
- {step.evaluationCount > 0 && (
- <span className="text-[#7788aa]">
- eval #{step.evaluationCount}
- </span>
- )}
- {step.reworkCount > 0 && (
- <span className="text-orange-400">
- rework x{step.reworkCount}
- </span>
- )}
- </div>
- </div>
- )}
-
- {/* Monitoring link (when evaluating) */}
- {step.status === "evaluating" && step.monitoringContractId && (
- <div className="border-t border-[rgba(117,170,252,0.1)] pt-1.5 mt-1">
- <span
- className="font-mono text-[9px] text-blue-400 cursor-pointer hover:text-blue-300"
- onClick={(e) => {
- e.stopPropagation();
- navigate(`/contracts/${step.monitoringContractId}`);
- }}
- >
- evaluation contract &rarr;
- </span>
- </div>
- )}
-
- {/* 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 && step.status !== "evaluating" && (
- <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>
- );
- }
-
- 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").length;
- const evaluatingCount = steps.filter(s => s.status === "evaluating").length;
-
- return (
- <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} running</span>
- )}
- {evaluatingCount > 0 && (
- <span className="text-blue-400">{evaluatingCount} evaluating</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>
- );
-}