diff options
Diffstat (limited to 'makima/frontend/src/components/mesh')
| -rw-r--r-- | makima/frontend/src/components/mesh/TaskOutput.tsx | 151 |
1 files changed, 148 insertions, 3 deletions
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); |
