diff options
Diffstat (limited to 'makima/frontend')
| -rw-r--r-- | makima/frontend/src/components/NavStrip.tsx | 1 | ||||
| -rw-r--r-- | makima/frontend/src/components/directives/DirectiveContractsTab.tsx | 148 | ||||
| -rw-r--r-- | makima/frontend/src/components/directives/DirectiveDetail.tsx | 425 | ||||
| -rw-r--r-- | makima/frontend/src/components/directives/DirectiveList.tsx | 143 | ||||
| -rw-r--r-- | makima/frontend/src/components/directives/StepDiagram.tsx | 313 | ||||
| -rw-r--r-- | makima/frontend/src/hooks/useDirectives.ts | 127 | ||||
| -rw-r--r-- | makima/frontend/src/lib/api.ts | 213 | ||||
| -rw-r--r-- | makima/frontend/src/main.tsx | 17 | ||||
| -rw-r--r-- | makima/frontend/src/routes/directives.tsx | 403 |
9 files changed, 0 insertions, 1790 deletions
diff --git a/makima/frontend/src/components/NavStrip.tsx b/makima/frontend/src/components/NavStrip.tsx index f7e67db..fb95c7f 100644 --- a/makima/frontend/src/components/NavStrip.tsx +++ b/makima/frontend/src/components/NavStrip.tsx @@ -11,7 +11,6 @@ interface NavLink { const NAV_LINKS: NavLink[] = [ { label: "Listen", href: "/listen" }, { label: "Contracts", href: "/contracts", requiresAuth: true }, - { label: "Directives", href: "/directives", requiresAuth: true }, { label: "Board", href: "/workflow", requiresAuth: true }, { label: "Mesh", href: "/mesh", requiresAuth: true }, { label: "History", href: "/history", requiresAuth: true }, 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]">→</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" - > - ← 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) + "..."}{" "} - → - </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 → - </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 → - </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> - ); - } - - 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" : ""} · {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> - ); -} diff --git a/makima/frontend/src/hooks/useDirectives.ts b/makima/frontend/src/hooks/useDirectives.ts deleted file mode 100644 index af1c8c6..0000000 --- a/makima/frontend/src/hooks/useDirectives.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { useState, useCallback, useEffect } from "react"; -import { - listDirectives, - getDirective, - createDirective, - updateDirective, - deleteDirective, - startDirective as startDirectiveApi, - type DirectiveSummary, - type DirectiveWithChains, - type CreateDirectiveRequest, - type UpdateDirectiveRequest, -} from "../lib/api"; - -export function useDirectives() { - const [directives, setDirectives] = useState<DirectiveSummary[]>([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState<string | null>(null); - - const fetchDirectives = useCallback(async () => { - setLoading(true); - setError(null); - try { - const response = await listDirectives(); - setDirectives(response.directives); - } catch (e) { - setError(e instanceof Error ? e.message : "Failed to fetch directives"); - } finally { - setLoading(false); - } - }, []); - - const fetchDirective = useCallback( - async (id: string): Promise<DirectiveWithChains | null> => { - setError(null); - try { - return await getDirective(id); - } catch (e) { - setError(e instanceof Error ? e.message : "Failed to fetch directive"); - return null; - } - }, - [] - ); - - const saveDirective = useCallback( - async (data: CreateDirectiveRequest): Promise<DirectiveSummary | null> => { - setError(null); - try { - const directive = await createDirective(data); - await fetchDirectives(); - return directive as unknown as DirectiveSummary; - } catch (e) { - setError(e instanceof Error ? e.message : "Failed to create directive"); - return null; - } - }, - [fetchDirectives] - ); - - const editDirective = useCallback( - async ( - id: string, - data: UpdateDirectiveRequest - ): Promise<DirectiveSummary | null> => { - setError(null); - try { - const directive = await updateDirective(id, data); - await fetchDirectives(); - return directive as unknown as DirectiveSummary; - } catch (e) { - setError(e instanceof Error ? e.message : "Failed to update directive"); - return null; - } - }, - [fetchDirectives] - ); - - const removeDirective = useCallback( - async (id: string): Promise<boolean> => { - setError(null); - try { - await deleteDirective(id); - await fetchDirectives(); - return true; - } catch (e) { - setError(e instanceof Error ? e.message : "Failed to delete directive"); - return false; - } - }, - [fetchDirectives] - ); - - const startDirective = useCallback( - async (id: string): Promise<boolean> => { - setError(null); - try { - await startDirectiveApi(id); - await fetchDirectives(); - return true; - } catch (e) { - setError( - e instanceof Error ? e.message : "Failed to start directive" - ); - return false; - } - }, - [fetchDirectives] - ); - - // Initial fetch - useEffect(() => { - fetchDirectives(); - }, [fetchDirectives]); - - return { - directives, - loading, - error, - fetchDirectives, - fetchDirective, - saveDirective, - editDirective, - removeDirective, - startDirective, - }; -} diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts index 5080ee1..7732725 100644 --- a/makima/frontend/src/lib/api.ts +++ b/makima/frontend/src/lib/api.ts @@ -3003,217 +3003,4 @@ export async function listTaskPatches(taskId: string, contractId: string): Promi return res.json(); } -// ============================================================================= -// Directive Types & API -// ============================================================================= - -export type DirectiveStatus = "draft" | "planning" | "active" | "paused" | "completed" | "archived" | "failed"; -export type AutonomyLevel = "full_auto" | "guardrails" | "manual"; - -export interface DirectiveSummary { - id: string; - title: string; - goal: string; - status: DirectiveStatus; - autonomyLevel: AutonomyLevel; - chainCount: number; - stepCount: number; - totalCostUsd: number; - version: number; - createdAt: string; - updatedAt: string; -} - -export interface Directive { - id: string; - ownerId: string; - title: string; - goal: string; - requirements: unknown[]; - acceptanceCriteria: unknown[]; - constraints: unknown[]; - externalDependencies: unknown[]; - status: DirectiveStatus; - autonomyLevel: AutonomyLevel; - confidenceThresholdGreen: number; - confidenceThresholdYellow: number; - maxTotalCostUsd: number | null; - maxWallTimeMinutes: number | null; - maxReworkCycles: number | null; - maxChainRegenerations: number | null; - repositoryUrl: string | null; - localPath: string | null; - baseBranch: string | null; - orchestratorContractId: string | null; - currentChainId: string | null; - chainGenerationCount: number; - totalCostUsd: number; - startedAt: string | null; - completedAt: string | null; - version: number; - createdAt: string; - updatedAt: string; -} - -export interface DirectiveChain { - id: string; - directiveId: string; - generation: number; - name: string; - description: string | null; - rationale: string | null; - planningModel: string | null; - status: string; - totalSteps: number; - completedSteps: number; - failedSteps: number; - currentConfidence: number | null; - startedAt: string | null; - completedAt: string | null; - version: number; - createdAt: string; - updatedAt: string; -} - -export interface StepContractSummary { - id: string; - name: string; - contractType: string; - phase: string; - status: string; - taskCount: number; - tasksDone: number; - tasksRunning: number; - tasksFailed: number; -} - -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; - monitoringContractId: string | null; - monitoringTaskId: string | null; - confidenceScore: number | null; - confidenceLevel: string | null; - evaluationCount: number; - reworkCount: number; - lastEvaluationId: string | null; - orderIndex: number; - startedAt: string | null; - completedAt: string | null; - createdAt: string; - contractSummary: StepContractSummary | null; -} - -export interface ChainWithSteps extends DirectiveChain { - steps: ChainStep[]; -} - -export interface DirectiveWithChains extends Directive { - orchestratorContractSummary: StepContractSummary | null; - chains: ChainWithSteps[]; -} - -export interface DirectiveListResponse { - directives: DirectiveSummary[]; - total: number; -} - -export interface CreateDirectiveRequest { - title: string; - goal: string; - requirements?: unknown[]; - acceptanceCriteria?: unknown[]; - constraints?: unknown[]; - externalDependencies?: unknown[]; - autonomyLevel?: AutonomyLevel; - repositoryUrl?: string; - localPath?: string; - baseBranch?: string; -} - -export interface UpdateDirectiveRequest { - title?: string; - goal?: string; - status?: DirectiveStatus; - autonomyLevel?: AutonomyLevel; - version?: number; -} - -export async function listDirectives(): Promise<DirectiveListResponse> { - const res = await authFetch(`${API_BASE}/api/v1/directives`); - if (!res.ok) { - throw new Error(`Failed to list directives: ${res.statusText}`); - } - return res.json(); -} - -export async function getDirective(id: string): Promise<DirectiveWithChains> { - const res = await authFetch(`${API_BASE}/api/v1/directives/${id}`); - if (!res.ok) { - throw new Error(`Failed to get directive: ${res.statusText}`); - } - return res.json(); -} - -export async function createDirective( - data: CreateDirectiveRequest -): Promise<Directive> { - const res = await authFetch(`${API_BASE}/api/v1/directives`, { - method: "POST", - body: JSON.stringify(data), - }); - if (!res.ok) { - throw new Error(`Failed to create directive: ${res.statusText}`); - } - return res.json(); -} - -export async function updateDirective( - id: string, - data: UpdateDirectiveRequest -): Promise<Directive> { - const res = await authFetch(`${API_BASE}/api/v1/directives/${id}`, { - method: "PUT", - body: JSON.stringify(data), - }); - if (res.status === 409) { - const conflict = (await res.json()) as ConflictErrorResponse; - throw new VersionConflictError(conflict); - } - if (!res.ok) { - throw new Error(`Failed to update directive: ${res.statusText}`); - } - return res.json(); -} - -export async function deleteDirective(id: string): Promise<void> { - const res = await authFetch(`${API_BASE}/api/v1/directives/${id}`, { - method: "DELETE", - }); - if (!res.ok) { - throw new Error(`Failed to delete directive: ${res.statusText}`); - } -} - -export async function startDirective(id: string): Promise<Directive> { - const res = await authFetch(`${API_BASE}/api/v1/directives/${id}/start`, { - method: "POST", - }); - if (!res.ok) { - const body = await res.json().catch(() => null); - const msg = body?.message || res.statusText; - throw new Error(`Failed to start directive: ${msg}`); - } - return res.json(); -} diff --git a/makima/frontend/src/main.tsx b/makima/frontend/src/main.tsx index f07a143..50fffe4 100644 --- a/makima/frontend/src/main.tsx +++ b/makima/frontend/src/main.tsx @@ -18,7 +18,6 @@ import HistoryPage from "./routes/history"; import LoginPage from "./routes/login"; import SettingsPage from "./routes/settings"; import ContractFilePage from "./routes/contract-file"; -import DirectivesPage from "./routes/directives"; import SpeakPage from "./routes/speak"; createRoot(document.getElementById("root")!).render( @@ -81,22 +80,6 @@ createRoot(document.getElementById("root")!).render( } /> <Route - path="/directives" - element={ - <ProtectedRoute> - <DirectivesPage /> - </ProtectedRoute> - } - /> - <Route - path="/directives/:id" - element={ - <ProtectedRoute> - <DirectivesPage /> - </ProtectedRoute> - } - /> - <Route path="/workflow" element={ <ProtectedRoute> diff --git a/makima/frontend/src/routes/directives.tsx b/makima/frontend/src/routes/directives.tsx deleted file mode 100644 index fd3808b..0000000 --- a/makima/frontend/src/routes/directives.tsx +++ /dev/null @@ -1,403 +0,0 @@ -import { useState, useCallback, useEffect } from "react"; -import { useParams, useNavigate } from "react-router"; -import { Masthead } from "../components/Masthead"; -import { DirectiveList } from "../components/directives/DirectiveList"; -import { DirectiveDetail } from "../components/directives/DirectiveDetail"; -import { DirectoryInput } from "../components/mesh/DirectoryInput"; -import { useDirectives } from "../hooks/useDirectives"; -import { useAuth } from "../contexts/AuthContext"; -import { - getDaemonDirectories, - getRepositorySuggestions, -} from "../lib/api"; -import type { - DirectiveWithChains, - CreateDirectiveRequest, - RepositorySourceType, - DaemonDirectory, - RepositoryHistoryEntry, -} from "../lib/api"; - -export default function DirectivesPage() { - const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth(); - const navigate = useNavigate(); - - // Redirect to login if not authenticated (when auth is configured) - useEffect(() => { - if (!authLoading && isAuthConfigured && !isAuthenticated) { - navigate("/login"); - } - }, [authLoading, isAuthConfigured, isAuthenticated, navigate]); - - if (authLoading) { - return ( - <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]"> - <Masthead showNav /> - <main className="flex-1 flex items-center justify-center"> - <p className="text-[#7788aa] font-mono text-sm">Loading...</p> - </main> - </div> - ); - } - - if (isAuthConfigured && !isAuthenticated) { - return null; - } - - return <DirectivesContent />; -} - -function DirectivesContent() { - const { id } = useParams<{ id?: string }>(); - const navigate = useNavigate(); - const { - directives, - loading, - error, - fetchDirective, - saveDirective, - removeDirective, - startDirective, - } = useDirectives(); - - const [selectedDirective, setSelectedDirective] = - useState<DirectiveWithChains | null>(null); - const [detailLoading, setDetailLoading] = useState(false); - const [showCreateForm, setShowCreateForm] = useState(false); - const [createTitle, setCreateTitle] = useState(""); - const [createGoal, setCreateGoal] = useState(""); - - // Repository state - const [repoType, setRepoType] = useState<RepositorySourceType>("remote"); - const [repoUrl, setRepoUrl] = useState(""); - const [repoPath, setRepoPath] = useState(""); - const [suggestedDirectories, setSuggestedDirectories] = useState<DaemonDirectory[]>([]); - const [repoSuggestions, setRepoSuggestions] = useState<RepositoryHistoryEntry[]>([]); - const [showRepoSuggestions, setShowRepoSuggestions] = useState(false); - - // Fetch repository suggestions when modal opens and repo type changes - useEffect(() => { - if (showCreateForm && (repoType === "remote" || repoType === "local")) { - getRepositorySuggestions(repoType, undefined, 10) - .then((res) => { - setRepoSuggestions(res.entries); - setShowRepoSuggestions(res.entries.length > 0); - }) - .catch(() => { - setRepoSuggestions([]); - setShowRepoSuggestions(false); - }); - } else { - setRepoSuggestions([]); - setShowRepoSuggestions(false); - } - }, [showCreateForm, repoType]); - - // Fetch daemon directories when "local" repo type is selected - useEffect(() => { - if (repoType === "local" && showCreateForm) { - getDaemonDirectories() - .then((res) => setSuggestedDirectories(res.directories)) - .catch(() => setSuggestedDirectories([])); - } - }, [repoType, showCreateForm]); - - // Apply a repository suggestion - const applyRepoSuggestion = useCallback((suggestion: RepositoryHistoryEntry) => { - if (suggestion.repositoryUrl) { - setRepoUrl(suggestion.repositoryUrl); - } - if (suggestion.localPath) { - setRepoPath(suggestion.localPath); - } - setShowRepoSuggestions(false); - }, []); - - // Load directive when ID changes - useEffect(() => { - if (id) { - setDetailLoading(true); - fetchDirective(id).then((d) => { - setSelectedDirective(d); - setDetailLoading(false); - }); - } else { - setSelectedDirective(null); - } - }, [id, fetchDirective]); - - const handleSelect = useCallback( - (directiveId: string) => { - navigate(`/directives/${directiveId}`); - }, - [navigate] - ); - - const handleBack = useCallback(() => { - navigate("/directives"); - }, [navigate]); - - const resetCreateForm = useCallback(() => { - setShowCreateForm(false); - setCreateTitle(""); - setCreateGoal(""); - setRepoType("remote"); - setRepoUrl(""); - setRepoPath(""); - }, []); - - const handleCreate = useCallback(async () => { - if (!createTitle.trim() || !createGoal.trim()) return; - - const data: CreateDirectiveRequest = { - title: createTitle.trim(), - goal: createGoal.trim(), - }; - if (repoType === "remote" && repoUrl.trim()) { - data.repositoryUrl = repoUrl.trim(); - } else if (repoType === "local" && repoPath.trim()) { - data.localPath = repoPath.trim(); - } - - const result = await saveDirective(data); - if (result) { - resetCreateForm(); - } - }, [createTitle, createGoal, repoType, repoUrl, repoPath, saveDirective, resetCreateForm]); - - const handleDelete = useCallback( - async (directiveId: string) => { - const ok = await removeDirective(directiveId); - if (ok && id === directiveId) { - navigate("/directives"); - } - }, - [removeDirective, id, navigate] - ); - - const handleStart = useCallback( - async (directiveId: string) => { - const ok = await startDirective(directiveId); - if (ok) { - const updated = await fetchDirective(directiveId); - if (updated) { - setSelectedDirective(updated); - } - } - }, - [startDirective, fetchDirective] - ); - - const handleRefresh = useCallback( - (updated: DirectiveWithChains) => { - setSelectedDirective(updated); - }, - [] - ); - - return ( - <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]"> - <Masthead showNav /> - <main className="flex-1 flex flex-col p-4 pt-2 gap-4 overflow-hidden"> - {error && ( - <div className="p-3 bg-red-400/10 border border-red-400/30 text-red-400 font-mono text-sm"> - {error} - </div> - )} - - {/* Create directive modal */} - {showCreateForm && ( - <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"> - <div className="w-full max-w-lg max-h-[90vh] overflow-y-auto p-6 bg-[#0a1628] border border-[rgba(117,170,252,0.3)]"> - <h3 className="font-mono text-sm text-[#75aafc] uppercase mb-4"> - New Directive - </h3> - <div className="space-y-4"> - {/* Title */} - <div> - <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1"> - Title - </label> - <input - type="text" - placeholder="Directive title" - value={createTitle} - onChange={(e) => setCreateTitle(e.target.value)} - className="w-full px-3 py-2 font-mono text-sm text-[#dbe7ff] bg-[#0d1b2d] border border-[#3f6fb3] focus:border-[#75aafc] outline-none" - autoFocus - /> - </div> - - {/* Goal */} - <div> - <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1"> - Goal - </label> - <textarea - placeholder="What should be accomplished?" - value={createGoal} - onChange={(e) => setCreateGoal(e.target.value)} - rows={3} - className="w-full px-3 py-2 font-mono text-sm text-[#dbe7ff] bg-[#0d1b2d] border border-[#3f6fb3] focus:border-[#75aafc] outline-none resize-none" - /> - </div> - - {/* Repository Configuration */} - <div className="border-t border-[rgba(117,170,252,0.2)] pt-4"> - <label className="block font-mono text-xs text-[#75aafc] uppercase mb-3"> - Repository (optional) - </label> - - {/* Repository type selector */} - <div className="flex gap-2 mb-3"> - <button - type="button" - onClick={() => setRepoType("remote")} - className={`flex-1 px-3 py-2 font-mono text-xs uppercase transition-colors ${ - repoType === "remote" - ? "bg-[#0f3c78] text-[#dbe7ff] border border-[#75aafc]" - : "bg-[#0d1b2d] text-[#8b949e] border border-[#3f6fb3] hover:border-[#75aafc]" - }`} - > - Remote - </button> - <button - type="button" - onClick={() => setRepoType("local")} - className={`flex-1 px-3 py-2 font-mono text-xs uppercase transition-colors ${ - repoType === "local" - ? "bg-[#0f3c78] text-[#dbe7ff] border border-[#75aafc]" - : "bg-[#0d1b2d] text-[#8b949e] border border-[#3f6fb3] hover:border-[#75aafc]" - }`} - > - Local - </button> - </div> - - {/* Repository suggestions */} - {showRepoSuggestions && repoSuggestions.length > 0 && ( - <div className="mb-3"> - <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1"> - Recent Repositories - </label> - <div className="border border-[rgba(117,170,252,0.2)] bg-[#0a1525] max-h-32 overflow-y-auto"> - {repoSuggestions.map((suggestion) => ( - <button - key={suggestion.id} - type="button" - onClick={() => applyRepoSuggestion(suggestion)} - className="w-full text-left px-3 py-2 font-mono text-xs hover:bg-[rgba(117,170,252,0.1)] border-b border-[rgba(117,170,252,0.1)] last:border-b-0" - > - <div className="flex items-center justify-between"> - <span className="text-[#9bc3ff] truncate">{suggestion.name}</span> - <span className="text-[10px] text-[#556677] ml-2"> - {suggestion.useCount}× - </span> - </div> - <div className="text-[10px] text-[#556677] truncate"> - {repoType === "local" ? suggestion.localPath : suggestion.repositoryUrl} - </div> - </button> - ))} - </div> - </div> - )} - - {/* Repository URL (for remote) */} - {repoType === "remote" && ( - <div> - <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1"> - Repository URL - </label> - <input - type="text" - value={repoUrl} - onChange={(e) => setRepoUrl(e.target.value)} - placeholder="https://github.com/user/repo.git" - className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]" - /> - </div> - )} - - {/* Repository path (for local) */} - {repoType === "local" && ( - <div> - <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1"> - Local Path - </label> - <DirectoryInput - value={repoPath} - onChange={setRepoPath} - suggestions={suggestedDirectories} - placeholder="/path/to/repository" - repoUrl={repoUrl || null} - /> - </div> - )} - </div> - - {/* Actions */} - <div className="flex gap-2 justify-end pt-2"> - <button - onClick={resetCreateForm} - className="px-4 py-2 font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors" - > - Cancel - </button> - <button - onClick={handleCreate} - disabled={!createTitle.trim() || !createGoal.trim()} - className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors disabled:opacity-50 disabled:cursor-not-allowed" - > - Create - </button> - </div> - </div> - </div> - </div> - )} - - <div className="flex-1 grid grid-cols-[350px_1fr] gap-4 min-h-0"> - {/* Directive list */} - <DirectiveList - directives={directives} - loading={loading} - onSelect={handleSelect} - onCreate={() => setShowCreateForm(true)} - onDelete={(d) => handleDelete(d.id)} - selectedId={id} - /> - - {/* Directive detail or empty state */} - {detailLoading ? ( - <div className="panel h-full flex items-center justify-center"> - <p className="text-[#7788aa] font-mono text-sm">Loading directive...</p> - </div> - ) : selectedDirective ? ( - <DirectiveDetail - directive={selectedDirective} - onBack={handleBack} - onDelete={handleDelete} - onStart={handleStart} - onRefresh={handleRefresh} - /> - ) : ( - <div className="panel h-full flex items-center justify-center"> - <div className="text-center"> - <p className="font-mono text-sm text-[#555] mb-4"> - Select a directive or create a new one - </p> - <button - onClick={() => setShowCreateForm(true)} - className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase" - > - + New Directive - </button> - </div> - </div> - )} - </div> - </main> - </div> - ); -} |
