diff options
| author | soryu <soryu@soryu.co> | 2026-01-20 23:20:32 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-20 23:20:32 +0000 |
| commit | 7155e6cd7ddf25a5a4d4f6d6abecd49f0cf519dc (patch) | |
| tree | 2475a951c4bcba685b010909bf4abd5351cb3623 /makima/frontend/src/components/mesh/ContractCompleteQuestion.tsx | |
| parent | 055e2c4a72e3b2331a18fdc9f8132ef990af7e38 (diff) | |
| download | soryu-7155e6cd7ddf25a5a4d4f6d6abecd49f0cf519dc.tar.gz soryu-7155e6cd7ddf25a5a4d4f6d6abecd49f0cf519dc.zip | |
Add non-blocking persistent contract completion questions (#14)
* [WIP] Heartbeat checkpoint - 2026-01-20 22:40:37 UTC
* Task completion checkpoint
Diffstat (limited to 'makima/frontend/src/components/mesh/ContractCompleteQuestion.tsx')
| -rw-r--r-- | makima/frontend/src/components/mesh/ContractCompleteQuestion.tsx | 165 |
1 files changed, 165 insertions, 0 deletions
diff --git a/makima/frontend/src/components/mesh/ContractCompleteQuestion.tsx b/makima/frontend/src/components/mesh/ContractCompleteQuestion.tsx new file mode 100644 index 0000000..d4ef618 --- /dev/null +++ b/makima/frontend/src/components/mesh/ContractCompleteQuestion.tsx @@ -0,0 +1,165 @@ +import { useState } from "react"; +import type { PendingQuestion } from "../../lib/api"; + +interface ContractCompleteQuestionProps { + question: PendingQuestion; + onAnswer: (questionId: string, response: string) => Promise<void>; +} + +/** + * Component for displaying contract_complete type questions prominently on the task page. + * These questions persist until answered and are not shown as floating notifications. + */ +export function ContractCompleteQuestion({ + question, + onAnswer, +}: ContractCompleteQuestionProps) { + const [submitting, setSubmitting] = useState(false); + const [minimized, setMinimized] = useState(false); + const [customInput, setCustomInput] = useState(""); + const [showCustom, setShowCustom] = useState(false); + + const handleAnswer = async (response: string) => { + if (submitting) return; + setSubmitting(true); + try { + await onAnswer(question.questionId, response); + } finally { + setSubmitting(false); + } + }; + + const handleCustomSubmit = async () => { + if (!customInput.trim() || submitting) return; + await handleAnswer(customInput.trim()); + setCustomInput(""); + setShowCustom(false); + }; + + // Default choices for contract completion questions + const defaultChoices = + question.choices.length > 0 + ? question.choices + : ["Yes, contract is complete", "No, more work needed"]; + + if (minimized) { + return ( + <div className="fixed bottom-4 left-4 z-40"> + <button + onClick={() => setMinimized(false)} + className="flex items-center gap-2 px-4 py-2 bg-green-600 hover:bg-green-500 text-white font-mono text-sm rounded-lg shadow-lg transition-colors" + > + <span className="w-2 h-2 bg-white rounded-full animate-pulse" /> + Contract Review Pending + </button> + </div> + ); + } + + return ( + <div className="bg-gradient-to-r from-green-900/40 to-emerald-900/40 border-2 border-green-500/60 rounded-lg shadow-xl my-4 overflow-hidden"> + {/* Header */} + <div className="flex items-center justify-between px-4 py-3 bg-green-900/50 border-b border-green-500/30"> + <div className="flex items-center gap-3"> + <div className="w-8 h-8 flex items-center justify-center bg-green-500/20 rounded-full"> + <span className="text-green-400 text-xl">?</span> + </div> + <div> + <h3 className="font-mono text-sm text-green-300 uppercase tracking-wide"> + Contract Completion Review + </h3> + <p className="text-xs text-green-400/60"> + Please review and respond + </p> + </div> + </div> + <button + onClick={() => setMinimized(true)} + className="px-2 py-1 text-xs font-mono text-green-400/70 hover:text-green-300 border border-green-500/30 hover:border-green-400/50 rounded transition-colors" + title="Minimize (question will remain pending)" + > + Minimize + </button> + </div> + + {/* Content */} + <div className="p-4 space-y-4"> + {/* Context */} + {question.context && ( + <div className="text-xs text-green-300/70 font-mono uppercase tracking-wide"> + {question.context} + </div> + )} + + {/* Question */} + <div className="text-green-100 font-mono text-base leading-relaxed"> + {question.question} + </div> + + {/* Choices */} + <div className="flex flex-wrap gap-3 pt-2"> + {defaultChoices.map((choice, idx) => ( + <button + key={idx} + onClick={() => handleAnswer(choice)} + disabled={submitting} + className={`px-4 py-2.5 font-mono text-sm border rounded-md transition-all disabled:opacity-50 disabled:cursor-not-allowed ${ + idx === 0 + ? "bg-green-500/20 border-green-400/60 hover:bg-green-500/30 text-green-100 hover:border-green-400" + : "bg-amber-500/20 border-amber-400/60 hover:bg-amber-500/30 text-amber-100 hover:border-amber-400" + }`} + > + {submitting ? "..." : choice} + </button> + ))} + </div> + + {/* Custom input option */} + {!showCustom ? ( + <button + onClick={() => setShowCustom(true)} + className="text-xs text-green-400/70 hover:text-green-300 font-mono transition-colors" + > + + Provide custom response + </button> + ) : ( + <div className="flex gap-2 pt-2"> + <input + type="text" + value={customInput} + onChange={(e) => setCustomInput(e.target.value)} + placeholder="Type your response..." + disabled={submitting} + className="flex-1 px-3 py-2 bg-[#0a1525] border border-green-500/30 text-green-100 text-sm font-mono rounded focus:outline-none focus:border-green-400" + onKeyDown={(e) => { + if (e.key === "Enter" && customInput.trim()) { + handleCustomSubmit(); + } + if (e.key === "Escape") { + setShowCustom(false); + setCustomInput(""); + } + }} + /> + <button + onClick={handleCustomSubmit} + disabled={submitting || !customInput.trim()} + className="px-4 py-2 bg-green-500 text-black text-sm font-medium rounded disabled:opacity-50 disabled:cursor-not-allowed transition-colors hover:bg-green-400" + > + {submitting ? "..." : "Submit"} + </button> + <button + onClick={() => { + setShowCustom(false); + setCustomInput(""); + }} + className="px-2 py-2 text-green-400/70 hover:text-green-300 text-sm font-mono" + > + Cancel + </button> + </div> + )} + </div> + </div> + ); +} |
