"use client"; import { useState, useEffect, useRef } from "react"; interface CooMessage { id: string; role: "user" | "assistant"; content: string; source?: "atlas" | "coo"; // atlas = discovery history, coo = orchestrator response streaming?: boolean; } export function CooChat({ projectId }: { projectId: string }) { const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [loading, setLoading] = useState(false); const [historyLoaded, setHistoryLoaded] = useState(false); const bottomRef = useRef(null); const textareaRef = useRef(null); // Scroll to bottom whenever messages change useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); // Pre-load Atlas discovery history on mount useEffect(() => { fetch(`/api/projects/${projectId}/atlas-chat`) .then(r => r.json()) .then((data: { messages?: Array<{ role: "user" | "assistant"; content: string }> }) => { const atlasMessages: CooMessage[] = (data.messages ?? []) .filter(m => m.content?.trim()) .map((m, i) => ({ id: `atlas_${i}`, role: m.role, content: m.content, source: "atlas" as const, })); if (atlasMessages.length > 0) { // Add a small divider message at the bottom of Atlas history setMessages([ ...atlasMessages, { id: "coo_divider", role: "assistant", content: "Discovery complete. I'm your product COO — I have the full context above. What do you need?", source: "coo" as const, }, ]); } else { // No Atlas history — show default COO welcome setMessages([{ id: "welcome", role: "assistant", content: "Hi. I'm your product COO — I know your codebase, your goals, and what's been built. What do you need?", source: "coo" as const, }]); } setHistoryLoaded(true); }) .catch(() => { setMessages([{ id: "welcome", role: "assistant", content: "Hi. I'm your product COO — I know your codebase, your goals, and what's been built. What do you need?", source: "coo" as const, }]); setHistoryLoaded(true); }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [projectId]); const send = async () => { const text = input.trim(); if (!text || loading) return; setInput(""); const userMsg: CooMessage = { id: Date.now().toString(), role: "user", content: text, source: "coo" }; const assistantId = (Date.now() + 1).toString(); const assistantMsg: CooMessage = { id: assistantId, role: "assistant", content: "", source: "coo", streaming: true }; setMessages(prev => [...prev, userMsg, assistantMsg]); setLoading(true); // Build history from COO messages only (skip atlas history for context to orchestrator) const history = messages .filter(m => m.source === "coo" && m.id !== "coo_divider" && m.content) .map(m => ({ role: m.role === "assistant" ? "model" as const : "user" as const, content: m.content })); try { const res = await fetch(`/api/projects/${projectId}/advisor`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: text, history }), }); if (!res.ok || !res.body) { setMessages(prev => prev.map(m => m.id === assistantId ? { ...m, content: "Something went wrong. Please try again.", streaming: false } : m)); return; } const reader = res.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); setMessages(prev => prev.map(m => m.id === assistantId ? { ...m, content: m.content + chunk } : m)); } setMessages(prev => prev.map(m => m.id === assistantId ? { ...m, streaming: false } : m)); } catch { setMessages(prev => prev.map(m => m.id === assistantId ? { ...m, content: "Connection error. Please try again.", streaming: false } : m)); } finally { setLoading(false); textareaRef.current?.focus(); } }; if (!historyLoaded) { return (
{[0, 1, 2].map(i => ( ))}
); } return (
{/* Messages */}
{messages.map((msg, idx) => { const isAtlas = msg.source === "atlas"; const isUser = msg.role === "user"; const isCoo = !isUser && !isAtlas; // Separator before the divider message const prevMsg = messages[idx - 1]; const showSeparator = msg.id === "coo_divider" && prevMsg?.source === "atlas"; return (
{showSeparator && (
Discovery · COO
)}
{/* Avatar */} {!isUser && ( {isAtlas ? "A" : "◈"} )}
{msg.content} {msg.streaming && msg.content === "" && ( {[0, 1, 2].map(i => ( ))} )} {msg.streaming && msg.content !== "" && ( )}
); })}
{/* Input */}