diff options
Diffstat (limited to 'makima/frontend/src/components/contracts/PhaseConfirmationModal.tsx')
| -rw-r--r-- | makima/frontend/src/components/contracts/PhaseConfirmationModal.tsx | 411 |
1 files changed, 411 insertions, 0 deletions
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">×</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">→</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" + > + ← 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">→</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> + ); +} |
