From 6b07707a4cc99c7e127a2bf6a0ca790fa033b5f5 Mon Sep 17 00:00:00 2001 From: soryu Date: Sat, 17 Jan 2026 06:44:22 +0000 Subject: feat(frontend): Add UI for phase transition confirmation requests When phase_guard is enabled and a supervisor tries to advance the contract phase, users now see a confirmation modal with: - Current and proposed next phase visualization - Phase deliverables checklist (if available) - Summary of the phase work - Options to "Approve & Advance" or "Request Changes" with feedback Components added: - PhaseConfirmationModal: Full modal dialog for phase confirmations - PhaseConfirmationInline: Inline variant for task output view - PhaseConfirmationNotification: Global notification wrapper - PhaseConfirmationToast: Alternative toast-style notification Integration: - Added phase_confirmation message type to TaskOutput renderer - Extended PendingQuestion API type with phase confirmation data - Integrated notification into main app layout The UI uses the existing supervisor question infrastructure (polling via /api/v1/mesh/questions) and responds with APPROVE or CHANGES_REQUESTED prefixed feedback. Co-Authored-By: Claude Opus 4.5 --- .../components/PhaseConfirmationNotification.tsx | 141 +++++++ .../contracts/PhaseConfirmationModal.tsx | 411 +++++++++++++++++++++ makima/frontend/src/components/mesh/TaskOutput.tsx | 66 ++++ makima/frontend/src/lib/api.ts | 13 + makima/frontend/src/main.tsx | 2 + 5 files changed, 633 insertions(+) create mode 100644 makima/frontend/src/components/PhaseConfirmationNotification.tsx create mode 100644 makima/frontend/src/components/contracts/PhaseConfirmationModal.tsx diff --git a/makima/frontend/src/components/PhaseConfirmationNotification.tsx b/makima/frontend/src/components/PhaseConfirmationNotification.tsx new file mode 100644 index 0000000..516211f --- /dev/null +++ b/makima/frontend/src/components/PhaseConfirmationNotification.tsx @@ -0,0 +1,141 @@ +import { useNavigate } from "react-router"; +import { useSupervisorQuestions } from "../contexts/SupervisorQuestionsContext"; +import { PhaseConfirmationModal, type PhaseConfirmationData } from "./contracts/PhaseConfirmationModal"; +import type { PendingQuestion } from "../lib/api"; + +/** + * Notification component for phase confirmation requests. + * Shows a modal when there are pending phase_confirmation type questions. + * Uses the same question infrastructure as supervisor questions. + */ +export function PhaseConfirmationNotification() { + const { notificationQuestions, submitAnswer, dismissNotification } = + useSupervisorQuestions(); + + // Filter for phase_confirmation type questions + const phaseConfirmationQuestions = notificationQuestions.filter( + (q) => q.questionType === "phase_confirmation" + ); + + if (phaseConfirmationQuestions.length === 0) { + return null; + } + + // Show the first phase confirmation question as a modal + const question = phaseConfirmationQuestions[0]; + + // Build phase confirmation data from the question + const data: PhaseConfirmationData = { + questionId: question.questionId, + contractId: question.contractId, + contractName: question.phaseConfirmation?.contractName, + currentPhase: question.phaseConfirmation?.currentPhase || "research", + nextPhase: question.phaseConfirmation?.nextPhase || "specify", + summary: question.phaseConfirmation?.summary, + deliverables: question.phaseConfirmation?.deliverables, + }; + + const handleApprove = async (questionId: string) => { + const success = await submitAnswer(questionId, "APPROVE"); + if (success) { + dismissNotification(questionId); + } + }; + + const handleRequestChanges = async (questionId: string, feedback: string) => { + const success = await submitAnswer( + questionId, + `CHANGES_REQUESTED: ${feedback}` + ); + if (success) { + dismissNotification(questionId); + } + }; + + const handleDismiss = () => { + // Dismiss to notification (user can still respond via task output) + dismissNotification(question.questionId); + }; + + return ( + + ); +} + +/** + * Alternative: Notification toast-style for phase confirmations + * Shows as a small notification in the corner (like regular supervisor questions) + */ +export function PhaseConfirmationToast() { + const navigate = useNavigate(); + const { notificationQuestions, dismissNotification } = useSupervisorQuestions(); + + // Filter for phase_confirmation type questions + const phaseConfirmationQuestions = notificationQuestions.filter( + (q) => q.questionType === "phase_confirmation" + ); + + if (phaseConfirmationQuestions.length === 0) { + return null; + } + + const handleGoToTask = (question: PendingQuestion) => { + dismissNotification(question.questionId); + navigate(`/mesh/${question.taskId}`); + }; + + return ( +
+ {phaseConfirmationQuestions.map((question) => ( +
+ {/* Header */} +
+
+ ? + + Phase Transition + +
+ +
+ + {/* Content preview */} +
+ {question.phaseConfirmation && ( +
+ + {question.phaseConfirmation.currentPhase} + + + + {question.phaseConfirmation.nextPhase} + +
+ )} +

+ {question.question} +

+ {question.phaseConfirmation?.contractName && ( +

+ Contract: {question.phaseConfirmation.contractName} +

+ )} +
+
+ ))} +
+ ); +} 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; + onRequestChanges: (questionId: string, feedback: string) => Promise; + 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(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 ( +
+
+ {/* Header */} +
+
+ ? +

+ Phase Transition Confirmation +

+
+ {onDismiss && ( + + )} +
+ + {/* Content */} +
+ {/* Contract name */} + {data.contractName && ( +

+ Contract: {data.contractName} +

+ )} + + {/* Phase transition indicator */} +
+
+ {currentConfig.label} +
+
+
+ {nextConfig.label} +
+
+ + {/* Summary */} + {data.summary && ( +
+

+ Phase Summary +

+

+ {data.summary} +

+
+ )} + + {/* Deliverables checklist */} + {data.deliverables && data.deliverables.length > 0 && ( +
+

+ Phase Deliverables +

+
    + {data.deliverables.map((d, idx) => ( +
  • + + {d.completed ? "+" : "-"} + + + {d.name} + +
  • + ))} +
+
+ )} + + {/* Error message */} + {error && ( +
+ {error} +
+ )} + + {/* Choice mode */} + {mode === "choice" && ( +
+

+ Ready to advance to the {nextConfig.label.toLowerCase()} phase? +

+
+ + +
+
+ )} + + {/* Feedback mode */} + {mode === "feedback" && ( +
+
+ +