diff options
Diffstat (limited to 'makima/frontend/src/components')
| -rw-r--r-- | makima/frontend/src/components/SupervisorQuestionNotification.tsx | 106 | ||||
| -rw-r--r-- | makima/frontend/src/components/mesh/TaskOutput.tsx | 151 |
2 files changed, 161 insertions, 96 deletions
diff --git a/makima/frontend/src/components/SupervisorQuestionNotification.tsx b/makima/frontend/src/components/SupervisorQuestionNotification.tsx index 6a71de2..1457d86 100644 --- a/makima/frontend/src/components/SupervisorQuestionNotification.tsx +++ b/makima/frontend/src/components/SupervisorQuestionNotification.tsx @@ -1,50 +1,22 @@ -import { useState } from "react"; import { useNavigate } from "react-router"; import { useSupervisorQuestions } from "../contexts/SupervisorQuestionsContext"; -import type { PendingQuestion } from "../lib/api"; export function SupervisorQuestionNotification() { const navigate = useNavigate(); - const { pendingQuestions, submitAnswer } = useSupervisorQuestions(); - const [expandedQuestion, setExpandedQuestion] = useState<string | null>(null); - const [response, setResponse] = useState(""); - const [submitting, setSubmitting] = useState(false); + const { notificationQuestions, dismissNotification } = useSupervisorQuestions(); - if (pendingQuestions.length === 0) { + if (notificationQuestions.length === 0) { return null; } - const handleGoToTask = (taskId: string) => { + const handleGoToTask = (questionId: string, taskId: string) => { + dismissNotification(questionId); navigate(`/mesh/${taskId}`); }; - const handleExpand = (questionId: string) => { - setExpandedQuestion(expandedQuestion === questionId ? null : questionId); - setResponse(""); - }; - - const handleSubmit = async (question: PendingQuestion) => { - if (!response.trim()) return; - - setSubmitting(true); - const success = await submitAnswer(question.questionId, response.trim()); - setSubmitting(false); - - if (success) { - setExpandedQuestion(null); - setResponse(""); - } - }; - - const handleChoiceSelect = async (question: PendingQuestion, choice: string) => { - setSubmitting(true); - await submitAnswer(question.questionId, choice); - setSubmitting(false); - }; - return ( <div className="fixed bottom-4 right-4 z-50 max-w-md space-y-2"> - {pendingQuestions.map((question) => ( + {notificationQuestions.map((question) => ( <div key={question.questionId} className="bg-[#0d1b2d] border border-amber-500/50 rounded-lg shadow-lg overflow-hidden" @@ -54,24 +26,15 @@ export function SupervisorQuestionNotification() { <div className="flex items-center gap-2"> <span className="text-amber-400 text-lg">?</span> <span className="font-mono text-sm text-amber-300 uppercase"> - Supervisor Question + Task needs input </span> </div> - <div className="flex items-center gap-2"> - <button - onClick={() => handleGoToTask(question.taskId)} - className="px-2 py-1 font-mono text-xs text-amber-400 hover:text-amber-300 transition-colors" - title="Go to task" - > - View Task - </button> - <button - onClick={() => handleExpand(question.questionId)} - className="px-2 py-1 font-mono text-xs text-amber-400 border border-amber-500/30 hover:border-amber-400/50 transition-colors uppercase" - > - {expandedQuestion === question.questionId ? "Collapse" : "Answer"} - </button> - </div> + <button + onClick={() => handleGoToTask(question.questionId, question.taskId)} + className="px-3 py-1 font-mono text-xs text-amber-400 border border-amber-500/30 hover:border-amber-400/50 hover:bg-amber-900/20 transition-colors uppercase" + > + View Task + </button> </div> {/* Question preview */} @@ -81,53 +44,10 @@ export function SupervisorQuestionNotification() { {question.context} </div> )} - <p className="text-sm text-[#dbe7ff] font-mono"> + <p className="text-sm text-[#dbe7ff] font-mono line-clamp-2"> {question.question} </p> </div> - - {/* Expanded answer section */} - {expandedQuestion === question.questionId && ( - <div className="px-4 pb-4 border-t border-amber-500/20 pt-3"> - {question.choices.length > 0 ? ( - // Choice buttons - <div className="space-y-2"> - <p className="text-xs text-[#8b949e] font-mono uppercase mb-2"> - Select an option: - </p> - {question.choices.map((choice, idx) => ( - <button - key={idx} - onClick={() => handleChoiceSelect(question, choice)} - disabled={submitting} - className="w-full px-3 py-2 text-left font-mono text-sm text-[#dbe7ff] bg-[#0a1628] border border-[#3f6fb3] hover:border-amber-400/50 hover:bg-amber-900/20 disabled:opacity-50 transition-colors" - > - {choice} - </button> - ))} - </div> - ) : ( - // Free-form text input - <div className="space-y-2"> - <textarea - value={response} - onChange={(e) => setResponse(e.target.value)} - placeholder="Type your response..." - rows={3} - className="w-full px-3 py-2 bg-[#0a1628] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-amber-400 resize-none" - disabled={submitting} - /> - <button - onClick={() => handleSubmit(question)} - disabled={submitting || !response.trim()} - className="w-full px-4 py-2 font-mono text-xs text-[#0a1628] bg-amber-500 hover:bg-amber-400 disabled:bg-amber-700 disabled:cursor-not-allowed transition-colors uppercase" - > - {submitting ? "Submitting..." : "Submit Response"} - </button> - </div> - )} - </div> - )} </div> ))} </div> diff --git a/makima/frontend/src/components/mesh/TaskOutput.tsx b/makima/frontend/src/components/mesh/TaskOutput.tsx index cb0eba3..d53429d 100644 --- a/makima/frontend/src/components/mesh/TaskOutput.tsx +++ b/makima/frontend/src/components/mesh/TaskOutput.tsx @@ -16,9 +16,23 @@ interface TaskOutputProps { taskId?: string | null; /** Callback when user sends input (to show it immediately in output) */ onUserInput?: (message: string) => void; + /** Set of pending question IDs (for supervisor questions) */ + pendingQuestionIds?: Set<string>; + /** Callback to answer a supervisor question */ + onAnswerQuestion?: (questionId: string, response: string) => Promise<void>; } -export function TaskOutput({ entries, isStreaming, viewingSubtaskName, onClearSubtaskView, onClear, taskId, onUserInput }: TaskOutputProps) { +export function TaskOutput({ + entries, + isStreaming, + viewingSubtaskName, + onClearSubtaskView, + onClear, + taskId, + onUserInput, + pendingQuestionIds, + onAnswerQuestion, +}: TaskOutputProps) { const containerRef = useRef<HTMLDivElement>(null); const [autoScroll, setAutoScroll] = useState(true); const [inputValue, setInputValue] = useState(""); @@ -135,7 +149,12 @@ export function TaskOutput({ entries, isStreaming, viewingSubtaskName, onClearSu ) : ( <div className="space-y-3"> {entries.map((entry, idx) => ( - <OutputEntryRenderer key={idx} entry={entry} /> + <OutputEntryRenderer + key={idx} + entry={entry} + pendingQuestionIds={pendingQuestionIds} + onAnswerQuestion={onAnswerQuestion} + /> ))} {isStreaming && ( <span className="inline-block w-2 h-4 bg-[#9bc3ff] animate-pulse" /> @@ -177,7 +196,13 @@ export function TaskOutput({ entries, isStreaming, viewingSubtaskName, onClearSu ); } -function OutputEntryRenderer({ entry }: { entry: TaskOutputEvent }) { +interface OutputEntryRendererProps { + entry: TaskOutputEvent; + pendingQuestionIds?: Set<string>; + onAnswerQuestion?: (questionId: string, response: string) => Promise<void>; +} + +function OutputEntryRenderer({ entry, pendingQuestionIds, onAnswerQuestion }: OutputEntryRendererProps) { const [expanded, setExpanded] = useState(false); switch (entry.messageType) { @@ -278,11 +303,131 @@ function OutputEntryRenderer({ entry }: { entry: TaskOutputEvent }) { case "auth_required": return <AuthRequiredEntry entry={entry} />; + case "supervisor_question": + return ( + <SupervisorQuestionEntry + entry={entry} + pendingQuestionIds={pendingQuestionIds} + onAnswerQuestion={onAnswerQuestion} + /> + ); + default: return null; } } +function SupervisorQuestionEntry({ + entry, + pendingQuestionIds, + onAnswerQuestion, +}: { + entry: TaskOutputEvent; + pendingQuestionIds?: Set<string>; + onAnswerQuestion?: (questionId: string, response: string) => Promise<void>; +}) { + const questionId = entry.toolInput?.question_id as string; + const choices = (entry.toolInput?.choices as string[]) || []; + const context = entry.toolInput?.context as string | null; + + const [customInput, setCustomInput] = useState(""); + const [showOther, setShowOther] = useState(false); + const [submitting, setSubmitting] = useState(false); + + const isPending = pendingQuestionIds?.has(questionId) ?? false; + + const handleChoiceSelect = async (choice: string) => { + if (!onAnswerQuestion || submitting) return; + setSubmitting(true); + try { + await onAnswerQuestion(questionId, choice); + } finally { + setSubmitting(false); + } + }; + + const handleOtherSubmit = async () => { + if (!onAnswerQuestion || !customInput.trim() || submitting) return; + setSubmitting(true); + try { + await onAnswerQuestion(questionId, customInput.trim()); + setCustomInput(""); + } finally { + setSubmitting(false); + } + }; + + return ( + <div className="bg-amber-900/20 border border-amber-500/50 rounded p-3 my-2"> + <div className="flex items-center gap-2 text-amber-400 font-semibold mb-2"> + <span>?</span> + <span>Question</span> + {!isPending && ( + <span className="text-green-400 text-xs font-normal">(Answered)</span> + )} + </div> + + {context && ( + <p className="text-amber-200/60 text-xs mb-2 uppercase">{context}</p> + )} + + <p className="text-amber-100 mb-3">{entry.content}</p> + + {isPending && ( + <div className="space-y-2"> + {choices.length > 0 && ( + <div className="flex flex-wrap gap-2"> + {choices.map((choice, idx) => ( + <button + key={idx} + onClick={() => handleChoiceSelect(choice)} + disabled={submitting} + className="px-3 py-1.5 text-sm font-mono bg-amber-500/20 border border-amber-500/50 hover:bg-amber-500/30 disabled:opacity-50 text-amber-100 transition-colors" + > + {choice} + </button> + ))} + </div> + )} + + {/* Other option */} + {!showOther ? ( + <button + onClick={() => setShowOther(true)} + className="text-xs text-amber-400 hover:text-amber-300 transition-colors" + > + + Other (custom response) + </button> + ) : ( + <div className="flex gap-2"> + <input + type="text" + value={customInput} + onChange={(e) => setCustomInput(e.target.value)} + placeholder="Type custom response..." + disabled={submitting} + className="flex-1 px-2 py-1 bg-[#0a1525] border border-amber-500/30 text-amber-100 text-sm rounded focus:outline-none focus:border-amber-400" + onKeyDown={(e) => { + if (e.key === "Enter" && customInput.trim()) { + handleOtherSubmit(); + } + }} + /> + <button + onClick={handleOtherSubmit} + disabled={submitting || !customInput.trim()} + className="px-3 py-1 bg-amber-500 text-black text-sm font-medium rounded disabled:opacity-50 disabled:cursor-not-allowed transition-colors hover:bg-amber-400" + > + {submitting ? "..." : "Submit"} + </button> + </div> + )} + </div> + )} + </div> + ); +} + function AuthRequiredEntry({ entry }: { entry: TaskOutputEvent }) { const [authCode, setAuthCode] = useState(""); const [submitting, setSubmitting] = useState(false); |
