summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/directives/DirectiveDetail.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/components/directives/DirectiveDetail.tsx')
-rw-r--r--makima/frontend/src/components/directives/DirectiveDetail.tsx216
1 files changed, 216 insertions, 0 deletions
diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx
new file mode 100644
index 0000000..abd2c55
--- /dev/null
+++ b/makima/frontend/src/components/directives/DirectiveDetail.tsx
@@ -0,0 +1,216 @@
+import { useState } from "react";
+import type { DirectiveWithSteps, DirectiveStatus } from "../../lib/api";
+import { DirectiveDAG } from "./DirectiveDAG";
+
+const STATUS_BADGE: Record<DirectiveStatus, { color: string; label: string }> = {
+ draft: { color: "text-[#7788aa] border-[#2a3a5a]", label: "DRAFT" },
+ active: { color: "text-green-400 border-green-800", label: "ACTIVE" },
+ idle: { color: "text-yellow-400 border-yellow-800", label: "IDLE" },
+ paused: { color: "text-orange-400 border-orange-800", label: "PAUSED" },
+ archived: { color: "text-[#556677] border-[#2a3a5a]", label: "ARCHIVED" },
+};
+
+interface DirectiveDetailProps {
+ directive: DirectiveWithSteps;
+ onStart: () => void;
+ onPause: () => void;
+ onAdvance: () => void;
+ onCompleteStep: (stepId: string) => void;
+ onFailStep: (stepId: string) => void;
+ onSkipStep: (stepId: string) => void;
+ onUpdateGoal: (goal: string) => void;
+ onDelete: () => void;
+ onRefresh: () => void;
+}
+
+export function DirectiveDetail({
+ directive,
+ onStart,
+ onPause,
+ onAdvance,
+ onCompleteStep,
+ onFailStep,
+ onSkipStep,
+ onUpdateGoal,
+ onDelete,
+ onRefresh,
+}: DirectiveDetailProps) {
+ const [editingGoal, setEditingGoal] = useState(false);
+ const [goalText, setGoalText] = useState(directive.goal);
+ const badge = STATUS_BADGE[directive.status] || STATUS_BADGE.draft;
+
+ const completedSteps = directive.steps.filter((s) => s.status === "completed").length;
+ const totalSteps = directive.steps.length;
+ const progress = totalSteps > 0 ? Math.round((completedSteps / totalSteps) * 100) : 0;
+
+ const handleGoalSave = () => {
+ if (goalText.trim() && goalText !== directive.goal) {
+ onUpdateGoal(goalText.trim());
+ }
+ setEditingGoal(false);
+ };
+
+ return (
+ <div className="flex flex-col h-full overflow-y-auto">
+ {/* Header */}
+ <div className="px-4 py-3 border-b border-dashed border-[rgba(117,170,252,0.2)]">
+ <div className="flex items-center justify-between mb-2">
+ <h2 className="text-[14px] font-mono text-white font-medium truncate pr-2">
+ {directive.title}
+ </h2>
+ <div className="flex items-center gap-2 shrink-0">
+ <span
+ className={`text-[10px] font-mono ${badge.color} border rounded px-2 py-0.5`}
+ >
+ {badge.label}
+ </span>
+ <button
+ type="button"
+ onClick={onRefresh}
+ className="text-[10px] font-mono text-[#7788aa] hover:text-white"
+ title="Refresh"
+ >
+ [refresh]
+ </button>
+ </div>
+ </div>
+
+ {/* Progress bar */}
+ {totalSteps > 0 && (
+ <div className="flex items-center gap-2 mb-2">
+ <div className="flex-1 h-1.5 bg-[#1a2540] rounded overflow-hidden">
+ <div
+ className="h-full bg-emerald-600 rounded transition-all"
+ style={{ width: `${progress}%` }}
+ />
+ </div>
+ <span className="text-[10px] font-mono text-[#7788aa] shrink-0">
+ {completedSteps}/{totalSteps} steps
+ </span>
+ </div>
+ )}
+
+ {/* Repo info */}
+ {(directive.repositoryUrl || directive.localPath) && (
+ <div className="text-[10px] font-mono text-[#556677] mb-2 truncate">
+ {directive.repositoryUrl || directive.localPath}
+ {directive.baseBranch && ` @ ${directive.baseBranch}`}
+ </div>
+ )}
+
+ {/* Controls */}
+ <div className="flex flex-wrap gap-2">
+ {(directive.status === "draft" || directive.status === "paused") && (
+ <button
+ type="button"
+ onClick={onStart}
+ className="text-[10px] font-mono text-green-400 hover:text-green-300 border border-green-800 rounded px-2 py-1"
+ >
+ Start
+ </button>
+ )}
+ {directive.status === "active" && (
+ <>
+ <button
+ type="button"
+ onClick={onPause}
+ className="text-[10px] font-mono text-orange-400 hover:text-orange-300 border border-orange-800 rounded px-2 py-1"
+ >
+ Pause
+ </button>
+ <button
+ type="button"
+ onClick={onAdvance}
+ className="text-[10px] font-mono text-[#75aafc] hover:text-white border border-[rgba(117,170,252,0.3)] rounded px-2 py-1"
+ >
+ Advance
+ </button>
+ </>
+ )}
+ {directive.status === "idle" && (
+ <div className="flex items-center gap-2">
+ <span className="text-[10px] font-mono text-yellow-400">
+ All steps done. Update goal to add new work.
+ </span>
+ <button
+ type="button"
+ onClick={() => setEditingGoal(true)}
+ className="text-[10px] font-mono text-[#75aafc] hover:text-white border border-[rgba(117,170,252,0.3)] rounded px-2 py-1"
+ >
+ Update Goal
+ </button>
+ </div>
+ )}
+ <button
+ type="button"
+ onClick={onDelete}
+ className="text-[10px] font-mono text-red-400 hover:text-red-300 border border-red-800 rounded px-2 py-1 ml-auto"
+ >
+ Delete
+ </button>
+ </div>
+ </div>
+
+ {/* Goal */}
+ <div className="px-4 py-3 border-b border-[rgba(117,170,252,0.1)]">
+ <div className="flex items-center justify-between mb-1">
+ <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide">
+ Goal
+ </span>
+ {!editingGoal && (
+ <button
+ type="button"
+ onClick={() => { setGoalText(directive.goal); setEditingGoal(true); }}
+ className="text-[9px] font-mono text-[#556677] hover:text-[#75aafc]"
+ >
+ [edit]
+ </button>
+ )}
+ </div>
+ {editingGoal ? (
+ <div className="flex flex-col gap-1.5">
+ <textarea
+ value={goalText}
+ onChange={(e) => setGoalText(e.target.value)}
+ className="w-full bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1.5 text-[11px] font-mono text-white resize-y min-h-[60px]"
+ rows={3}
+ />
+ <div className="flex gap-1.5">
+ <button
+ type="button"
+ onClick={handleGoalSave}
+ className="text-[10px] font-mono text-emerald-400 hover:text-emerald-300 border border-emerald-800 rounded px-2 py-0.5"
+ >
+ Save
+ </button>
+ <button
+ type="button"
+ onClick={() => setEditingGoal(false)}
+ className="text-[10px] font-mono text-[#7788aa] hover:text-white border border-[#2a3a5a] rounded px-2 py-0.5"
+ >
+ Cancel
+ </button>
+ </div>
+ </div>
+ ) : (
+ <p className="text-[11px] font-mono text-[#c0d0e0] whitespace-pre-wrap">
+ {directive.goal}
+ </p>
+ )}
+ </div>
+
+ {/* DAG */}
+ <div className="px-4 py-3 flex-1">
+ <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide block mb-2">
+ Steps ({totalSteps})
+ </span>
+ <DirectiveDAG
+ steps={directive.steps}
+ onComplete={onCompleteStep}
+ onFail={onFailStep}
+ onSkip={onSkipStep}
+ />
+ </div>
+ </div>
+ );
+}