diff options
Diffstat (limited to 'makima/frontend/src')
14 files changed, 1178 insertions, 1088 deletions
diff --git a/makima/frontend/src/components/directives/ApprovalsTab.tsx b/makima/frontend/src/components/directives/ApprovalsTab.tsx new file mode 100644 index 0000000..dca48df --- /dev/null +++ b/makima/frontend/src/components/directives/ApprovalsTab.tsx @@ -0,0 +1,77 @@ +import type { DirectiveWithProgress } from "../../lib/api"; + +export function ApprovalsTab({ directive, onRefresh }: { directive: DirectiveWithProgress; onRefresh: () => void }) { + if (directive.pendingApprovals.length === 0) { + return ( + <div className="text-center py-8"> + <p className="font-mono text-sm text-[#556677]">No pending approvals</p> + </div> + ); + } + + const handleApprove = async (approvalId: string) => { + try { + const { approveDirectiveRequest } = await import("../../lib/api"); + await approveDirectiveRequest(directive.id, approvalId); + onRefresh(); + } catch (err) { + console.error("Failed to approve:", err); + } + }; + + const handleDeny = async (approvalId: string) => { + try { + const { denyDirectiveRequest } = await import("../../lib/api"); + await denyDirectiveRequest(directive.id, approvalId); + onRefresh(); + } catch (err) { + console.error("Failed to deny:", err); + } + }; + + return ( + <div className="space-y-3"> + {directive.pendingApprovals.map((approval) => { + const urgencyColor = { + low: "text-[#556677]", + normal: "text-[#75aafc]", + high: "text-yellow-400", + critical: "text-red-400", + }[approval.urgency] || "text-[#556677]"; + + return ( + <div + key={approval.id} + className="p-4 bg-[rgba(117,170,252,0.05)] border border-[rgba(117,170,252,0.2)]" + > + <div className="flex items-start justify-between"> + <div> + <div className="flex items-center gap-2"> + <span className="font-mono text-sm text-[#dbe7ff]">{approval.approvalType}</span> + <span className={`font-mono text-[10px] uppercase ${urgencyColor}`}> + {approval.urgency} + </span> + </div> + <p className="font-mono text-xs text-[#9bc3ff] mt-1">{approval.description}</p> + </div> + <div className="flex gap-2"> + <button + onClick={() => handleApprove(approval.id)} + className="px-3 py-1 font-mono text-[10px] text-[#dbe7ff] bg-green-700 border border-green-600 hover:bg-green-600 uppercase" + > + Approve + </button> + <button + onClick={() => handleDeny(approval.id)} + className="px-3 py-1 font-mono text-[10px] text-[#dbe7ff] bg-red-700 border border-red-600 hover:bg-red-600 uppercase" + > + Deny + </button> + </div> + </div> + </div> + ); + })} + </div> + ); +} diff --git a/makima/frontend/src/components/directives/ChainTab.tsx b/makima/frontend/src/components/directives/ChainTab.tsx new file mode 100644 index 0000000..ccefe81 --- /dev/null +++ b/makima/frontend/src/components/directives/ChainTab.tsx @@ -0,0 +1,212 @@ +import { useState, useMemo } from "react"; +import { + ReactFlow, + Edge, + Controls, + Background, + BackgroundVariant, + MarkerType, +} from "@xyflow/react"; +import "@xyflow/react/dist/style.css"; +import type { DirectiveWithProgress, DirectiveGraphResponse } from "../../lib/api"; +import { StepNodeComponent, stepStatusStyles } from "./StepNode"; + +// Node types for React Flow +const nodeTypes = { + step: StepNodeComponent, +}; + +export function ChainTab({ directive, graph }: { directive: DirectiveWithProgress; graph: DirectiveGraphResponse | null }) { + const [viewMode, setViewMode] = useState<"dag" | "list">("dag"); + + // Convert graph to React Flow nodes and edges + const { nodes, edges } = useMemo(() => { + if (!graph || !graph.nodes.length) { + // Fallback: generate positions from directive.steps + const stepNodes = directive.steps.map((step, index) => ({ + id: step.id, + type: "step" as const, + position: { + x: (index % 3) * 220 + 50, + y: Math.floor(index / 3) * 120 + 50, + }, + data: { + id: step.id, + name: step.name, + stepType: step.stepType, + status: step.status, + confidenceScore: step.confidenceScore, + confidenceLevel: step.confidenceLevel, + contractId: step.contractId, + editorX: null, + editorY: null, + }, + })); + + // Build edges from dependencies + const stepEdges: Edge[] = []; + directive.steps.forEach((step) => { + (step.dependsOn ?? []).forEach((depName) => { + const depStep = directive.steps.find((s) => s.name === depName); + if (depStep) { + stepEdges.push({ + id: `${depStep.id}-${step.id}`, + source: depStep.id, + target: step.id, + type: "smoothstep", + markerEnd: { type: MarkerType.ArrowClosed, color: "#556677" }, + style: { stroke: "#556677", strokeWidth: 2 }, + }); + } + }); + }); + + return { nodes: stepNodes, edges: stepEdges }; + } + + // Use graph data + const graphNodes = graph.nodes.map((node) => ({ + id: node.id, + type: "step" as const, + position: { + x: node.editorX ?? 50, + y: node.editorY ?? 50, + }, + data: { ...node }, + })); + + const graphEdges: Edge[] = graph.edges.map((edge) => ({ + id: `${edge.source}-${edge.target}`, + source: edge.source, + target: edge.target, + type: "smoothstep", + markerEnd: { type: MarkerType.ArrowClosed, color: "#556677" }, + style: { stroke: "#556677", strokeWidth: 2 }, + })); + + return { nodes: graphNodes, edges: graphEdges }; + }, [graph, directive.steps]); + + if (!directive.chain) { + return ( + <div className="text-center py-8"> + <p className="font-mono text-sm text-[#556677]"> + No chain generated yet. Start the directive to generate a chain. + </p> + </div> + ); + } + + return ( + <div className="space-y-4"> + {/* Chain info header */} + <div className="flex items-center justify-between p-3 bg-[rgba(117,170,252,0.05)] border border-[rgba(117,170,252,0.15)]"> + <div> + <div className="font-mono text-sm text-[#dbe7ff]">{directive.chain.name}</div> + <div className="font-mono text-[10px] text-[#556677]">Generation {directive.chain.generation}</div> + </div> + <div className="flex items-center gap-4"> + <div className="font-mono text-xs text-[#556677]"> + {directive.chain.completedSteps}/{directive.chain.totalSteps} steps + </div> + {/* View toggle */} + <div className="flex border border-[rgba(117,170,252,0.2)]"> + <button + onClick={() => setViewMode("dag")} + className={`px-2 py-1 font-mono text-[10px] uppercase ${ + viewMode === "dag" + ? "bg-[rgba(117,170,252,0.2)] text-[#75aafc]" + : "text-[#556677] hover:text-[#75aafc]" + }`} + > + DAG + </button> + <button + onClick={() => setViewMode("list")} + className={`px-2 py-1 font-mono text-[10px] uppercase ${ + viewMode === "list" + ? "bg-[rgba(117,170,252,0.2)] text-[#75aafc]" + : "text-[#556677] hover:text-[#75aafc]" + }`} + > + List + </button> + </div> + </div> + </div> + + {viewMode === "dag" ? ( + /* DAG visualization */ + <div className="h-[400px] border border-[rgba(117,170,252,0.1)] rounded bg-[#050d18]"> + {directive.steps.length === 0 ? ( + <div className="flex items-center justify-center h-full"> + <p className="font-mono text-sm text-[#556677]">No steps in chain</p> + </div> + ) : ( + <ReactFlow + nodes={nodes} + edges={edges} + nodeTypes={nodeTypes} + fitView + fitViewOptions={{ padding: 0.2 }} + minZoom={0.5} + maxZoom={1.5} + defaultEdgeOptions={{ + type: "smoothstep", + style: { stroke: "#556677", strokeWidth: 2 }, + }} + > + <Background variant={BackgroundVariant.Dots} gap={20} size={1} color="#1a2a3a" /> + <Controls className="!bg-[#0a1628] !border-[rgba(117,170,252,0.2)]" /> + </ReactFlow> + )} + </div> + ) : ( + /* List view */ + <div className="space-y-2"> + <h3 className="font-mono text-xs text-[#75aafc] uppercase">Steps</h3> + {directive.steps.length === 0 ? ( + <p className="font-mono text-sm text-[#556677]">No steps in chain</p> + ) : ( + directive.steps.map((step) => { + const styles = stepStatusStyles[step.status] || stepStatusStyles.pending; + + return ( + <div + key={step.id} + className="p-3 bg-[rgba(117,170,252,0.02)] border border-[rgba(117,170,252,0.1)]" + > + <div className="flex items-center justify-between"> + <div className="flex items-center gap-2"> + <span className="font-mono text-sm text-[#dbe7ff]">{step.name}</span> + <span + className="px-1.5 py-0.5 font-mono text-[10px] uppercase border" + style={{ color: styles.text, borderColor: `${styles.border}50` }} + > + {step.status} + </span> + {step.confidenceScore !== null && ( + <span className="font-mono text-[10px] text-[#556677]"> + ({Math.round(step.confidenceScore * 100)}%) + </span> + )} + </div> + <div className="font-mono text-[10px] text-[#556677]">{step.stepType}</div> + </div> + {step.description && ( + <p className="font-mono text-xs text-[#556677] mt-1">{step.description}</p> + )} + {step.dependsOn?.length > 0 && ( + <div className="font-mono text-[10px] text-[#556677] mt-1"> + Depends on: {step.dependsOn.join(", ")} + </div> + )} + </div> + ); + }) + )} + </div> + )} + </div> + ); +} diff --git a/makima/frontend/src/components/directives/CreateDirectiveModal.tsx b/makima/frontend/src/components/directives/CreateDirectiveModal.tsx new file mode 100644 index 0000000..7f52a7e --- /dev/null +++ b/makima/frontend/src/components/directives/CreateDirectiveModal.tsx @@ -0,0 +1,146 @@ +import { useState, useEffect } from "react"; +import type { AutonomyLevel, RepositoryHistoryEntry } from "../../lib/api"; +import { getRepositorySuggestions } from "../../lib/api"; + +interface CreateDirectiveModalProps { + onSubmit: (goal: string, repositoryUrl: string | undefined, autonomyLevel: AutonomyLevel) => void; + onCancel: () => void; +} + +export function CreateDirectiveModal({ onSubmit, onCancel }: CreateDirectiveModalProps) { + const [goal, setGoal] = useState(""); + const [repositoryUrl, setRepositoryUrl] = useState(""); + const [autonomyLevel, setAutonomyLevel] = useState<AutonomyLevel>("guardrails"); + const [suggestions, setSuggestions] = useState<RepositoryHistoryEntry[]>([]); + const [showSuggestions, setShowSuggestions] = useState(false); + + // Load suggestions + useEffect(() => { + getRepositorySuggestions("remote", undefined, 5) + .then((res) => { + setSuggestions(res.entries); + }) + .catch(() => { + setSuggestions([]); + }); + }, []); + + const handleSubmit = () => { + if (goal.trim()) { + onSubmit(goal.trim(), repositoryUrl.trim() || undefined, autonomyLevel); + } + }; + + return ( + <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"> + <div className="w-full max-w-lg p-6 bg-[#0a1628] border border-[rgba(117,170,252,0.3)] max-h-[90vh] overflow-y-auto"> + <h3 className="font-mono text-sm text-[#75aafc] uppercase mb-4"> + Create Directive + </h3> + + <div className="space-y-4"> + {/* Goal */} + <div> + <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1"> + Goal * + </label> + <textarea + value={goal} + onChange={(e) => setGoal(e.target.value)} + placeholder="Describe what you want to accomplish..." + rows={3} + className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc] resize-none" + autoFocus + /> + </div> + + {/* Repository URL */} + <div> + <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1"> + Repository URL (optional) + </label> + <div className="relative"> + <input + type="text" + value={repositoryUrl} + onChange={(e) => setRepositoryUrl(e.target.value)} + onFocus={() => suggestions.length > 0 && setShowSuggestions(true)} + onBlur={() => setTimeout(() => setShowSuggestions(false), 200)} + placeholder="https://github.com/owner/repo" + className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]" + /> + {showSuggestions && suggestions.length > 0 && ( + <div className="absolute top-full left-0 right-0 mt-1 border border-[rgba(117,170,252,0.2)] bg-[#0a1525] max-h-32 overflow-y-auto z-10"> + {suggestions.map((s) => ( + <button + key={s.id} + type="button" + onClick={() => { + setRepositoryUrl(s.repositoryUrl || ""); + setShowSuggestions(false); + }} + 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="text-[#9bc3ff] truncate">{s.name}</div> + <div className="text-[10px] text-[#556677] truncate">{s.repositoryUrl}</div> + </button> + ))} + </div> + )} + </div> + </div> + + {/* Autonomy Level */} + <div> + <label className="block font-mono text-xs text-[#8b949e] uppercase mb-2"> + Autonomy Level + </label> + <div className="flex gap-2"> + {(["full_auto", "guardrails", "manual"] as const).map((level) => ( + <button + key={level} + type="button" + onClick={() => setAutonomyLevel(level)} + className={`flex-1 px-3 py-2 font-mono text-xs uppercase ${ + autonomyLevel === level + ? "text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3]" + : "text-[#556677] border border-[rgba(117,170,252,0.2)] hover:border-[#3f6fb3]" + }`} + > + {level.replace("_", " ")} + </button> + ))} + </div> + <p className="font-mono text-[10px] text-[#556677] mt-1"> + {autonomyLevel === "full_auto" && "Automatic progression without approval gates"} + {autonomyLevel === "guardrails" && "Request approval for yellow/red confidence scores"} + {autonomyLevel === "manual" && "Request approval for all step completions"} + </p> + </div> + + <p className="font-mono text-xs text-[#8b949e]"> + A directive is a top-level goal that generates a chain of steps. Each step spawns + contracts that are verified before progression. + </p> + + {/* Actions */} + <div className="flex gap-2 justify-end pt-2"> + <button + onClick={onCancel} + className="px-4 py-2 font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors" + > + Cancel + </button> + <button + onClick={handleSubmit} + disabled={!goal.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> + ); +} diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx new file mode 100644 index 0000000..06b24bb --- /dev/null +++ b/makima/frontend/src/components/directives/DirectiveDetail.tsx @@ -0,0 +1,160 @@ +import { useState } from "react"; +import type { DirectiveWithProgress, DirectiveGraphResponse } from "../../lib/api"; +import { OverviewTab } from "./OverviewTab"; +import { ChainTab } from "./ChainTab"; +import { EventsTab } from "./EventsTab"; +import { EvaluationsTab } from "./EvaluationsTab"; +import { ApprovalsTab } from "./ApprovalsTab"; +import { VerifiersTab } from "./VerifiersTab"; + +interface DirectiveDetailProps { + directive: DirectiveWithProgress; + graph: DirectiveGraphResponse | null; + loading: boolean; + onBack: () => void; + onRefresh: () => void; + onStart: () => void; + onPause: () => void; + onResume: () => void; + onStop: () => void; +} + +export function DirectiveDetail({ + directive, + graph, + loading, + onBack, + onRefresh, + onStart, + onPause, + onResume, + onStop, +}: DirectiveDetailProps) { + const [activeTab, setActiveTab] = useState<"overview" | "chain" | "events" | "evaluations" | "approvals" | "verifiers">("overview"); + + if (loading) { + return ( + <div className="panel h-full flex items-center justify-center"> + <p className="font-mono text-xs text-[#556677]">Loading...</p> + </div> + ); + } + + const statusColor = { + draft: "text-[#556677] bg-[#556677]/10 border-[#556677]/30", + planning: "text-yellow-400 bg-yellow-400/10 border-yellow-400/30", + active: "text-green-400 bg-green-400/10 border-green-400/30", + paused: "text-yellow-400 bg-yellow-400/10 border-yellow-400/30", + completed: "text-[#75aafc] bg-[#75aafc]/10 border-[#75aafc]/30", + archived: "text-[#556677] bg-[#556677]/10 border-[#556677]/30", + failed: "text-red-400 bg-red-400/10 border-red-400/30", + }[directive.status] || "text-[#556677] bg-[#556677]/10 border-[#556677]/30"; + + return ( + <div className="panel h-full flex flex-col overflow-hidden"> + {/* Header */} + <div className="p-3 border-b border-[rgba(117,170,252,0.15)]"> + <div className="flex items-center justify-between"> + <div className="flex items-center gap-3"> + <button + onClick={onBack} + className="font-mono text-xs text-[#556677] hover:text-[#9bc3ff]" + > + ← Back + </button> + <h2 className="font-mono text-sm text-[#dbe7ff]"> + {directive.title || directive.goal.slice(0, 50)} + </h2> + <span className={`px-2 py-0.5 font-mono text-[10px] uppercase border ${statusColor}`}> + {directive.status} + </span> + </div> + <div className="flex items-center gap-2"> + {directive.status === "draft" && ( + <button + onClick={onStart} + className="px-3 py-1 font-mono text-[10px] text-[#dbe7ff] bg-green-700 border border-green-600 hover:bg-green-600 uppercase" + > + Start + </button> + )} + {directive.status === "active" && ( + <button + onClick={onPause} + className="px-3 py-1 font-mono text-[10px] text-[#dbe7ff] bg-yellow-700 border border-yellow-600 hover:bg-yellow-600 uppercase" + > + Pause + </button> + )} + {directive.status === "paused" && ( + <button + onClick={onResume} + className="px-3 py-1 font-mono text-[10px] text-[#dbe7ff] bg-green-700 border border-green-600 hover:bg-green-600 uppercase" + > + Resume + </button> + )} + {["active", "paused"].includes(directive.status) && ( + <button + onClick={onStop} + className="px-3 py-1 font-mono text-[10px] text-[#dbe7ff] bg-red-700 border border-red-600 hover:bg-red-600 uppercase" + > + Stop + </button> + )} + <button + onClick={onRefresh} + className="px-3 py-1 font-mono text-[10px] text-[#9bc3ff] hover:text-[#dbe7ff]" + > + Refresh + </button> + </div> + </div> + </div> + + {/* Tabs */} + <div className="flex gap-1 p-2 border-b border-[rgba(117,170,252,0.1)]"> + {(["overview", "chain", "events", "evaluations", "approvals", "verifiers"] as const).map((tab) => ( + <button + key={tab} + onClick={() => setActiveTab(tab)} + className={`px-3 py-1.5 font-mono text-[10px] uppercase ${ + activeTab === tab + ? "text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3]" + : "text-[#556677] hover:text-[#9bc3ff]" + }`} + > + {tab} + {tab === "approvals" && directive.pendingApprovals.length > 0 && ( + <span className="ml-1 px-1 bg-yellow-500 text-black rounded text-[8px]"> + {directive.pendingApprovals.length} + </span> + )} + </button> + ))} + </div> + + {/* Tab Content */} + <div className="flex-1 overflow-y-auto p-4"> + {activeTab === "overview" && ( + <OverviewTab directive={directive} /> + )} + {activeTab === "chain" && ( + <ChainTab directive={directive} graph={graph} /> + )} + {activeTab === "events" && ( + <EventsTab directive={directive} /> + )} + {activeTab === "evaluations" && ( + <EvaluationsTab directive={directive} /> + )} + {activeTab === "approvals" && ( + <ApprovalsTab directive={directive} onRefresh={onRefresh} /> + )} + {activeTab === "verifiers" && ( + <VerifiersTab directive={directive} /> + )} + </div> + </div> + ); +} diff --git a/makima/frontend/src/components/directives/DirectiveList.tsx b/makima/frontend/src/components/directives/DirectiveList.tsx new file mode 100644 index 0000000..d0371e0 --- /dev/null +++ b/makima/frontend/src/components/directives/DirectiveList.tsx @@ -0,0 +1,85 @@ +import { useState } from "react"; +import type { DirectiveSummary } from "../../lib/api"; +import { DirectiveListItem } from "./DirectiveListItem"; + +interface DirectiveListProps { + directives: DirectiveSummary[]; + loading: boolean; + onSelect: (id: string) => void; + onCreate: () => void; + selectedId?: string; + onArchive: (directive: DirectiveSummary) => void; +} + +export function DirectiveList({ + directives, + loading, + onSelect, + onCreate, + selectedId, + onArchive, +}: DirectiveListProps) { + const [filter, setFilter] = useState<"all" | "active" | "completed" | "failed">("all"); + + const filteredDirectives = directives.filter((d) => { + if (filter === "all") return true; + if (filter === "active") return ["draft", "planning", "active", "paused"].includes(d.status); + if (filter === "completed") return d.status === "completed"; + if (filter === "failed") return d.status === "failed"; + return true; + }); + + return ( + <div className="panel h-full flex flex-col overflow-hidden"> + <div className="flex items-center justify-between p-3 border-b border-[rgba(117,170,252,0.15)]"> + <h2 className="font-mono text-sm text-[#75aafc] uppercase">Directives</h2> + <button + onClick={onCreate} + className="px-3 py-1 font-mono text-[10px] text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase" + > + + New + </button> + </div> + + {/* Filters */} + <div className="flex gap-1 p-2 border-b border-[rgba(117,170,252,0.1)]"> + {(["all", "active", "completed", "failed"] as const).map((f) => ( + <button + key={f} + onClick={() => setFilter(f)} + className={`px-2 py-1 font-mono text-[10px] uppercase ${ + filter === f + ? "text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3]" + : "text-[#556677] hover:text-[#9bc3ff]" + }`} + > + {f} + </button> + ))} + </div> + + {/* List */} + <div className="flex-1 overflow-y-auto"> + {loading ? ( + <div className="p-4 text-center"> + <p className="font-mono text-xs text-[#556677]">Loading...</p> + </div> + ) : filteredDirectives.length === 0 ? ( + <div className="p-4 text-center"> + <p className="font-mono text-xs text-[#556677]">No directives found</p> + </div> + ) : ( + filteredDirectives.map((d) => ( + <DirectiveListItem + key={d.id} + directive={d} + selected={d.id === selectedId} + onClick={() => onSelect(d.id)} + onArchive={() => onArchive(d)} + /> + )) + )} + </div> + </div> + ); +} diff --git a/makima/frontend/src/components/directives/DirectiveListItem.tsx b/makima/frontend/src/components/directives/DirectiveListItem.tsx new file mode 100644 index 0000000..6ff82e4 --- /dev/null +++ b/makima/frontend/src/components/directives/DirectiveListItem.tsx @@ -0,0 +1,83 @@ +import type { DirectiveSummary } from "../../lib/api"; + +interface DirectiveListItemProps { + directive: DirectiveSummary; + selected: boolean; + onClick: () => void; + onArchive: () => void; +} + +export function DirectiveListItem({ directive, selected, onClick, onArchive }: DirectiveListItemProps) { + const progress = directive.totalSteps > 0 + ? Math.round((directive.completedSteps / directive.totalSteps) * 100) + : 0; + + const statusColor = { + draft: "text-[#556677]", + planning: "text-yellow-400", + active: "text-green-400", + paused: "text-yellow-400", + completed: "text-[#75aafc]", + archived: "text-[#556677]", + failed: "text-red-400", + }[directive.status] || "text-[#556677]"; + + const confidenceColor = { + green: "bg-green-500", + yellow: "bg-yellow-500", + red: "bg-red-500", + }[directive.currentConfidence !== null && directive.currentConfidence >= 0.8 + ? "green" + : directive.currentConfidence !== null && directive.currentConfidence >= 0.5 + ? "yellow" + : "red"] || "bg-[#556677]"; + + return ( + <div + onClick={onClick} + className={`p-3 cursor-pointer border-b border-[rgba(117,170,252,0.1)] hover:bg-[rgba(117,170,252,0.05)] ${ + selected ? "bg-[rgba(117,170,252,0.1)]" : "" + }`} + > + <div className="flex items-start justify-between gap-2"> + <div className="flex-1 min-w-0"> + <div className="font-mono text-sm text-[#dbe7ff] truncate"> + {directive.title || directive.goal.slice(0, 50)} + </div> + <div className="flex items-center gap-2 mt-1"> + <span className={`font-mono text-[10px] uppercase ${statusColor}`}> + {directive.status} + </span> + <span className="font-mono text-[10px] text-[#556677]"> + {directive.completedSteps}/{directive.totalSteps} steps + </span> + </div> + </div> + <div className="flex flex-col items-end gap-1"> + {directive.currentConfidence !== null && ( + <div className={`w-2 h-2 rounded-full ${confidenceColor}`} title={`Confidence: ${Math.round(directive.currentConfidence * 100)}%`} /> + )} + <button + onClick={(e) => { + e.stopPropagation(); + onArchive(); + }} + className="font-mono text-[10px] text-[#556677] hover:text-red-400" + > + Archive + </button> + </div> + </div> + + {/* Progress bar */} + {directive.totalSteps > 0 && ( + <div className="mt-2 h-1 bg-[rgba(117,170,252,0.1)] overflow-hidden"> + <div + className="h-full bg-[#75aafc] transition-all duration-300" + style={{ width: `${progress}%` }} + /> + </div> + )} + </div> + ); +} diff --git a/makima/frontend/src/components/directives/EvaluationsTab.tsx b/makima/frontend/src/components/directives/EvaluationsTab.tsx new file mode 100644 index 0000000..c1d65db --- /dev/null +++ b/makima/frontend/src/components/directives/EvaluationsTab.tsx @@ -0,0 +1,12 @@ +import type { DirectiveWithProgress } from "../../lib/api"; + +export function EvaluationsTab({ directive: _directive }: { directive: DirectiveWithProgress }) { + // TODO: Fetch evaluations separately + return ( + <div className="text-center py-8"> + <p className="font-mono text-sm text-[#556677]"> + Evaluations will be shown here after steps are evaluated + </p> + </div> + ); +} diff --git a/makima/frontend/src/components/directives/EventsTab.tsx b/makima/frontend/src/components/directives/EventsTab.tsx new file mode 100644 index 0000000..4dd739a --- /dev/null +++ b/makima/frontend/src/components/directives/EventsTab.tsx @@ -0,0 +1,77 @@ +import { useMemo } from "react"; +import type { DirectiveWithProgress } from "../../lib/api"; +import { useDirectiveEventSubscription } from "../../hooks/useDirectives"; + +export function EventsTab({ directive }: { directive: DirectiveWithProgress }) { + // Subscribe to real-time events via SSE + const { events: streamEvents, isConnected, error: sseError } = useDirectiveEventSubscription(directive.id); + + // Combine initial events with streamed events (avoiding duplicates) + const allEvents = useMemo(() => { + const eventMap = new Map(); + // Add initial events first + directive.recentEvents.forEach((e) => eventMap.set(e.id, e)); + // Add streamed events (will override any duplicates) + streamEvents.forEach((e) => eventMap.set(e.id, e)); + // Sort by created_at descending (most recent first) + return Array.from(eventMap.values()).sort( + (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + ); + }, [directive.recentEvents, streamEvents]); + + return ( + <div className="space-y-4"> + {/* Connection status */} + <div className="flex items-center justify-between text-[10px] font-mono"> + <div className="flex items-center gap-2"> + <span className={isConnected ? "text-green-400" : "text-[#556677]"}> + {isConnected ? "\u25CF Live" : "\u25CB Connecting..."} + </span> + {sseError && <span className="text-red-400">{sseError}</span>} + </div> + <span className="text-[#556677]">{allEvents.length} events</span> + </div> + + {/* Event list */} + {allEvents.length === 0 ? ( + <div className="text-center py-8"> + <p className="font-mono text-sm text-[#556677]">No events yet</p> + </div> + ) : ( + <div className="space-y-2"> + {allEvents.map((event) => { + const severityColors: Record<string, string> = { + info: "text-[#75aafc]", + warning: "text-yellow-400", + error: "text-red-400", + critical: "text-red-600", + }; + const severityColor = severityColors[event.severity] || "text-[#556677]"; + + return ( + <div + key={event.id} + className="p-3 bg-[rgba(117,170,252,0.02)] border border-[rgba(117,170,252,0.1)]" + > + <div className="flex items-center justify-between"> + <div className="flex items-center gap-2"> + <span className={`font-mono text-xs ${severityColor}`}>{event.eventType}</span> + <span className="font-mono text-[10px] text-[#556677]">{event.actorType}</span> + </div> + <span className="font-mono text-[10px] text-[#556677]"> + {new Date(event.createdAt).toLocaleString()} + </span> + </div> + {event.eventData != null && ( + <pre className="font-mono text-[10px] text-[#556677] mt-1 overflow-x-auto"> + {JSON.stringify(event.eventData, null, 2)} + </pre> + )} + </div> + ); + })} + </div> + )} + </div> + ); +} diff --git a/makima/frontend/src/components/directives/OverviewTab.tsx b/makima/frontend/src/components/directives/OverviewTab.tsx new file mode 100644 index 0000000..41cd7dc --- /dev/null +++ b/makima/frontend/src/components/directives/OverviewTab.tsx @@ -0,0 +1,73 @@ +import type { DirectiveWithProgress } from "../../lib/api"; + +export function OverviewTab({ directive }: { directive: DirectiveWithProgress }) { + return ( + <div className="space-y-6"> + {/* Goal */} + <div> + <h3 className="font-mono text-xs text-[#75aafc] uppercase mb-2">Goal</h3> + <p className="font-mono text-sm text-[#dbe7ff] whitespace-pre-wrap"> + {directive.goal} + </p> + </div> + + {/* Progress */} + <div> + <h3 className="font-mono text-xs text-[#75aafc] uppercase mb-2">Progress</h3> + <div className="grid grid-cols-3 gap-4"> + <div className="p-3 bg-[rgba(117,170,252,0.05)] border border-[rgba(117,170,252,0.15)]"> + <div className="font-mono text-2xl text-[#dbe7ff]"> + {directive.chain?.completedSteps || 0} + </div> + <div className="font-mono text-[10px] text-[#556677] uppercase">Completed Steps</div> + </div> + <div className="p-3 bg-[rgba(117,170,252,0.05)] border border-[rgba(117,170,252,0.15)]"> + <div className="font-mono text-2xl text-[#dbe7ff]"> + {directive.chain?.totalSteps || 0} + </div> + <div className="font-mono text-[10px] text-[#556677] uppercase">Total Steps</div> + </div> + <div className="p-3 bg-[rgba(117,170,252,0.05)] border border-[rgba(117,170,252,0.15)]"> + <div className="font-mono text-2xl text-[#dbe7ff]"> + {directive.chain?.currentConfidence != null + ? `${Math.round((directive.chain?.currentConfidence ?? 0) * 100)}%` + : "-"} + </div> + <div className="font-mono text-[10px] text-[#556677] uppercase">Confidence</div> + </div> + </div> + </div> + + {/* Configuration */} + <div> + <h3 className="font-mono text-xs text-[#75aafc] uppercase mb-2">Configuration</h3> + <div className="grid grid-cols-2 gap-2 text-sm"> + <div className="flex justify-between"> + <span className="font-mono text-[#556677]">Autonomy Level</span> + <span className="font-mono text-[#dbe7ff]">{directive.autonomyLevel}</span> + </div> + <div className="flex justify-between"> + <span className="font-mono text-[#556677]">Max Rework Cycles</span> + <span className="font-mono text-[#dbe7ff]">{directive.maxReworkCycles}</span> + </div> + <div className="flex justify-between"> + <span className="font-mono text-[#556677]">Green Threshold</span> + <span className="font-mono text-[#dbe7ff]">{directive.confidenceThresholdGreen}</span> + </div> + <div className="flex justify-between"> + <span className="font-mono text-[#556677]">Yellow Threshold</span> + <span className="font-mono text-[#dbe7ff]">{directive.confidenceThresholdYellow}</span> + </div> + </div> + </div> + + {/* Repository */} + {directive.repositoryUrl && ( + <div> + <h3 className="font-mono text-xs text-[#75aafc] uppercase mb-2">Repository</h3> + <p className="font-mono text-sm text-[#9bc3ff]">{directive.repositoryUrl}</p> + </div> + )} + </div> + ); +} diff --git a/makima/frontend/src/components/directives/StepNode.tsx b/makima/frontend/src/components/directives/StepNode.tsx new file mode 100644 index 0000000..e54f5eb --- /dev/null +++ b/makima/frontend/src/components/directives/StepNode.tsx @@ -0,0 +1,87 @@ +import { Handle, Position } from "@xyflow/react"; +import type { StepStatus, ConfidenceLevel, DirectiveGraphNode } from "../../lib/api"; + +// Step status colors for both list and DAG views +export const stepStatusStyles: Record<StepStatus, { border: string; bg: string; text: string }> = { + pending: { border: "#556677", bg: "#556677", text: "#556677" }, + ready: { border: "#3b82f6", bg: "#3b82f6", text: "#3b82f6" }, + running: { border: "#22c55e", bg: "#22c55e", text: "#22c55e" }, + evaluating: { border: "#eab308", bg: "#eab308", text: "#eab308" }, + passed: { border: "#75aafc", bg: "#75aafc", text: "#75aafc" }, + failed: { border: "#ef4444", bg: "#ef4444", text: "#ef4444" }, + rework: { border: "#f97316", bg: "#f97316", text: "#f97316" }, + skipped: { border: "#556677", bg: "#556677", text: "#556677" }, + blocked: { border: "#ef4444", bg: "#ef4444", text: "#ef4444" }, +}; + +// Confidence level colors +export const confidenceColors: Record<ConfidenceLevel, string> = { + green: "#22c55e", + yellow: "#eab308", + red: "#ef4444", +}; + +// Node dimensions +export const NODE_WIDTH = 180; +export const NODE_HEIGHT = 70; + +// Custom node component for steps +export function StepNodeComponent({ data }: { data: DirectiveGraphNode & { selected?: boolean } }) { + const styles = stepStatusStyles[data.status] || stepStatusStyles.pending; + const isRunning = data.status === "running" || data.status === "evaluating"; + + return ( + <div + className={`rounded-lg border-2 bg-[#0a1628] overflow-hidden ${ + isRunning ? "animate-pulse" : "" + }`} + style={{ + width: NODE_WIDTH, + height: NODE_HEIGHT, + borderColor: styles.border, + borderStyle: data.status === "pending" ? "dashed" : "solid", + }} + > + <Handle + type="target" + position={Position.Top} + className="!bg-[#75aafc] !w-3 !h-3 !border-2 !border-[#0a1628]" + /> + + {/* Status indicator bar */} + <div className="h-1.5" style={{ backgroundColor: styles.bg }} /> + + {/* Content */} + <div className="p-2"> + <div className="flex items-center justify-between mb-1"> + <span className="font-mono text-xs text-[#dbe7ff] truncate flex-1">{data.name}</span> + {data.confidenceScore !== null && data.confidenceLevel && ( + <div + className="w-2 h-2 rounded-full flex-shrink-0 ml-1" + style={{ backgroundColor: confidenceColors[data.confidenceLevel] }} + title={`Confidence: ${Math.round(data.confidenceScore * 100)}%`} + /> + )} + </div> + <div className="flex items-center justify-between"> + <span + className="font-mono text-[10px] uppercase px-1.5 py-0.5 rounded" + style={{ + color: styles.text, + backgroundColor: `${styles.bg}20`, + }} + > + {data.status} + </span> + <span className="font-mono text-[10px] text-[#8b949e]">{data.stepType}</span> + </div> + </div> + + <Handle + type="source" + position={Position.Bottom} + className="!bg-[#f59e0b] !w-3 !h-3 !border-2 !border-[#0a1628]" + /> + </div> + ); +} diff --git a/makima/frontend/src/components/directives/VerifiersTab.tsx b/makima/frontend/src/components/directives/VerifiersTab.tsx new file mode 100644 index 0000000..cfcfdd8 --- /dev/null +++ b/makima/frontend/src/components/directives/VerifiersTab.tsx @@ -0,0 +1,12 @@ +import type { DirectiveWithProgress } from "../../lib/api"; + +export function VerifiersTab({ directive: _directive }: { directive: DirectiveWithProgress }) { + // TODO: Fetch verifiers separately + return ( + <div className="text-center py-8"> + <p className="font-mono text-sm text-[#556677]"> + Verifiers will be shown here. Use auto-detect to find available verifiers. + </p> + </div> + ); +} diff --git a/makima/frontend/src/components/directives/index.ts b/makima/frontend/src/components/directives/index.ts new file mode 100644 index 0000000..718b1f2 --- /dev/null +++ b/makima/frontend/src/components/directives/index.ts @@ -0,0 +1,11 @@ +export { DirectiveList } from "./DirectiveList"; +export { DirectiveListItem } from "./DirectiveListItem"; +export { DirectiveDetail } from "./DirectiveDetail"; +export { OverviewTab } from "./OverviewTab"; +export { ChainTab } from "./ChainTab"; +export { EventsTab } from "./EventsTab"; +export { EvaluationsTab } from "./EvaluationsTab"; +export { ApprovalsTab } from "./ApprovalsTab"; +export { VerifiersTab } from "./VerifiersTab"; +export { CreateDirectiveModal } from "./CreateDirectiveModal"; +export { StepNodeComponent, stepStatusStyles, confidenceColors, NODE_WIDTH, NODE_HEIGHT } from "./StepNode"; diff --git a/makima/frontend/src/hooks/useDirectiveDetail.ts b/makima/frontend/src/hooks/useDirectiveDetail.ts new file mode 100644 index 0000000..1167242 --- /dev/null +++ b/makima/frontend/src/hooks/useDirectiveDetail.ts @@ -0,0 +1,125 @@ +import { useState, useCallback, useEffect } from "react"; +import { + getDirective, + getDirectiveGraph, + startDirective, + pauseDirective, + resumeDirective, + stopDirective, + type DirectiveWithProgress, + type DirectiveGraphResponse, + type StartDirectiveResponse, +} from "../lib/api"; + +interface UseDirectiveDetailResult { + directive: DirectiveWithProgress | null; + graph: DirectiveGraphResponse | null; + loading: boolean; + error: string | null; + refresh: () => Promise<void>; + start: () => Promise<StartDirectiveResponse | null>; + pause: () => Promise<boolean>; + resume: () => Promise<boolean>; + stop: () => Promise<boolean>; +} + +export function useDirectiveDetail(directiveId: string | undefined): UseDirectiveDetailResult { + const [directive, setDirective] = useState<DirectiveWithProgress | null>(null); + const [graph, setGraph] = useState<DirectiveGraphResponse | null>(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState<string | null>(null); + + const fetchDetail = useCallback(async () => { + if (!directiveId) { + setDirective(null); + setGraph(null); + return; + } + + setLoading(true); + setError(null); + try { + const [d, g] = await Promise.all([ + getDirective(directiveId), + getDirectiveGraph(directiveId).catch(() => null), + ]); + setDirective(d); + setGraph(g); + } catch (err) { + console.error("Failed to fetch directive detail:", err); + setError(err instanceof Error ? err.message : "Failed to fetch directive"); + setDirective(null); + setGraph(null); + } finally { + setLoading(false); + } + }, [directiveId]); + + useEffect(() => { + fetchDetail(); + }, [fetchDetail]); + + const start = useCallback(async (): Promise<StartDirectiveResponse | null> => { + if (!directiveId) return null; + try { + const response = await startDirective(directiveId); + await fetchDetail(); + return response; + } catch (err) { + console.error("Failed to start directive:", err); + setError(err instanceof Error ? err.message : "Failed to start directive"); + return null; + } + }, [directiveId, fetchDetail]); + + const pause = useCallback(async (): Promise<boolean> => { + if (!directiveId) return false; + try { + await pauseDirective(directiveId); + await fetchDetail(); + return true; + } catch (err) { + console.error("Failed to pause directive:", err); + setError(err instanceof Error ? err.message : "Failed to pause directive"); + return false; + } + }, [directiveId, fetchDetail]); + + const resume = useCallback(async (): Promise<boolean> => { + if (!directiveId) return false; + try { + await resumeDirective(directiveId); + await fetchDetail(); + return true; + } catch (err) { + console.error("Failed to resume directive:", err); + setError(err instanceof Error ? err.message : "Failed to resume directive"); + return false; + } + }, [directiveId, fetchDetail]); + + const stop = useCallback(async (): Promise<boolean> => { + if (!directiveId) return false; + try { + await stopDirective(directiveId); + await fetchDetail(); + return true; + } catch (err) { + console.error("Failed to stop directive:", err); + setError(err instanceof Error ? err.message : "Failed to stop directive"); + return false; + } + }, [directiveId, fetchDetail]); + + return { + directive, + graph, + loading, + error, + refresh: fetchDetail, + start, + pause, + resume, + stop, + }; +} diff --git a/makima/frontend/src/routes/directives.tsx b/makima/frontend/src/routes/directives.tsx index 35e5703..90f0854 100644 --- a/makima/frontend/src/routes/directives.tsx +++ b/makima/frontend/src/routes/directives.tsx @@ -1,31 +1,17 @@ -import { useState, useCallback, useEffect, useMemo } from "react"; +import { useState, useCallback, useEffect } from "react"; import { useParams, useNavigate } from "react-router"; -import { - ReactFlow, - Edge, - Controls, - Background, - Handle, - Position, - BackgroundVariant, - MarkerType, -} from "@xyflow/react"; -import "@xyflow/react/dist/style.css"; import { Masthead } from "../components/Masthead"; -import { useDirectives, useDirectiveEventSubscription } from "../hooks/useDirectives"; +import { useDirectives } from "../hooks/useDirectives"; +import { useDirectiveDetail } from "../hooks/useDirectiveDetail"; import { useAuth } from "../contexts/AuthContext"; import type { DirectiveSummary, - DirectiveWithProgress, - DirectiveGraphResponse, - DirectiveGraphNode, CreateDirectiveRequest, - RepositoryHistoryEntry, AutonomyLevel, - StepStatus, - ConfidenceLevel, } from "../lib/api"; -import { getRepositorySuggestions } from "../lib/api"; +import { DirectiveList } from "../components/directives/DirectiveList"; +import { DirectiveDetail } from "../components/directives/DirectiveDetail"; +import { CreateDirectiveModal } from "../components/directives/CreateDirectiveModal"; export default function DirectivesPage() { const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth(); @@ -67,33 +53,20 @@ function DirectivesPageContent() { error, createNewDirective, archiveExistingDirective, - getDirectiveById, - getGraph, - start, - pause, - resume, - stop, } = useDirectives(); - const [directiveDetail, setDirectiveDetail] = useState<DirectiveWithProgress | null>(null); - const [directiveGraph, setDirectiveGraph] = useState<DirectiveGraphResponse | null>(null); - const [detailLoading, setDetailLoading] = useState(false); - const [isCreating, setIsCreating] = useState(false); + const { + directive: directiveDetail, + graph: directiveGraph, + loading: detailLoading, + refresh: refreshDetail, + start: handleStart, + pause: handlePause, + resume: handleResume, + stop: handleStop, + } = useDirectiveDetail(id); - // Load directive detail when ID changes - useEffect(() => { - if (id) { - setDetailLoading(true); - Promise.all([getDirectiveById(id), getGraph(id).catch(() => null)]).then(([directive, graph]) => { - setDirectiveDetail(directive); - setDirectiveGraph(graph); - setDetailLoading(false); - }); - } else { - setDirectiveDetail(null); - setDirectiveGraph(null); - } - }, [id, getDirectiveById, getGraph]); + const [isCreating, setIsCreating] = useState(false); const handleSelect = useCallback( (directiveId: string) => { @@ -147,45 +120,6 @@ function DirectivesPageContent() { [archiveExistingDirective, id, navigate] ); - const handleRefresh = useCallback(async () => { - if (id) { - const [directive, graph] = await Promise.all([ - getDirectiveById(id), - getGraph(id).catch(() => null), - ]); - setDirectiveDetail(directive); - setDirectiveGraph(graph); - } - }, [id, getDirectiveById, getGraph]); - - const handleStart = useCallback(async () => { - if (id) { - await start(id); - handleRefresh(); - } - }, [id, start, handleRefresh]); - - const handlePause = useCallback(async () => { - if (id) { - await pause(id); - handleRefresh(); - } - }, [id, pause, handleRefresh]); - - const handleResume = useCallback(async () => { - if (id) { - await resume(id); - handleRefresh(); - } - }, [id, resume, handleRefresh]); - - const handleStop = useCallback(async () => { - if (id) { - await stop(id); - handleRefresh(); - } - }, [id, stop, handleRefresh]); - return ( <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]"> <Masthead showNav /> @@ -222,7 +156,7 @@ function DirectivesPageContent() { graph={directiveGraph} loading={detailLoading} onBack={handleBack} - onRefresh={handleRefresh} + onRefresh={refreshDetail} onStart={handleStart} onPause={handlePause} onResume={handleResume} @@ -248,1007 +182,3 @@ function DirectivesPageContent() { </div> ); } - -// ============================================================================= -// Directive List Component -// ============================================================================= - -interface DirectiveListProps { - directives: DirectiveSummary[]; - loading: boolean; - onSelect: (id: string) => void; - onCreate: () => void; - selectedId?: string; - onArchive: (directive: DirectiveSummary) => void; -} - -function DirectiveList({ - directives, - loading, - onSelect, - onCreate, - selectedId, - onArchive, -}: DirectiveListProps) { - const [filter, setFilter] = useState<"all" | "active" | "completed" | "failed">("all"); - - const filteredDirectives = directives.filter((d) => { - if (filter === "all") return true; - if (filter === "active") return ["draft", "planning", "active", "paused"].includes(d.status); - if (filter === "completed") return d.status === "completed"; - if (filter === "failed") return d.status === "failed"; - return true; - }); - - return ( - <div className="panel h-full flex flex-col overflow-hidden"> - <div className="flex items-center justify-between p-3 border-b border-[rgba(117,170,252,0.15)]"> - <h2 className="font-mono text-sm text-[#75aafc] uppercase">Directives</h2> - <button - onClick={onCreate} - className="px-3 py-1 font-mono text-[10px] text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase" - > - + New - </button> - </div> - - {/* Filters */} - <div className="flex gap-1 p-2 border-b border-[rgba(117,170,252,0.1)]"> - {(["all", "active", "completed", "failed"] as const).map((f) => ( - <button - key={f} - onClick={() => setFilter(f)} - className={`px-2 py-1 font-mono text-[10px] uppercase ${ - filter === f - ? "text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3]" - : "text-[#556677] hover:text-[#9bc3ff]" - }`} - > - {f} - </button> - ))} - </div> - - {/* List */} - <div className="flex-1 overflow-y-auto"> - {loading ? ( - <div className="p-4 text-center"> - <p className="font-mono text-xs text-[#556677]">Loading...</p> - </div> - ) : filteredDirectives.length === 0 ? ( - <div className="p-4 text-center"> - <p className="font-mono text-xs text-[#556677]">No directives found</p> - </div> - ) : ( - filteredDirectives.map((d) => ( - <DirectiveListItem - key={d.id} - directive={d} - selected={d.id === selectedId} - onClick={() => onSelect(d.id)} - onArchive={() => onArchive(d)} - /> - )) - )} - </div> - </div> - ); -} - -interface DirectiveListItemProps { - directive: DirectiveSummary; - selected: boolean; - onClick: () => void; - onArchive: () => void; -} - -function DirectiveListItem({ directive, selected, onClick, onArchive }: DirectiveListItemProps) { - const progress = directive.totalSteps > 0 - ? Math.round((directive.completedSteps / directive.totalSteps) * 100) - : 0; - - const statusColor = { - draft: "text-[#556677]", - planning: "text-yellow-400", - active: "text-green-400", - paused: "text-yellow-400", - completed: "text-[#75aafc]", - archived: "text-[#556677]", - failed: "text-red-400", - }[directive.status] || "text-[#556677]"; - - const confidenceColor = { - green: "bg-green-500", - yellow: "bg-yellow-500", - red: "bg-red-500", - }[directive.currentConfidence !== null && directive.currentConfidence >= 0.8 - ? "green" - : directive.currentConfidence !== null && directive.currentConfidence >= 0.5 - ? "yellow" - : "red"] || "bg-[#556677]"; - - return ( - <div - onClick={onClick} - className={`p-3 cursor-pointer border-b border-[rgba(117,170,252,0.1)] hover:bg-[rgba(117,170,252,0.05)] ${ - selected ? "bg-[rgba(117,170,252,0.1)]" : "" - }`} - > - <div className="flex items-start justify-between gap-2"> - <div className="flex-1 min-w-0"> - <div className="font-mono text-sm text-[#dbe7ff] truncate"> - {directive.title || directive.goal.slice(0, 50)} - </div> - <div className="flex items-center gap-2 mt-1"> - <span className={`font-mono text-[10px] uppercase ${statusColor}`}> - {directive.status} - </span> - <span className="font-mono text-[10px] text-[#556677]"> - {directive.completedSteps}/{directive.totalSteps} steps - </span> - </div> - </div> - <div className="flex flex-col items-end gap-1"> - {directive.currentConfidence !== null && ( - <div className={`w-2 h-2 rounded-full ${confidenceColor}`} title={`Confidence: ${Math.round(directive.currentConfidence * 100)}%`} /> - )} - <button - onClick={(e) => { - e.stopPropagation(); - onArchive(); - }} - className="font-mono text-[10px] text-[#556677] hover:text-red-400" - > - Archive - </button> - </div> - </div> - - {/* Progress bar */} - {directive.totalSteps > 0 && ( - <div className="mt-2 h-1 bg-[rgba(117,170,252,0.1)] overflow-hidden"> - <div - className="h-full bg-[#75aafc] transition-all duration-300" - style={{ width: `${progress}%` }} - /> - </div> - )} - </div> - ); -} - -// ============================================================================= -// Directive Detail Component -// ============================================================================= - -interface DirectiveDetailProps { - directive: DirectiveWithProgress; - graph: DirectiveGraphResponse | null; - loading: boolean; - onBack: () => void; - onRefresh: () => void; - onStart: () => void; - onPause: () => void; - onResume: () => void; - onStop: () => void; -} - -function DirectiveDetail({ - directive, - graph, - loading, - onBack, - onRefresh, - onStart, - onPause, - onResume, - onStop, -}: DirectiveDetailProps) { - const [activeTab, setActiveTab] = useState<"overview" | "chain" | "events" | "evaluations" | "approvals" | "verifiers">("overview"); - - if (loading) { - return ( - <div className="panel h-full flex items-center justify-center"> - <p className="font-mono text-xs text-[#556677]">Loading...</p> - </div> - ); - } - - const statusColor = { - draft: "text-[#556677] bg-[#556677]/10 border-[#556677]/30", - planning: "text-yellow-400 bg-yellow-400/10 border-yellow-400/30", - active: "text-green-400 bg-green-400/10 border-green-400/30", - paused: "text-yellow-400 bg-yellow-400/10 border-yellow-400/30", - completed: "text-[#75aafc] bg-[#75aafc]/10 border-[#75aafc]/30", - archived: "text-[#556677] bg-[#556677]/10 border-[#556677]/30", - failed: "text-red-400 bg-red-400/10 border-red-400/30", - }[directive.status] || "text-[#556677] bg-[#556677]/10 border-[#556677]/30"; - - return ( - <div className="panel h-full flex flex-col overflow-hidden"> - {/* Header */} - <div className="p-3 border-b border-[rgba(117,170,252,0.15)]"> - <div className="flex items-center justify-between"> - <div className="flex items-center gap-3"> - <button - onClick={onBack} - className="font-mono text-xs text-[#556677] hover:text-[#9bc3ff]" - > - ← Back - </button> - <h2 className="font-mono text-sm text-[#dbe7ff]"> - {directive.title || directive.goal.slice(0, 50)} - </h2> - <span className={`px-2 py-0.5 font-mono text-[10px] uppercase border ${statusColor}`}> - {directive.status} - </span> - </div> - <div className="flex items-center gap-2"> - {directive.status === "draft" && ( - <button - onClick={onStart} - className="px-3 py-1 font-mono text-[10px] text-[#dbe7ff] bg-green-700 border border-green-600 hover:bg-green-600 uppercase" - > - Start - </button> - )} - {directive.status === "active" && ( - <button - onClick={onPause} - className="px-3 py-1 font-mono text-[10px] text-[#dbe7ff] bg-yellow-700 border border-yellow-600 hover:bg-yellow-600 uppercase" - > - Pause - </button> - )} - {directive.status === "paused" && ( - <button - onClick={onResume} - className="px-3 py-1 font-mono text-[10px] text-[#dbe7ff] bg-green-700 border border-green-600 hover:bg-green-600 uppercase" - > - Resume - </button> - )} - {["active", "paused"].includes(directive.status) && ( - <button - onClick={onStop} - className="px-3 py-1 font-mono text-[10px] text-[#dbe7ff] bg-red-700 border border-red-600 hover:bg-red-600 uppercase" - > - Stop - </button> - )} - <button - onClick={onRefresh} - className="px-3 py-1 font-mono text-[10px] text-[#9bc3ff] hover:text-[#dbe7ff]" - > - Refresh - </button> - </div> - </div> - </div> - - {/* Tabs */} - <div className="flex gap-1 p-2 border-b border-[rgba(117,170,252,0.1)]"> - {(["overview", "chain", "events", "evaluations", "approvals", "verifiers"] as const).map((tab) => ( - <button - key={tab} - onClick={() => setActiveTab(tab)} - className={`px-3 py-1.5 font-mono text-[10px] uppercase ${ - activeTab === tab - ? "text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3]" - : "text-[#556677] hover:text-[#9bc3ff]" - }`} - > - {tab} - {tab === "approvals" && directive.pendingApprovals.length > 0 && ( - <span className="ml-1 px-1 bg-yellow-500 text-black rounded text-[8px]"> - {directive.pendingApprovals.length} - </span> - )} - </button> - ))} - </div> - - {/* Tab Content */} - <div className="flex-1 overflow-y-auto p-4"> - {activeTab === "overview" && ( - <OverviewTab directive={directive} /> - )} - {activeTab === "chain" && ( - <ChainTab directive={directive} graph={graph} /> - )} - {activeTab === "events" && ( - <EventsTab directive={directive} /> - )} - {activeTab === "evaluations" && ( - <EvaluationsTab directive={directive} /> - )} - {activeTab === "approvals" && ( - <ApprovalsTab directive={directive} onRefresh={onRefresh} /> - )} - {activeTab === "verifiers" && ( - <VerifiersTab directive={directive} /> - )} - </div> - </div> - ); -} - -// ============================================================================= -// Tab Components -// ============================================================================= - -function OverviewTab({ directive }: { directive: DirectiveWithProgress }) { - return ( - <div className="space-y-6"> - {/* Goal */} - <div> - <h3 className="font-mono text-xs text-[#75aafc] uppercase mb-2">Goal</h3> - <p className="font-mono text-sm text-[#dbe7ff] whitespace-pre-wrap"> - {directive.goal} - </p> - </div> - - {/* Progress */} - <div> - <h3 className="font-mono text-xs text-[#75aafc] uppercase mb-2">Progress</h3> - <div className="grid grid-cols-3 gap-4"> - <div className="p-3 bg-[rgba(117,170,252,0.05)] border border-[rgba(117,170,252,0.15)]"> - <div className="font-mono text-2xl text-[#dbe7ff]"> - {directive.chain?.completedSteps || 0} - </div> - <div className="font-mono text-[10px] text-[#556677] uppercase">Completed Steps</div> - </div> - <div className="p-3 bg-[rgba(117,170,252,0.05)] border border-[rgba(117,170,252,0.15)]"> - <div className="font-mono text-2xl text-[#dbe7ff]"> - {directive.chain?.totalSteps || 0} - </div> - <div className="font-mono text-[10px] text-[#556677] uppercase">Total Steps</div> - </div> - <div className="p-3 bg-[rgba(117,170,252,0.05)] border border-[rgba(117,170,252,0.15)]"> - <div className="font-mono text-2xl text-[#dbe7ff]"> - {directive.chain?.currentConfidence != null - ? `${Math.round((directive.chain?.currentConfidence ?? 0) * 100)}%` - : "-"} - </div> - <div className="font-mono text-[10px] text-[#556677] uppercase">Confidence</div> - </div> - </div> - </div> - - {/* Configuration */} - <div> - <h3 className="font-mono text-xs text-[#75aafc] uppercase mb-2">Configuration</h3> - <div className="grid grid-cols-2 gap-2 text-sm"> - <div className="flex justify-between"> - <span className="font-mono text-[#556677]">Autonomy Level</span> - <span className="font-mono text-[#dbe7ff]">{directive.autonomyLevel}</span> - </div> - <div className="flex justify-between"> - <span className="font-mono text-[#556677]">Max Rework Cycles</span> - <span className="font-mono text-[#dbe7ff]">{directive.maxReworkCycles}</span> - </div> - <div className="flex justify-between"> - <span className="font-mono text-[#556677]">Green Threshold</span> - <span className="font-mono text-[#dbe7ff]">{directive.confidenceThresholdGreen}</span> - </div> - <div className="flex justify-between"> - <span className="font-mono text-[#556677]">Yellow Threshold</span> - <span className="font-mono text-[#dbe7ff]">{directive.confidenceThresholdYellow}</span> - </div> - </div> - </div> - - {/* Repository */} - {directive.repositoryUrl && ( - <div> - <h3 className="font-mono text-xs text-[#75aafc] uppercase mb-2">Repository</h3> - <p className="font-mono text-sm text-[#9bc3ff]">{directive.repositoryUrl}</p> - </div> - )} - </div> - ); -} - -// Step status colors for both list and DAG views -const stepStatusStyles: Record<StepStatus, { border: string; bg: string; text: string }> = { - pending: { border: "#556677", bg: "#556677", text: "#556677" }, - ready: { border: "#3b82f6", bg: "#3b82f6", text: "#3b82f6" }, - running: { border: "#22c55e", bg: "#22c55e", text: "#22c55e" }, - evaluating: { border: "#eab308", bg: "#eab308", text: "#eab308" }, - passed: { border: "#75aafc", bg: "#75aafc", text: "#75aafc" }, - failed: { border: "#ef4444", bg: "#ef4444", text: "#ef4444" }, - rework: { border: "#f97316", bg: "#f97316", text: "#f97316" }, - skipped: { border: "#556677", bg: "#556677", text: "#556677" }, - blocked: { border: "#ef4444", bg: "#ef4444", text: "#ef4444" }, -}; - -// Confidence level colors -const confidenceColors: Record<ConfidenceLevel, string> = { - green: "#22c55e", - yellow: "#eab308", - red: "#ef4444", -}; - -// Node dimensions -const NODE_WIDTH = 180; -const NODE_HEIGHT = 70; - -// Custom node component for steps -function StepNodeComponent({ data }: { data: DirectiveGraphNode & { selected?: boolean } }) { - const styles = stepStatusStyles[data.status] || stepStatusStyles.pending; - const isRunning = data.status === "running" || data.status === "evaluating"; - - return ( - <div - className={`rounded-lg border-2 bg-[#0a1628] overflow-hidden ${ - isRunning ? "animate-pulse" : "" - }`} - style={{ - width: NODE_WIDTH, - height: NODE_HEIGHT, - borderColor: styles.border, - borderStyle: data.status === "pending" ? "dashed" : "solid", - }} - > - <Handle - type="target" - position={Position.Top} - className="!bg-[#75aafc] !w-3 !h-3 !border-2 !border-[#0a1628]" - /> - - {/* Status indicator bar */} - <div className="h-1.5" style={{ backgroundColor: styles.bg }} /> - - {/* Content */} - <div className="p-2"> - <div className="flex items-center justify-between mb-1"> - <span className="font-mono text-xs text-[#dbe7ff] truncate flex-1">{data.name}</span> - {data.confidenceScore !== null && data.confidenceLevel && ( - <div - className="w-2 h-2 rounded-full flex-shrink-0 ml-1" - style={{ backgroundColor: confidenceColors[data.confidenceLevel] }} - title={`Confidence: ${Math.round(data.confidenceScore * 100)}%`} - /> - )} - </div> - <div className="flex items-center justify-between"> - <span - className="font-mono text-[10px] uppercase px-1.5 py-0.5 rounded" - style={{ - color: styles.text, - backgroundColor: `${styles.bg}20`, - }} - > - {data.status} - </span> - <span className="font-mono text-[10px] text-[#8b949e]">{data.stepType}</span> - </div> - </div> - - <Handle - type="source" - position={Position.Bottom} - className="!bg-[#f59e0b] !w-3 !h-3 !border-2 !border-[#0a1628]" - /> - </div> - ); -} - -// Node types for React Flow -const nodeTypes = { - step: StepNodeComponent, -}; - -function ChainTab({ directive, graph }: { directive: DirectiveWithProgress; graph: DirectiveGraphResponse | null }) { - const [viewMode, setViewMode] = useState<"dag" | "list">("dag"); - - // Convert graph to React Flow nodes and edges - const { nodes, edges } = useMemo(() => { - if (!graph || !graph.nodes.length) { - // Fallback: generate positions from directive.steps - const stepNodes = directive.steps.map((step, index) => ({ - id: step.id, - type: "step" as const, - position: { - x: (index % 3) * 220 + 50, - y: Math.floor(index / 3) * 120 + 50, - }, - data: { - id: step.id, - name: step.name, - stepType: step.stepType, - status: step.status, - confidenceScore: step.confidenceScore, - confidenceLevel: step.confidenceLevel, - contractId: step.contractId, - editorX: null, - editorY: null, - }, - })); - - // Build edges from dependencies - const stepEdges: Edge[] = []; - directive.steps.forEach((step) => { - (step.dependsOn ?? []).forEach((depName) => { - const depStep = directive.steps.find((s) => s.name === depName); - if (depStep) { - stepEdges.push({ - id: `${depStep.id}-${step.id}`, - source: depStep.id, - target: step.id, - type: "smoothstep", - markerEnd: { type: MarkerType.ArrowClosed, color: "#556677" }, - style: { stroke: "#556677", strokeWidth: 2 }, - }); - } - }); - }); - - return { nodes: stepNodes, edges: stepEdges }; - } - - // Use graph data - const graphNodes = graph.nodes.map((node) => ({ - id: node.id, - type: "step" as const, - position: { - x: node.editorX ?? 50, - y: node.editorY ?? 50, - }, - data: { ...node }, - })); - - const graphEdges: Edge[] = graph.edges.map((edge) => ({ - id: `${edge.source}-${edge.target}`, - source: edge.source, - target: edge.target, - type: "smoothstep", - markerEnd: { type: MarkerType.ArrowClosed, color: "#556677" }, - style: { stroke: "#556677", strokeWidth: 2 }, - })); - - return { nodes: graphNodes, edges: graphEdges }; - }, [graph, directive.steps]); - - if (!directive.chain) { - return ( - <div className="text-center py-8"> - <p className="font-mono text-sm text-[#556677]"> - No chain generated yet. Start the directive to generate a chain. - </p> - </div> - ); - } - - return ( - <div className="space-y-4"> - {/* Chain info header */} - <div className="flex items-center justify-between p-3 bg-[rgba(117,170,252,0.05)] border border-[rgba(117,170,252,0.15)]"> - <div> - <div className="font-mono text-sm text-[#dbe7ff]">{directive.chain.name}</div> - <div className="font-mono text-[10px] text-[#556677]">Generation {directive.chain.generation}</div> - </div> - <div className="flex items-center gap-4"> - <div className="font-mono text-xs text-[#556677]"> - {directive.chain.completedSteps}/{directive.chain.totalSteps} steps - </div> - {/* View toggle */} - <div className="flex border border-[rgba(117,170,252,0.2)]"> - <button - onClick={() => setViewMode("dag")} - className={`px-2 py-1 font-mono text-[10px] uppercase ${ - viewMode === "dag" - ? "bg-[rgba(117,170,252,0.2)] text-[#75aafc]" - : "text-[#556677] hover:text-[#75aafc]" - }`} - > - DAG - </button> - <button - onClick={() => setViewMode("list")} - className={`px-2 py-1 font-mono text-[10px] uppercase ${ - viewMode === "list" - ? "bg-[rgba(117,170,252,0.2)] text-[#75aafc]" - : "text-[#556677] hover:text-[#75aafc]" - }`} - > - List - </button> - </div> - </div> - </div> - - {viewMode === "dag" ? ( - /* DAG visualization */ - <div className="h-[400px] border border-[rgba(117,170,252,0.1)] rounded bg-[#050d18]"> - {directive.steps.length === 0 ? ( - <div className="flex items-center justify-center h-full"> - <p className="font-mono text-sm text-[#556677]">No steps in chain</p> - </div> - ) : ( - <ReactFlow - nodes={nodes} - edges={edges} - nodeTypes={nodeTypes} - fitView - fitViewOptions={{ padding: 0.2 }} - minZoom={0.5} - maxZoom={1.5} - defaultEdgeOptions={{ - type: "smoothstep", - style: { stroke: "#556677", strokeWidth: 2 }, - }} - > - <Background variant={BackgroundVariant.Dots} gap={20} size={1} color="#1a2a3a" /> - <Controls className="!bg-[#0a1628] !border-[rgba(117,170,252,0.2)]" /> - </ReactFlow> - )} - </div> - ) : ( - /* List view */ - <div className="space-y-2"> - <h3 className="font-mono text-xs text-[#75aafc] uppercase">Steps</h3> - {directive.steps.length === 0 ? ( - <p className="font-mono text-sm text-[#556677]">No steps in chain</p> - ) : ( - directive.steps.map((step) => { - const styles = stepStatusStyles[step.status] || stepStatusStyles.pending; - - return ( - <div - key={step.id} - className="p-3 bg-[rgba(117,170,252,0.02)] border border-[rgba(117,170,252,0.1)]" - > - <div className="flex items-center justify-between"> - <div className="flex items-center gap-2"> - <span className="font-mono text-sm text-[#dbe7ff]">{step.name}</span> - <span - className="px-1.5 py-0.5 font-mono text-[10px] uppercase border" - style={{ color: styles.text, borderColor: `${styles.border}50` }} - > - {step.status} - </span> - {step.confidenceScore !== null && ( - <span className="font-mono text-[10px] text-[#556677]"> - ({Math.round(step.confidenceScore * 100)}%) - </span> - )} - </div> - <div className="font-mono text-[10px] text-[#556677]">{step.stepType}</div> - </div> - {step.description && ( - <p className="font-mono text-xs text-[#556677] mt-1">{step.description}</p> - )} - {step.dependsOn?.length > 0 && ( - <div className="font-mono text-[10px] text-[#556677] mt-1"> - Depends on: {step.dependsOn.join(", ")} - </div> - )} - </div> - ); - }) - )} - </div> - )} - </div> - ); -} - -function EventsTab({ directive }: { directive: DirectiveWithProgress }) { - // Subscribe to real-time events via SSE - const { events: streamEvents, isConnected, error: sseError } = useDirectiveEventSubscription(directive.id); - - // Combine initial events with streamed events (avoiding duplicates) - const allEvents = useMemo(() => { - const eventMap = new Map(); - // Add initial events first - directive.recentEvents.forEach((e) => eventMap.set(e.id, e)); - // Add streamed events (will override any duplicates) - streamEvents.forEach((e) => eventMap.set(e.id, e)); - // Sort by created_at descending (most recent first) - return Array.from(eventMap.values()).sort( - (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() - ); - }, [directive.recentEvents, streamEvents]); - - return ( - <div className="space-y-4"> - {/* Connection status */} - <div className="flex items-center justify-between text-[10px] font-mono"> - <div className="flex items-center gap-2"> - <span className={isConnected ? "text-green-400" : "text-[#556677]"}> - {isConnected ? "● Live" : "○ Connecting..."} - </span> - {sseError && <span className="text-red-400">{sseError}</span>} - </div> - <span className="text-[#556677]">{allEvents.length} events</span> - </div> - - {/* Event list */} - {allEvents.length === 0 ? ( - <div className="text-center py-8"> - <p className="font-mono text-sm text-[#556677]">No events yet</p> - </div> - ) : ( - <div className="space-y-2"> - {allEvents.map((event) => { - const severityColors: Record<string, string> = { - info: "text-[#75aafc]", - warning: "text-yellow-400", - error: "text-red-400", - critical: "text-red-600", - }; - const severityColor = severityColors[event.severity] || "text-[#556677]"; - - return ( - <div - key={event.id} - className="p-3 bg-[rgba(117,170,252,0.02)] border border-[rgba(117,170,252,0.1)]" - > - <div className="flex items-center justify-between"> - <div className="flex items-center gap-2"> - <span className={`font-mono text-xs ${severityColor}`}>{event.eventType}</span> - <span className="font-mono text-[10px] text-[#556677]">{event.actorType}</span> - </div> - <span className="font-mono text-[10px] text-[#556677]"> - {new Date(event.createdAt).toLocaleString()} - </span> - </div> - {event.eventData != null && ( - <pre className="font-mono text-[10px] text-[#556677] mt-1 overflow-x-auto"> - {JSON.stringify(event.eventData, null, 2)} - </pre> - )} - </div> - ); - })} - </div> - )} - </div> - ); -} - -function EvaluationsTab({ directive: _directive }: { directive: DirectiveWithProgress }) { - // TODO: Fetch evaluations separately - return ( - <div className="text-center py-8"> - <p className="font-mono text-sm text-[#556677]"> - Evaluations will be shown here after steps are evaluated - </p> - </div> - ); -} - -function ApprovalsTab({ directive, onRefresh }: { directive: DirectiveWithProgress; onRefresh: () => void }) { - if (directive.pendingApprovals.length === 0) { - return ( - <div className="text-center py-8"> - <p className="font-mono text-sm text-[#556677]">No pending approvals</p> - </div> - ); - } - - const handleApprove = async (approvalId: string) => { - try { - const { approveDirectiveRequest } = await import("../lib/api"); - await approveDirectiveRequest(directive.id, approvalId); - onRefresh(); - } catch (err) { - console.error("Failed to approve:", err); - } - }; - - const handleDeny = async (approvalId: string) => { - try { - const { denyDirectiveRequest } = await import("../lib/api"); - await denyDirectiveRequest(directive.id, approvalId); - onRefresh(); - } catch (err) { - console.error("Failed to deny:", err); - } - }; - - return ( - <div className="space-y-3"> - {directive.pendingApprovals.map((approval) => { - const urgencyColor = { - low: "text-[#556677]", - normal: "text-[#75aafc]", - high: "text-yellow-400", - critical: "text-red-400", - }[approval.urgency] || "text-[#556677]"; - - return ( - <div - key={approval.id} - className="p-4 bg-[rgba(117,170,252,0.05)] border border-[rgba(117,170,252,0.2)]" - > - <div className="flex items-start justify-between"> - <div> - <div className="flex items-center gap-2"> - <span className="font-mono text-sm text-[#dbe7ff]">{approval.approvalType}</span> - <span className={`font-mono text-[10px] uppercase ${urgencyColor}`}> - {approval.urgency} - </span> - </div> - <p className="font-mono text-xs text-[#9bc3ff] mt-1">{approval.description}</p> - </div> - <div className="flex gap-2"> - <button - onClick={() => handleApprove(approval.id)} - className="px-3 py-1 font-mono text-[10px] text-[#dbe7ff] bg-green-700 border border-green-600 hover:bg-green-600 uppercase" - > - Approve - </button> - <button - onClick={() => handleDeny(approval.id)} - className="px-3 py-1 font-mono text-[10px] text-[#dbe7ff] bg-red-700 border border-red-600 hover:bg-red-600 uppercase" - > - Deny - </button> - </div> - </div> - </div> - ); - })} - </div> - ); -} - -function VerifiersTab({ directive: _directive }: { directive: DirectiveWithProgress }) { - // TODO: Fetch verifiers separately - return ( - <div className="text-center py-8"> - <p className="font-mono text-sm text-[#556677]"> - Verifiers will be shown here. Use auto-detect to find available verifiers. - </p> - </div> - ); -} - -// ============================================================================= -// Create Directive Modal -// ============================================================================= - -interface CreateDirectiveModalProps { - onSubmit: (goal: string, repositoryUrl: string | undefined, autonomyLevel: AutonomyLevel) => void; - onCancel: () => void; -} - -function CreateDirectiveModal({ onSubmit, onCancel }: CreateDirectiveModalProps) { - const [goal, setGoal] = useState(""); - const [repositoryUrl, setRepositoryUrl] = useState(""); - const [autonomyLevel, setAutonomyLevel] = useState<AutonomyLevel>("guardrails"); - const [suggestions, setSuggestions] = useState<RepositoryHistoryEntry[]>([]); - const [showSuggestions, setShowSuggestions] = useState(false); - - // Load suggestions - useEffect(() => { - getRepositorySuggestions("remote", undefined, 5) - .then((res) => { - setSuggestions(res.entries); - }) - .catch(() => { - setSuggestions([]); - }); - }, []); - - const handleSubmit = () => { - if (goal.trim()) { - onSubmit(goal.trim(), repositoryUrl.trim() || undefined, autonomyLevel); - } - }; - - return ( - <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"> - <div className="w-full max-w-lg p-6 bg-[#0a1628] border border-[rgba(117,170,252,0.3)] max-h-[90vh] overflow-y-auto"> - <h3 className="font-mono text-sm text-[#75aafc] uppercase mb-4"> - Create Directive - </h3> - - <div className="space-y-4"> - {/* Goal */} - <div> - <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1"> - Goal * - </label> - <textarea - value={goal} - onChange={(e) => setGoal(e.target.value)} - placeholder="Describe what you want to accomplish..." - rows={3} - className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc] resize-none" - autoFocus - /> - </div> - - {/* Repository URL */} - <div> - <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1"> - Repository URL (optional) - </label> - <div className="relative"> - <input - type="text" - value={repositoryUrl} - onChange={(e) => setRepositoryUrl(e.target.value)} - onFocus={() => suggestions.length > 0 && setShowSuggestions(true)} - onBlur={() => setTimeout(() => setShowSuggestions(false), 200)} - placeholder="https://github.com/owner/repo" - className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]" - /> - {showSuggestions && suggestions.length > 0 && ( - <div className="absolute top-full left-0 right-0 mt-1 border border-[rgba(117,170,252,0.2)] bg-[#0a1525] max-h-32 overflow-y-auto z-10"> - {suggestions.map((s) => ( - <button - key={s.id} - type="button" - onClick={() => { - setRepositoryUrl(s.repositoryUrl || ""); - setShowSuggestions(false); - }} - 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="text-[#9bc3ff] truncate">{s.name}</div> - <div className="text-[10px] text-[#556677] truncate">{s.repositoryUrl}</div> - </button> - ))} - </div> - )} - </div> - </div> - - {/* Autonomy Level */} - <div> - <label className="block font-mono text-xs text-[#8b949e] uppercase mb-2"> - Autonomy Level - </label> - <div className="flex gap-2"> - {(["full_auto", "guardrails", "manual"] as const).map((level) => ( - <button - key={level} - type="button" - onClick={() => setAutonomyLevel(level)} - className={`flex-1 px-3 py-2 font-mono text-xs uppercase ${ - autonomyLevel === level - ? "text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3]" - : "text-[#556677] border border-[rgba(117,170,252,0.2)] hover:border-[#3f6fb3]" - }`} - > - {level.replace("_", " ")} - </button> - ))} - </div> - <p className="font-mono text-[10px] text-[#556677] mt-1"> - {autonomyLevel === "full_auto" && "Automatic progression without approval gates"} - {autonomyLevel === "guardrails" && "Request approval for yellow/red confidence scores"} - {autonomyLevel === "manual" && "Request approval for all step completions"} - </p> - </div> - - <p className="font-mono text-xs text-[#8b949e]"> - A directive is a top-level goal that generates a chain of steps. Each step spawns - contracts that are verified before progression. - </p> - - {/* Actions */} - <div className="flex gap-2 justify-end pt-2"> - <button - onClick={onCancel} - className="px-4 py-2 font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors" - > - Cancel - </button> - <button - onClick={handleSubmit} - disabled={!goal.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> - ); -} |
