summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/contracts
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/components/contracts')
-rw-r--r--makima/frontend/src/components/contracts/AutopilotPanel.tsx50
-rw-r--r--makima/frontend/src/components/contracts/PhaseConfirmationModal.tsx411
2 files changed, 461 insertions, 0 deletions
diff --git a/makima/frontend/src/components/contracts/AutopilotPanel.tsx b/makima/frontend/src/components/contracts/AutopilotPanel.tsx
index a8a8e2e..cf42e44 100644
--- a/makima/frontend/src/components/contracts/AutopilotPanel.tsx
+++ b/makima/frontend/src/components/contracts/AutopilotPanel.tsx
@@ -5,6 +5,7 @@ import {
startSupervisor,
stopSupervisor,
resumeSupervisor,
+ updateContract,
type SupervisorStatus,
} from "../../lib/api";
@@ -111,6 +112,23 @@ export function AutopilotPanel({ contract, onUpdate }: AutopilotPanelProps) {
}
}, [contract.id, supervisorStatus.supervisorTaskId, onUpdate]);
+ const handlePhaseGuardChange = useCallback(async (enabled: boolean) => {
+ setLoading(true);
+ setError(null);
+
+ try {
+ await updateContract(contract.id, {
+ phaseGuard: enabled,
+ version: contract.version,
+ });
+ onUpdate();
+ } catch (e) {
+ setError(e instanceof Error ? e.message : "Failed to update phase guard setting");
+ } finally {
+ setLoading(false);
+ }
+ }, [contract.id, contract.version, onUpdate]);
+
// Don't show panel for task-type contracts (they don't have supervisors)
if (contract.contractType === "task") {
return null;
@@ -185,6 +203,38 @@ export function AutopilotPanel({ contract, onUpdate }: AutopilotPanelProps) {
)}
</div>
+ {/* Phase Guard Toggle */}
+ <div className="pt-3 border-t border-dashed border-[rgba(117,170,252,0.2)]">
+ <label className="flex items-start gap-3 cursor-pointer group">
+ <div className="relative mt-0.5">
+ <input
+ type="checkbox"
+ checked={contract.phaseGuard ?? false}
+ onChange={(e) => handlePhaseGuardChange(e.target.checked)}
+ disabled={loading}
+ className="sr-only peer"
+ />
+ <div className="w-9 h-5 bg-[rgba(117,170,252,0.1)] border border-[rgba(117,170,252,0.3)] rounded-full peer-checked:bg-[rgba(117,170,252,0.3)] transition-colors peer-disabled:opacity-50" />
+ <div className="absolute left-0.5 top-0.5 w-4 h-4 bg-[#555] rounded-full transition-transform peer-checked:translate-x-4 peer-checked:bg-[#75aafc] peer-disabled:opacity-50" />
+ </div>
+ <div className="flex-1">
+ <div className="flex items-center gap-2">
+ <span className="font-mono text-sm text-[#dbe7ff] group-hover:text-white transition-colors">
+ Phase Guard
+ </span>
+ {contract.phaseGuard && (
+ <span className="px-1.5 py-0.5 text-[9px] font-mono uppercase bg-yellow-500/20 text-yellow-400 border border-yellow-400/30 rounded">
+ active
+ </span>
+ )}
+ </div>
+ <div className="font-mono text-xs text-[#555] mt-0.5">
+ Ask for confirmation before advancing to the next phase
+ </div>
+ </div>
+ </label>
+ </div>
+
{/* Show running indicator when active */}
{supervisorStatus.status === "running" && (
<div className="flex items-center gap-2 pt-2 border-t border-dashed border-[rgba(117,170,252,0.2)]">
diff --git a/makima/frontend/src/components/contracts/PhaseConfirmationModal.tsx b/makima/frontend/src/components/contracts/PhaseConfirmationModal.tsx
new file mode 100644
index 0000000..96cc1d4
--- /dev/null
+++ b/makima/frontend/src/components/contracts/PhaseConfirmationModal.tsx
@@ -0,0 +1,411 @@
+import { useState } from "react";
+import type { ContractPhase } from "../../lib/api";
+
+/** Phase configuration for styling */
+const phaseConfig: Record<
+ ContractPhase,
+ { label: string; color: string; borderColor: string }
+> = {
+ research: {
+ label: "Research",
+ color: "text-purple-400",
+ borderColor: "border-purple-400/50",
+ },
+ specify: {
+ label: "Specify",
+ color: "text-blue-400",
+ borderColor: "border-blue-400/50",
+ },
+ plan: {
+ label: "Plan",
+ color: "text-cyan-400",
+ borderColor: "border-cyan-400/50",
+ },
+ execute: {
+ label: "Execute",
+ color: "text-green-400",
+ borderColor: "border-green-400/50",
+ },
+ review: {
+ label: "Review",
+ color: "text-yellow-400",
+ borderColor: "border-yellow-400/50",
+ },
+};
+
+export interface PhaseConfirmationData {
+ questionId: string;
+ contractId: string;
+ contractName?: string;
+ currentPhase: ContractPhase;
+ nextPhase: ContractPhase;
+ summary?: string;
+ deliverables?: Array<{
+ name: string;
+ completed: boolean;
+ }>;
+}
+
+interface PhaseConfirmationModalProps {
+ data: PhaseConfirmationData;
+ onApprove: (questionId: string) => Promise<void>;
+ onRequestChanges: (questionId: string, feedback: string) => Promise<void>;
+ onDismiss?: () => void;
+}
+
+export function PhaseConfirmationModal({
+ data,
+ onApprove,
+ onRequestChanges,
+ onDismiss,
+}: PhaseConfirmationModalProps) {
+ const [mode, setMode] = useState<"choice" | "feedback">("choice");
+ const [feedback, setFeedback] = useState("");
+ const [submitting, setSubmitting] = useState(false);
+ const [error, setError] = useState<string | null>(null);
+
+ const currentConfig = phaseConfig[data.currentPhase];
+ const nextConfig = phaseConfig[data.nextPhase];
+
+ const handleApprove = async () => {
+ setSubmitting(true);
+ setError(null);
+ try {
+ await onApprove(data.questionId);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to approve");
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const handleRequestChanges = () => {
+ setMode("feedback");
+ };
+
+ const handleSubmitFeedback = async () => {
+ if (!feedback.trim()) return;
+ setSubmitting(true);
+ setError(null);
+ try {
+ await onRequestChanges(data.questionId, feedback.trim());
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to submit feedback");
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const handleBack = () => {
+ setMode("choice");
+ setFeedback("");
+ setError(null);
+ };
+
+ return (
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm">
+ <div className="w-full max-w-lg mx-4 bg-[#0a1628] border border-[rgba(117,170,252,0.3)] shadow-2xl">
+ {/* Header */}
+ <div className="flex items-center justify-between p-4 border-b border-[rgba(117,170,252,0.2)]">
+ <div className="flex items-center gap-2">
+ <span className="text-amber-400 text-lg">?</span>
+ <h2 className="font-mono text-sm text-[#75aafc] uppercase tracking-wide">
+ Phase Transition Confirmation
+ </h2>
+ </div>
+ {onDismiss && (
+ <button
+ onClick={onDismiss}
+ className="text-[#555] hover:text-[#9bc3ff] transition-colors"
+ aria-label="Dismiss"
+ >
+ <span className="text-xl">&times;</span>
+ </button>
+ )}
+ </div>
+
+ {/* Content */}
+ <div className="p-4 space-y-4">
+ {/* Contract name */}
+ {data.contractName && (
+ <p className="font-mono text-sm text-[#9bc3ff]">
+ Contract: <span className="text-[#dbe7ff]">{data.contractName}</span>
+ </p>
+ )}
+
+ {/* Phase transition indicator */}
+ <div className="flex items-center justify-center gap-3 py-4">
+ <div
+ className={`px-4 py-2 border ${currentConfig.borderColor} ${currentConfig.color} font-mono text-sm uppercase`}
+ >
+ {currentConfig.label}
+ </div>
+ <div className="text-[#555] font-mono">&rarr;</div>
+ <div
+ className={`px-4 py-2 border ${nextConfig.borderColor} ${nextConfig.color} font-mono text-sm uppercase`}
+ >
+ {nextConfig.label}
+ </div>
+ </div>
+
+ {/* Summary */}
+ {data.summary && (
+ <div className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.15)] p-3">
+ <p className="font-mono text-xs text-[#9bc3ff] mb-2 uppercase">
+ Phase Summary
+ </p>
+ <p className="font-mono text-sm text-[#dbe7ff] whitespace-pre-wrap">
+ {data.summary}
+ </p>
+ </div>
+ )}
+
+ {/* Deliverables checklist */}
+ {data.deliverables && data.deliverables.length > 0 && (
+ <div className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.15)] p-3">
+ <p className="font-mono text-xs text-[#9bc3ff] mb-2 uppercase">
+ Phase Deliverables
+ </p>
+ <ul className="space-y-1">
+ {data.deliverables.map((d, idx) => (
+ <li key={idx} className="flex items-center gap-2">
+ <span
+ className={
+ d.completed ? "text-green-400" : "text-[#555]"
+ }
+ >
+ {d.completed ? "+" : "-"}
+ </span>
+ <span
+ className={`font-mono text-sm ${
+ d.completed ? "text-[#9bc3ff]" : "text-[#555]"
+ }`}
+ >
+ {d.name}
+ </span>
+ </li>
+ ))}
+ </ul>
+ </div>
+ )}
+
+ {/* Error message */}
+ {error && (
+ <div className="bg-red-900/20 border border-red-500/30 p-3 text-red-400 font-mono text-sm">
+ {error}
+ </div>
+ )}
+
+ {/* Choice mode */}
+ {mode === "choice" && (
+ <div className="space-y-3 pt-4 border-t border-[rgba(117,170,252,0.2)]">
+ <p className="font-mono text-sm text-[#dbe7ff]">
+ Ready to advance to the {nextConfig.label.toLowerCase()} phase?
+ </p>
+ <div className="flex gap-3">
+ <button
+ onClick={handleApprove}
+ disabled={submitting}
+ className="flex-1 px-4 py-2.5 font-mono text-sm text-[#dbe7ff] bg-green-700/30 border border-green-400/50 hover:bg-green-700/40 hover:border-green-400/70 disabled:opacity-50 disabled:cursor-not-allowed transition-colors uppercase"
+ >
+ {submitting ? "Advancing..." : "Approve & Advance"}
+ </button>
+ <button
+ onClick={handleRequestChanges}
+ disabled={submitting}
+ className="flex-1 px-4 py-2.5 font-mono text-sm text-amber-300 border border-amber-500/50 hover:border-amber-400/70 hover:bg-amber-900/20 disabled:opacity-50 disabled:cursor-not-allowed transition-colors uppercase"
+ >
+ Request Changes
+ </button>
+ </div>
+ </div>
+ )}
+
+ {/* Feedback mode */}
+ {mode === "feedback" && (
+ <div className="space-y-3 pt-4 border-t border-[rgba(117,170,252,0.2)]">
+ <div>
+ <label className="block font-mono text-xs text-[#9bc3ff] uppercase mb-2">
+ What changes are needed before advancing?
+ </label>
+ <textarea
+ value={feedback}
+ onChange={(e) => setFeedback(e.target.value)}
+ placeholder="Describe what needs to be changed or completed..."
+ rows={4}
+ 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>
+ <div className="flex gap-3">
+ <button
+ onClick={handleBack}
+ disabled={submitting}
+ className="px-4 py-2 font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors uppercase"
+ >
+ &larr; Back
+ </button>
+ <button
+ onClick={handleSubmitFeedback}
+ disabled={submitting || !feedback.trim()}
+ className="flex-1 px-4 py-2 font-mono text-sm text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] disabled:opacity-50 disabled:cursor-not-allowed transition-colors uppercase"
+ >
+ {submitting ? "Submitting..." : "Submit Feedback"}
+ </button>
+ </div>
+ </div>
+ )}
+ </div>
+ </div>
+ </div>
+ );
+}
+
+/** Inline variant for task output view */
+interface PhaseConfirmationInlineProps {
+ data: PhaseConfirmationData;
+ isPending: boolean;
+ onApprove: (questionId: string) => Promise<void>;
+ onRequestChanges: (questionId: string, feedback: string) => Promise<void>;
+}
+
+export function PhaseConfirmationInline({
+ data,
+ isPending,
+ onApprove,
+ onRequestChanges,
+}: PhaseConfirmationInlineProps) {
+ const [mode, setMode] = useState<"choice" | "feedback">("choice");
+ const [feedback, setFeedback] = useState("");
+ const [submitting, setSubmitting] = useState(false);
+
+ const currentConfig = phaseConfig[data.currentPhase];
+ const nextConfig = phaseConfig[data.nextPhase];
+
+ const handleApprove = async () => {
+ setSubmitting(true);
+ try {
+ await onApprove(data.questionId);
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const handleSubmitFeedback = async () => {
+ if (!feedback.trim()) return;
+ setSubmitting(true);
+ try {
+ await onRequestChanges(data.questionId, feedback.trim());
+ setFeedback("");
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ return (
+ <div className="bg-[#0f1825] border border-[rgba(117,170,252,0.4)] rounded p-3 my-2">
+ <div className="flex items-center gap-2 mb-3">
+ <span className="text-[#75aafc] text-lg">?</span>
+ <span className="font-mono text-sm text-[#75aafc] uppercase">
+ Phase Transition
+ </span>
+ {!isPending && (
+ <span className="text-green-400 text-xs font-mono">(Responded)</span>
+ )}
+ </div>
+
+ {/* Phase transition indicator */}
+ <div className="flex items-center gap-2 mb-3">
+ <span className={`font-mono text-sm ${currentConfig.color}`}>
+ {currentConfig.label}
+ </span>
+ <span className="text-[#555] font-mono">&rarr;</span>
+ <span className={`font-mono text-sm ${nextConfig.color}`}>
+ {nextConfig.label}
+ </span>
+ </div>
+
+ {/* Summary */}
+ {data.summary && (
+ <p className="font-mono text-xs text-[#9bc3ff] mb-3 whitespace-pre-wrap">
+ {data.summary}
+ </p>
+ )}
+
+ {/* Deliverables */}
+ {data.deliverables && data.deliverables.length > 0 && (
+ <div className="mb-3">
+ <p className="font-mono text-[10px] text-[#555] uppercase mb-1">
+ Deliverables
+ </p>
+ <div className="flex flex-wrap gap-2">
+ {data.deliverables.map((d, idx) => (
+ <span
+ key={idx}
+ className={`px-2 py-0.5 font-mono text-xs border ${
+ d.completed
+ ? "border-green-400/30 text-green-400"
+ : "border-[#555] text-[#555]"
+ }`}
+ >
+ {d.completed ? "+" : "-"} {d.name}
+ </span>
+ ))}
+ </div>
+ </div>
+ )}
+
+ {isPending && (
+ <>
+ {mode === "choice" && (
+ <div className="flex flex-wrap gap-2">
+ <button
+ onClick={handleApprove}
+ disabled={submitting}
+ className="px-3 py-1.5 font-mono text-xs bg-green-700/30 border border-green-400/50 text-green-300 hover:bg-green-700/40 disabled:opacity-50 transition-colors"
+ >
+ {submitting ? "..." : "Approve & Advance"}
+ </button>
+ <button
+ onClick={() => setMode("feedback")}
+ disabled={submitting}
+ className="px-3 py-1.5 font-mono text-xs border border-amber-500/50 text-amber-300 hover:bg-amber-900/20 disabled:opacity-50 transition-colors"
+ >
+ Request Changes
+ </button>
+ </div>
+ )}
+
+ {mode === "feedback" && (
+ <div className="space-y-2">
+ <textarea
+ value={feedback}
+ onChange={(e) => setFeedback(e.target.value)}
+ placeholder="Describe what changes are needed..."
+ rows={2}
+ className="w-full px-2 py-1.5 bg-[#0a1525] border border-[rgba(117,170,252,0.3)] text-[#dbe7ff] font-mono text-xs focus:outline-none focus:border-[#75aafc] resize-none"
+ autoFocus
+ />
+ <div className="flex gap-2">
+ <button
+ onClick={() => setMode("choice")}
+ disabled={submitting}
+ className="px-2 py-1 font-mono text-[10px] text-[#555] hover:text-[#9bc3ff] transition-colors"
+ >
+ Cancel
+ </button>
+ <button
+ onClick={handleSubmitFeedback}
+ disabled={submitting || !feedback.trim()}
+ className="px-3 py-1 font-mono text-xs bg-[#0f3c78] border border-[#3f6fb3] text-[#dbe7ff] hover:bg-[#153667] disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
+ >
+ {submitting ? "..." : "Submit"}
+ </button>
+ </div>
+ </div>
+ )}
+ </>
+ )}
+ </div>
+ );
+}