summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/mesh
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/components/mesh')
-rw-r--r--makima/frontend/src/components/mesh/ContractCompleteQuestion.tsx165
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>
+ );
+}