summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/contracts/ContractCliInput.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/components/contracts/ContractCliInput.tsx')
-rw-r--r--makima/frontend/src/components/contracts/ContractCliInput.tsx974
1 files changed, 0 insertions, 974 deletions
diff --git a/makima/frontend/src/components/contracts/ContractCliInput.tsx b/makima/frontend/src/components/contracts/ContractCliInput.tsx
deleted file mode 100644
index 54d9f3a..0000000
--- a/makima/frontend/src/components/contracts/ContractCliInput.tsx
+++ /dev/null
@@ -1,974 +0,0 @@
-import { useState, useCallback, useRef, useEffect, useMemo } from "react";
-import {
- getContractChatHistory,
- clearContractChatHistory,
- startTask,
- sendTaskMessage,
- type UserQuestion,
- type ContractWithRelations,
- type TaskStatus,
-} from "../../lib/api";
-import { SimpleMarkdown } from "../SimpleMarkdown";
-import {
- QuickActionButtons,
- type QuickAction,
-} from "./QuickActionButtons";
-import { TaskDerivationPreview, type ParsedTask } from "./TaskDerivationPreview";
-import { useTaskSubscription, type TaskOutputEvent } from "../../hooks/useTaskSubscription";
-
-interface ContractCliInputProps {
- contractId: string;
- contract: ContractWithRelations;
- onUpdate: () => void;
-}
-
-interface Message {
- id: string;
- type: "user" | "assistant" | "error" | "question";
- content: string;
- toolCalls?: { name: string; success: boolean; message: string }[];
- questions?: UserQuestion[];
- quickActions?: QuickAction[];
-}
-
-export function ContractCliInput({ contractId, contract, onUpdate }: ContractCliInputProps) {
- const [input, setInput] = useState("");
- const [loading, setLoading] = useState(false);
- const [historyLoading, setHistoryLoading] = useState(true);
- const [messages, setMessages] = useState<Message[]>([]);
- const [expanded, setExpanded] = useState(false);
- const [fullscreen, setFullscreen] = useState(false);
- const [pendingQuestions, setPendingQuestions] = useState<UserQuestion[] | null>(null);
- const [userAnswers, setUserAnswers] = useState<Map<string, string[]>>(new Map());
- const [customInputs, setCustomInputs] = useState<Map<string, string>>(new Map());
-
- // Task derivation state
- const [parsedTasks, setParsedTasks] = useState<ParsedTask[] | null>(null);
- const [parsedTaskGroups, setParsedTaskGroups] = useState<string[]>([]);
- const [parsedTasksFileName, setParsedTasksFileName] = useState<string>("");
- const [creatingTasks, setCreatingTasks] = useState(false);
-
- // Supervisor state
- const [supervisorStarting, setSupervisorStarting] = useState(false);
- const [supervisorOutput, setSupervisorOutput] = useState<TaskOutputEvent[]>([]);
- const [supervisorQuestion, setSupervisorQuestion] = useState<{
- id: string;
- question: string;
- options: string[];
- allowMultiple?: boolean;
- allowCustom?: boolean;
- } | null>(null);
-
- const inputRef = useRef<HTMLInputElement>(null);
- const messagesRef = useRef<HTMLDivElement>(null);
-
- // Find the supervisor task for this contract
- // First try by supervisorTaskId on the contract, then fall back to isSupervisor flag
- const supervisorTask = useMemo(() => {
- // Use contract.supervisorTaskId if available (most reliable)
- if (contract.supervisorTaskId) {
- const taskById = contract.tasks.find((t) => t.id === contract.supervisorTaskId);
- if (taskById) return taskById;
- }
- // Fallback to finding by isSupervisor flag
- return contract.tasks.find((t) => t.isSupervisor);
- }, [contract.tasks, contract.supervisorTaskId]);
-
- // Log for debugging
- useEffect(() => {
- console.log("Supervisor lookup:", {
- contractId: contract.id,
- supervisorTaskId: contract.supervisorTaskId,
- tasksCount: contract.tasks.length,
- foundSupervisor: supervisorTask ? { id: supervisorTask.id, status: supervisorTask.status, isSupervisor: supervisorTask.isSupervisor } : null,
- allTasks: contract.tasks.map(t => ({ id: t.id, name: t.name, isSupervisor: t.isSupervisor }))
- });
- }, [contract.id, contract.supervisorTaskId, contract.tasks, supervisorTask]);
-
- const supervisorTaskId = supervisorTask?.id ?? null;
- const supervisorStatus = supervisorTask?.status as TaskStatus | undefined;
- const isSupervisorRunning = supervisorStatus === "running";
- const isSupervisorPending = supervisorStatus === "pending";
-
- // Subscribe to supervisor output when it's running
- const handleSupervisorOutput = useCallback((event: TaskOutputEvent) => {
- // Check for question pattern in output
- // Pattern: {"__supervisor_question__": {"id": "...", "question": "...", "options": [...]}}
- if (!event.isPartial && event.content) {
- const questionMatch = event.content.match(/\{"__supervisor_question__":\s*(\{[^}]+\})\}/);
- if (questionMatch) {
- try {
- const questionData = JSON.parse(questionMatch[1]);
- if (questionData.id && questionData.question && questionData.options) {
- setSupervisorQuestion({
- id: questionData.id,
- question: questionData.question,
- options: questionData.options,
- allowMultiple: questionData.allowMultiple ?? false,
- allowCustom: questionData.allowCustom ?? true,
- });
- // Don't add this to output since it's a control message
- return;
- }
- } catch {
- // Not valid JSON, continue as normal output
- }
- }
- }
-
- setSupervisorOutput((prev) => {
- // If it's a partial message, update the last message
- if (event.isPartial && prev.length > 0) {
- const lastEvent = prev[prev.length - 1];
- if (lastEvent.messageType === event.messageType && lastEvent.isPartial) {
- return [...prev.slice(0, -1), { ...event, content: lastEvent.content + event.content }];
- }
- }
- return [...prev, event];
- });
- }, []);
-
- useTaskSubscription({
- taskId: supervisorTaskId,
- subscribeOutput: isSupervisorRunning,
- onOutput: handleSupervisorOutput,
- });
-
- // Auto-start supervisor function - starts and waits for it to be running
- const ensureSupervisorStarted = useCallback(async (): Promise<boolean> => {
- if (!supervisorTask) {
- console.warn("No supervisor task found for contract");
- return false;
- }
-
- if (isSupervisorRunning) {
- return true; // Already running
- }
-
- if (isSupervisorPending) {
- try {
- setSupervisorStarting(true);
- await startTask(supervisorTask.id);
-
- // Poll for the task to be running (up to 10 seconds)
- for (let i = 0; i < 20; i++) {
- await new Promise(resolve => setTimeout(resolve, 500));
- onUpdate(); // Refresh contract to get updated task status
- // Note: We can't check the new status here directly since state updates are async
- // The UI will update when onUpdate triggers a re-render
- }
-
- // Return true - the caller should check if supervisor is running after this
- return true;
- } catch (err) {
- console.error("Failed to start supervisor:", err);
- return false;
- } finally {
- setSupervisorStarting(false);
- }
- }
-
- // Supervisor exists but is in some other state (paused, done, failed, etc.)
- // Can still send messages to paused tasks
- return supervisorStatus === "paused";
- }, [supervisorTask, isSupervisorRunning, isSupervisorPending, supervisorStatus, onUpdate]);
-
- // Handle answering supervisor questions
- const [supervisorAnswers, setSupervisorAnswers] = useState<string[]>([]);
- const [supervisorCustomInput, setSupervisorCustomInput] = useState("");
-
- const handleSupervisorOptionToggle = useCallback((option: string) => {
- setSupervisorAnswers((prev) => {
- if (supervisorQuestion?.allowMultiple) {
- if (prev.includes(option)) {
- return prev.filter((a) => a !== option);
- }
- return [...prev, option];
- }
- return [option];
- });
- }, [supervisorQuestion?.allowMultiple]);
-
- const handleSubmitSupervisorAnswer = useCallback(async () => {
- if (!supervisorQuestion || !supervisorTask) return;
-
- const customAnswer = supervisorCustomInput.trim();
- const allAnswers = customAnswer
- ? [...supervisorAnswers, customAnswer]
- : supervisorAnswers;
-
- if (allAnswers.length === 0) return;
-
- // Format answer message for supervisor
- const answerMessage = `__supervisor_answer__ ${JSON.stringify({
- id: supervisorQuestion.id,
- answers: allAnswers,
- })}`;
-
- try {
- await sendTaskMessage(supervisorTask.id, answerMessage);
-
- // Add user message to chat
- const userMsgId = Date.now().toString();
- setMessages((prev) => [
- ...prev,
- {
- id: userMsgId,
- type: "user",
- content: `[Answer to: ${supervisorQuestion.question}]\n${allAnswers.join(", ")}`,
- },
- ]);
- } catch (err) {
- console.error("Failed to send supervisor answer:", err);
- } finally {
- setSupervisorQuestion(null);
- setSupervisorAnswers([]);
- setSupervisorCustomInput("");
- }
- }, [supervisorQuestion, supervisorTask, supervisorAnswers, supervisorCustomInput]);
-
- const handleCancelSupervisorQuestion = useCallback(() => {
- setSupervisorQuestion(null);
- setSupervisorAnswers([]);
- setSupervisorCustomInput("");
- }, []);
-
- // Load chat history on mount
- useEffect(() => {
- let mounted = true;
-
- async function loadHistory() {
- try {
- const history = await getContractChatHistory(contractId);
- if (!mounted) return;
-
- // Convert saved messages to display messages
- const displayMessages: Message[] = history.messages.map((msg) => ({
- id: msg.id,
- type: msg.role as "user" | "assistant" | "error",
- content: msg.content,
- toolCalls: msg.toolCalls as { name: string; success: boolean; message: string }[] | undefined,
- }));
-
- setMessages(displayMessages);
-
- // Auto-expand if there's history
- if (displayMessages.length > 0) {
- setExpanded(true);
- }
- } catch (err) {
- console.error("Failed to load contract chat history:", err);
- } finally {
- if (mounted) {
- setHistoryLoading(false);
- }
- }
- }
-
- loadHistory();
-
- return () => {
- mounted = false;
- };
- }, [contractId]);
-
- // Auto-scroll to bottom when messages change
- useEffect(() => {
- if (messagesRef.current) {
- messagesRef.current.scrollTop = messagesRef.current.scrollHeight;
- }
- }, [messages]);
-
- // Auto-start supervisor when component mounts if it's pending (but not for completed contracts)
- useEffect(() => {
- if (supervisorTask && isSupervisorPending && !supervisorStarting && contract.status !== 'completed') {
- console.log("Auto-starting supervisor task on mount...");
- ensureSupervisorStarted().then((started) => {
- if (started) {
- console.log("Supervisor started successfully");
- }
- });
- }
- }, [supervisorTask?.id, contract.status]); // Only run when task ID or contract status changes
-
- // Convert supervisor output events to messages
- useEffect(() => {
- if (supervisorOutput.length === 0) return;
-
- // Get the latest event
- const latestEvent = supervisorOutput[supervisorOutput.length - 1];
-
- // Only add complete messages (not partials) to the message history
- if (!latestEvent.isPartial && latestEvent.content.trim()) {
- const msgId = `supervisor-${Date.now()}`;
- let msgType: "assistant" | "error" = "assistant";
- let content = latestEvent.content;
-
- // Format based on message type
- switch (latestEvent.messageType) {
- case "assistant":
- content = latestEvent.content;
- break;
- case "tool_use":
- content = `_Using tool: ${latestEvent.toolName}_`;
- break;
- case "tool_result":
- content = latestEvent.isError
- ? `Tool error: ${latestEvent.content}`
- : `Tool result: ${latestEvent.content.slice(0, 200)}${latestEvent.content.length > 200 ? "..." : ""}`;
- msgType = latestEvent.isError ? "error" : "assistant";
- break;
- case "error":
- msgType = "error";
- break;
- case "result":
- // Final result - show cost info if available
- if (latestEvent.costUsd) {
- content = `${latestEvent.content}\n\n_Cost: $${latestEvent.costUsd.toFixed(4)}_`;
- }
- break;
- default:
- // system, raw, etc.
- break;
- }
-
- setMessages((prev) => {
- // Don't add duplicate messages
- if (prev.some((m) => m.content === content)) return prev;
- return [
- ...prev,
- { id: msgId, type: msgType, content },
- ];
- });
- }
- }, [supervisorOutput]);
-
- const handleSubmit = useCallback(
- async (e: React.FormEvent) => {
- e.preventDefault();
- if (!input.trim() || loading) return;
-
- const userMessage = input.trim();
- setInput("");
- setExpanded(true);
-
- const userMsgId = Date.now().toString();
- setMessages((prev) => [
- ...prev,
- { id: userMsgId, type: "user", content: userMessage },
- ]);
-
- setLoading(true);
-
- try {
- // Supervisor is the ONLY way to interact with contracts
- if (!supervisorTask) {
- throw new Error("No supervisor task found. Please create a contract with a supervisor.");
- }
-
- // Ensure supervisor is started (this will start it if pending)
- await ensureSupervisorStarted();
-
- // Send message to supervisor task stdin
- await sendTaskMessage(supervisorTask.id, userMessage);
-
- // Response will come through WebSocket subscription
- // No need for a placeholder message - output will stream in
- } catch (err) {
- const errorMsgId = (Date.now() + 1).toString();
- setMessages((prev) => [
- ...prev,
- {
- id: errorMsgId,
- type: "error",
- content: err instanceof Error ? err.message : "An error occurred",
- },
- ]);
- } finally {
- setLoading(false);
- inputRef.current?.focus();
- }
- },
- [input, loading, supervisorTask, ensureSupervisorStarted]
- );
-
- const handleOptionToggle = useCallback((questionId: string, option: string, allowMultiple: boolean) => {
- setUserAnswers((prev) => {
- const newMap = new Map(prev);
- const currentAnswers = newMap.get(questionId) || [];
-
- if (allowMultiple) {
- if (currentAnswers.includes(option)) {
- newMap.set(questionId, currentAnswers.filter((a) => a !== option));
- } else {
- newMap.set(questionId, [...currentAnswers, option]);
- }
- } else {
- newMap.set(questionId, [option]);
- }
-
- return newMap;
- });
- }, []);
-
- const handleCustomInputChange = useCallback((questionId: string, value: string) => {
- setCustomInputs((prev) => {
- const newMap = new Map(prev);
- newMap.set(questionId, value);
- return newMap;
- });
- }, []);
-
- const handleSubmitAnswers = useCallback(async () => {
- if (!pendingQuestions || loading) return;
-
- const answers = pendingQuestions.map((q) => {
- const selectedOptions = userAnswers.get(q.id) || [];
- const customInput = customInputs.get(q.id)?.trim();
- const finalAnswers = customInput
- ? [...selectedOptions, customInput]
- : selectedOptions;
-
- return {
- id: q.id,
- answers: finalAnswers,
- };
- });
-
- const answerText = answers
- .map((a) => {
- const question = pendingQuestions.find((q) => q.id === a.id);
- return `${question?.question || a.id}: ${a.answers.join(", ")}`;
- })
- .join("\n");
-
- setPendingQuestions(null);
- setUserAnswers(new Map());
- setCustomInputs(new Map());
-
- const userMsgId = Date.now().toString();
- setMessages((prev) => [
- ...prev,
- { id: userMsgId, type: "user", content: `[Answers]\n${answerText}` },
- ]);
-
- setLoading(true);
-
- try {
- if (!supervisorTask) {
- throw new Error("No supervisor task found");
- }
- await ensureSupervisorStarted();
- await sendTaskMessage(supervisorTask.id, answerText);
- // Response will come through WebSocket
- } catch (err) {
- const errorMsgId = (Date.now() + 1).toString();
- setMessages((prev) => [
- ...prev,
- {
- id: errorMsgId,
- type: "error",
- content: err instanceof Error ? err.message : "An error occurred",
- },
- ]);
- } finally {
- setLoading(false);
- }
- }, [pendingQuestions, userAnswers, customInputs, loading, supervisorTask, ensureSupervisorStarted]);
-
- const handleCancelQuestions = useCallback(() => {
- setPendingQuestions(null);
- setUserAnswers(new Map());
- setCustomInputs(new Map());
- }, []);
-
- const clearMessages = useCallback(() => {
- setMessages([]);
- setPendingQuestions(null);
- setUserAnswers(new Map());
- setCustomInputs(new Map());
- setParsedTasks(null);
- setParsedTaskGroups([]);
- setParsedTasksFileName("");
- setSupervisorOutput([]);
- setSupervisorQuestion(null);
- setSupervisorAnswers([]);
- setSupervisorCustomInput("");
- }, []);
-
- // Handle creating tasks from the preview
- const handleCreateDerivedTasks = useCallback(
- async (selectedTasks: ParsedTask[]) => {
- if (selectedTasks.length === 0) {
- setParsedTasks(null);
- return;
- }
-
- setCreatingTasks(true);
-
- // Build a message asking the supervisor to create these tasks
- const taskList = selectedTasks
- .map((t, i) => `${i + 1}. ${t.name}${t.description ? `: ${t.description}` : ""}`)
- .join("\n");
-
- const message = `Create these ${selectedTasks.length} tasks as chained tasks:\n${taskList}`;
-
- // Add user message
- const userMsgId = Date.now().toString();
- setMessages((prev) => [
- ...prev,
- { id: userMsgId, type: "user", content: message },
- ]);
-
- try {
- if (!supervisorTask) {
- throw new Error("No supervisor task found");
- }
- await ensureSupervisorStarted();
- await sendTaskMessage(supervisorTask.id, message);
- // Response will come through WebSocket
- } catch (err) {
- const errorMsgId = (Date.now() + 1).toString();
- setMessages((prev) => [
- ...prev,
- {
- id: errorMsgId,
- type: "error",
- content: err instanceof Error ? err.message : "An error occurred",
- },
- ]);
- } finally {
- setCreatingTasks(false);
- setParsedTasks(null);
- setParsedTaskGroups([]);
- setParsedTasksFileName("");
- }
- },
- [supervisorTask, ensureSupervisorStarted]
- );
-
- const handleCancelTaskDerivation = useCallback(() => {
- setParsedTasks(null);
- setParsedTaskGroups([]);
- setParsedTasksFileName("");
- }, []);
-
- const handleQuickAction = useCallback(
- async (action: QuickAction) => {
- // Convert the action into a chat message that triggers the appropriate behavior
- let message = "";
- switch (action.type) {
- case "create_file":
- message = "Create the suggested file from the template.";
- break;
- case "create_task":
- message = "Yes, create the tasks.";
- break;
- case "derive_tasks":
- message = "Show me the tasks to review and create them.";
- break;
- case "run_task":
- message = "Run the next task.";
- break;
- case "advance_phase":
- if (action.data?.phase) {
- message = `Advance to the ${action.data.phase} phase.`;
- } else {
- message = "Advance to the next phase.";
- }
- break;
- case "update_file":
- message = "Update the file with the task output.";
- break;
- default:
- return;
- }
-
- setExpanded(true);
-
- // Submit the message
- const userMsgId = Date.now().toString();
- setMessages((prev) => [
- ...prev,
- { id: userMsgId, type: "user", content: message },
- ]);
-
- setLoading(true);
- try {
- if (!supervisorTask) {
- throw new Error("No supervisor task found");
- }
- await ensureSupervisorStarted();
- await sendTaskMessage(supervisorTask.id, message);
- // Response will come through WebSocket
- } catch (err) {
- const errorMsgId = (Date.now() + 1).toString();
- setMessages((prev) => [
- ...prev,
- {
- id: errorMsgId,
- type: "error",
- content: err instanceof Error ? err.message : "An error occurred",
- },
- ]);
- } finally {
- setLoading(false);
- }
- },
- [supervisorTask, ensureSupervisorStarted]
- );
-
- return (
- <div className="border-t border-[rgba(117,170,252,0.35)] bg-[#0d1b2d]">
- {/* Header bar with supervisor status and toggle */}
- <div className="px-3 py-2 flex items-center justify-between border-b border-[rgba(117,170,252,0.2)]">
- <div className="flex items-center gap-3">
- <span className="font-mono text-[10px] text-[#555] uppercase tracking-wide">
- Supervisor
- </span>
- {supervisorTask && (
- <span className={`font-mono text-[10px] px-2 py-0.5 border ${
- isSupervisorRunning
- ? "text-green-400 border-green-400/30 bg-green-400/10"
- : isSupervisorPending || supervisorStarting
- ? "text-yellow-400 border-yellow-400/30 bg-yellow-400/10"
- : "text-[#555] border-[rgba(117,170,252,0.2)]"
- }`}>
- {supervisorStarting ? "Starting..." : isSupervisorRunning ? "Running" : supervisorStatus || "Unknown"}
- </span>
- )}
- {!supervisorTask && (
- <span className="font-mono text-[10px] text-red-400">
- No supervisor
- </span>
- )}
- </div>
- {messages.length > 0 && (
- <button
- type="button"
- onClick={() => setExpanded(!expanded)}
- className="font-mono text-[10px] text-[#555] hover:text-[#9bc3ff] transition-colors"
- >
- {expanded ? "Hide Messages" : `Show Messages (${messages.length})`}
- </button>
- )}
- </div>
-
- {/* History loading indicator */}
- {historyLoading && (
- <div className="px-3 py-2 text-[10px] font-mono text-[#555] flex items-center gap-2 border-b border-[rgba(117,170,252,0.2)]">
- <span className="animate-pulse">Loading history...</span>
- </div>
- )}
-
- {/* Messages Panel (expandable) */}
- {expanded && messages.length > 0 && !historyLoading && (
- <div className="relative border-b border-[rgba(117,170,252,0.2)]">
- {/* Expand/Collapse button */}
- <div className="absolute top-2 right-2 z-10 flex gap-1">
- <button
- type="button"
- onClick={() => setFullscreen(!fullscreen)}
- className="px-2 py-1 font-mono text-[10px] text-[#555] hover:text-[#9bc3ff] bg-[#0d1b2d] border border-[rgba(117,170,252,0.2)] hover:border-[rgba(117,170,252,0.4)] transition-colors"
- title={fullscreen ? "Collapse" : "Expand"}
- >
- {fullscreen ? "▼ Collapse" : "▲ Expand"}
- </button>
- </div>
- <div
- ref={messagesRef}
- className={`overflow-y-auto p-3 pr-24 space-y-2 transition-all duration-200 ${
- fullscreen ? "max-h-[60vh]" : "max-h-48"
- }`}
- >
- {messages.map((msg) => (
- <div key={msg.id} className="font-mono text-xs">
- {msg.type === "user" && (
- <div className="flex gap-2">
- <span className="text-[#9bc3ff]">&gt;</span>
- <span className="text-white/80 whitespace-pre-wrap">{msg.content}</span>
- </div>
- )}
- {(msg.type === "assistant" || msg.type === "question") && (
- <div className="pl-4 space-y-1">
- <SimpleMarkdown content={msg.content} className="text-[#75aafc]" />
- {msg.toolCalls && msg.toolCalls.length > 0 && (
- <div className="text-[#555] text-[10px] space-y-0.5">
- {msg.toolCalls.map((tc, i) => (
- <div key={i}>
- <span
- className={
- tc.success ? "text-green-500" : "text-red-400"
- }
- >
- {tc.success ? "+" : "x"}
- </span>{" "}
- {tc.name}: {tc.message}
- </div>
- ))}
- </div>
- )}
- {msg.quickActions && msg.quickActions.length > 0 && (
- <QuickActionButtons
- actions={msg.quickActions}
- onAction={handleQuickAction}
- loading={loading}
- />
- )}
- </div>
- )}
- {msg.type === "error" && (
- <div className="pl-4 text-red-400">{msg.content}</div>
- )}
- </div>
- ))}
- </div>
- </div>
- )}
-
- {/* Pending Questions UI */}
- {pendingQuestions && pendingQuestions.length > 0 && (
- <div className="p-3 border-b border-[rgba(117,170,252,0.2)] space-y-3">
- <div className="text-[#9bc3ff] font-mono text-xs uppercase tracking-wide">
- Questions from AI
- </div>
- {pendingQuestions.map((q) => (
- <div key={q.id} className="space-y-2">
- <div className="text-white/90 font-mono text-sm">{q.question}</div>
- <div className="flex flex-wrap gap-2">
- {q.options.map((option) => {
- const isSelected = (userAnswers.get(q.id) || []).includes(option);
- return (
- <button
- key={option}
- type="button"
- onClick={() => handleOptionToggle(q.id, option, q.allowMultiple)}
- className={`px-2 py-1 font-mono text-xs border transition-colors ${
- isSelected
- ? "bg-[#3f6fb3] border-[#75aafc] text-white"
- : "bg-transparent border-[rgba(117,170,252,0.25)] text-[#9bc3ff] hover:border-[#3f6fb3]"
- }`}
- >
- {q.allowMultiple && (
- <span className="mr-1">{isSelected ? "[x]" : "[ ]"}</span>
- )}
- {option}
- </button>
- );
- })}
- </div>
- {q.allowCustom && (
- <input
- type="text"
- value={customInputs.get(q.id) || ""}
- onChange={(e) => handleCustomInputChange(q.id, e.target.value)}
- placeholder="Or type a custom answer..."
- className="w-full bg-transparent border border-[rgba(117,170,252,0.25)] text-white font-mono text-xs px-2 py-1 outline-none focus:border-[#3f6fb3] placeholder-[#555]"
- />
- )}
- </div>
- ))}
- <div className="flex gap-2 pt-2">
- <button
- type="button"
- onClick={handleSubmitAnswers}
- disabled={loading}
- className="px-3 py-1 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] disabled:opacity-50 disabled:cursor-not-allowed transition-colors uppercase"
- >
- {loading ? "..." : "Submit Answers"}
- </button>
- <button
- type="button"
- onClick={handleCancelQuestions}
- disabled={loading}
- className="px-3 py-1 font-mono text-xs text-[#555] hover:text-[#9bc3ff] transition-colors"
- >
- Cancel
- </button>
- </div>
- </div>
- )}
-
- {/* Supervisor Question UI */}
- {supervisorQuestion && (
- <div className="p-3 border-b border-[rgba(117,170,252,0.2)] space-y-3 bg-[rgba(117,170,252,0.05)]">
- <div className="text-green-400 font-mono text-xs uppercase tracking-wide flex items-center gap-2">
- <span className="w-2 h-2 rounded-full bg-green-400 animate-pulse" />
- Question from Supervisor
- </div>
- <div className="text-white/90 font-mono text-sm">{supervisorQuestion.question}</div>
- <div className="flex flex-wrap gap-2">
- {supervisorQuestion.options.map((option) => {
- const isSelected = supervisorAnswers.includes(option);
- return (
- <button
- key={option}
- type="button"
- onClick={() => handleSupervisorOptionToggle(option)}
- className={`px-2 py-1 font-mono text-xs border transition-colors ${
- isSelected
- ? "bg-green-500/30 border-green-400 text-white"
- : "bg-transparent border-[rgba(117,170,252,0.25)] text-[#9bc3ff] hover:border-green-400"
- }`}
- >
- {supervisorQuestion.allowMultiple && (
- <span className="mr-1">{isSelected ? "[x]" : "[ ]"}</span>
- )}
- {option}
- </button>
- );
- })}
- </div>
- {supervisorQuestion.allowCustom && (
- <input
- type="text"
- value={supervisorCustomInput}
- onChange={(e) => setSupervisorCustomInput(e.target.value)}
- placeholder="Or type a custom answer..."
- className="w-full bg-transparent border border-[rgba(117,170,252,0.25)] text-white font-mono text-xs px-2 py-1 outline-none focus:border-green-400 placeholder-[#555]"
- />
- )}
- <div className="flex gap-2 pt-2">
- <button
- type="button"
- onClick={handleSubmitSupervisorAnswer}
- disabled={supervisorAnswers.length === 0 && !supervisorCustomInput.trim()}
- className="px-3 py-1 font-mono text-xs text-green-400 border border-green-400/50 hover:border-green-400 disabled:opacity-50 disabled:cursor-not-allowed transition-colors uppercase"
- >
- Send Answer
- </button>
- <button
- type="button"
- onClick={handleCancelSupervisorQuestion}
- className="px-3 py-1 font-mono text-xs text-[#555] hover:text-[#9bc3ff] transition-colors"
- >
- Dismiss
- </button>
- </div>
- </div>
- )}
-
- {/* Contract Context Badge */}
- <div className="px-3 pt-2 pb-1 flex items-center gap-2 text-[10px] font-mono text-[#555]">
- <span className="text-[#75aafc]">{contract.phase}</span>
- <span>|</span>
- {supervisorTask && (
- <>
- <span
- className={
- isSupervisorRunning
- ? "text-green-400"
- : isSupervisorPending
- ? "text-yellow-400"
- : supervisorStarting
- ? "text-cyan-400 animate-pulse"
- : "text-[#555]"
- }
- >
- Supervisor: {supervisorStarting ? "starting..." : supervisorTask.status}
- </span>
- <span>|</span>
- </>
- )}
- <span>{contract.files.length} files</span>
- <span>|</span>
- <span>{contract.tasks.length} tasks</span>
- <span>|</span>
- <span>{contract.repositories.length} repos</span>
- <span>|</span>
- <button
- type="button"
- onClick={() => {
- const prompt = "Guide me to complete this phase and advance to the next. Analyze my current deliverables, identify what's missing, and suggest specific next steps.";
- setInput(prompt);
- // Auto-submit the prompt
- setTimeout(() => {
- const form = document.querySelector('form');
- if (form) form.requestSubmit();
- }, 0);
- }}
- disabled={loading || !!pendingQuestions}
- className="text-[#75aafc] hover:text-[#9bc3ff] disabled:opacity-50 transition-colors cursor-pointer"
- >
- Progress →
- </button>
- {messages.length > 0 && (
- <>
- <span>|</span>
- <button
- type="button"
- onClick={async () => {
- if (window.confirm("Clear all chat history for this contract?")) {
- try {
- await clearContractChatHistory(contractId);
- setMessages([]);
- } catch (err) {
- console.error("Failed to clear history:", err);
- }
- }
- }}
- disabled={loading}
- className="text-[#555] hover:text-red-400 disabled:opacity-50 transition-colors cursor-pointer"
- >
- Clear
- </button>
- </>
- )}
- </div>
-
- {/* Input Bar */}
- <form onSubmit={handleSubmit} className="flex items-center gap-2 px-3 pb-3">
- <span className="text-[#9bc3ff] font-mono text-sm">&gt;</span>
- <input
- ref={inputRef}
- type="text"
- value={input}
- onChange={(e) => setInput(e.target.value)}
- placeholder={
- loading
- ? "Processing..."
- : supervisorStarting
- ? "Starting supervisor..."
- : supervisorQuestion
- ? "Answer supervisor question above..."
- : pendingQuestions
- ? "Answer questions above first..."
- : isSupervisorRunning
- ? "Message supervisor..."
- : "Create a task, add a file, or ask about the contract..."
- }
- disabled={loading || !!pendingQuestions || !!supervisorQuestion}
- className="flex-1 bg-transparent border-none outline-none font-mono text-sm text-white placeholder-[#555]"
- />
- {messages.length > 0 && (
- <button
- type="button"
- onClick={clearMessages}
- className="text-[#555] hover:text-[#9bc3ff] font-mono text-xs transition-colors"
- >
- clear
- </button>
- )}
- <button
- type="submit"
- disabled={loading || !input.trim() || !!pendingQuestions || !!supervisorQuestion}
- className="px-3 py-1 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] disabled:opacity-50 disabled:cursor-not-allowed transition-colors uppercase"
- >
- {loading ? "..." : "Send"}
- </button>
- </form>
-
- {/* Task Derivation Preview Modal */}
- {parsedTasks && parsedTasks.length > 0 && (
- <TaskDerivationPreview
- tasks={parsedTasks}
- groups={parsedTaskGroups}
- fileName={parsedTasksFileName}
- onCreateTasks={handleCreateDerivedTasks}
- onCancel={handleCancelTaskDerivation}
- loading={creatingTasks}
- />
- )}
- </div>
- );
-}