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
useEffect(() => {
if (supervisorTask && isSupervisorPending && !supervisorStarting) {
console.log("Auto-starting supervisor task on mount...");
ensureSupervisorStarted().then((started) => {
if (started) {
console.log("Supervisor started successfully");
}
});
}
}, [supervisorTask?.id]); // Only run when task ID changes, not on every render
// 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]">></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">></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>
);
}