From 1ef3f9baa33d36438febe89c98d26bad85b3a7ed Mon Sep 17 00:00:00 2001 From: Mark Henderson Date: Mon, 9 Mar 2026 15:51:48 -0700 Subject: [PATCH] feat: top navbar (Build|Market|Assist) + persistent Assist chat in shell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New top navbar in ProjectShell: logo + project name | Build | Market | Assist tabs | user avatar — replaces the left icon sidebar for project pages - CooChat extracted to components/layout/coo-chat.tsx and moved into the shell so it persists across Build/Market/Assist route changes - Build page inner layout simplified: inner nav (200px) + file viewer, no longer owns the chat column - Layout: [top nav 48px] / [Assist chat 320px | content flex] Made-with: Cursor --- .../project/[projectId]/build/page.tsx | 200 +----------------- components/layout/coo-chat.tsx | 191 +++++++++++++++++ components/layout/project-shell.tsx | 161 ++++++++++---- 3 files changed, 313 insertions(+), 239 deletions(-) create mode 100644 components/layout/coo-chat.tsx diff --git a/app/[workspace]/project/[projectId]/build/page.tsx b/app/[workspace]/project/[projectId]/build/page.tsx index 9c5a456..3185bf4 100644 --- a/app/[workspace]/project/[projectId]/build/page.tsx +++ b/app/[workspace]/project/[projectId]/build/page.tsx @@ -812,181 +812,6 @@ function TerminalPanel({ appName }: { appName: string }) { ); } -// ── COO / Assist chat — persistent left-side advisor ───────────────────────── - -interface CooMessage { - id: string; - role: "user" | "assistant"; - content: string; - streaming?: boolean; -} - -const WELCOME: CooMessage = { - 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?", -}; - -function CooChat({ projectId, projectName }: { projectId: string; projectName: string }) { - const [messages, setMessages] = useState([WELCOME]); - const [input, setInput] = useState(""); - const [loading, setLoading] = useState(false); - const bottomRef = useRef(null); - const textareaRef = useRef(null); - - useEffect(() => { - bottomRef.current?.scrollIntoView({ behavior: "smooth" }); - }, [messages]); - - const send = async () => { - const text = input.trim(); - if (!text || loading) return; - setInput(""); - - const userMsg: CooMessage = { id: Date.now().toString(), role: "user", content: text }; - const assistantId = (Date.now() + 1).toString(); - const assistantMsg: CooMessage = { id: assistantId, role: "assistant", content: "", streaming: true }; - - setMessages(prev => [...prev, userMsg, assistantMsg]); - setLoading(true); - - // Build history for the API (exclude welcome message, exclude the new blank assistant placeholder) - const history = messages - .filter(m => m.id !== "welcome" && 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(); - } - }; - - return ( -
- {/* Messages */} -
- {messages.map(msg => ( -
- {msg.role === "assistant" && ( - - )} -
- {msg.content || (msg.streaming ? "" : "")} - {msg.streaming && msg.content === "" && ( - - {[0, 1, 2].map(i => ( - - ))} - - )} - {msg.streaming && msg.content !== "" && ( - - )} -
-
- ))} -
-
- - {/* Input */} -
-
-