// ───────────────────────── ChatDrawer.jsx import { useEffect, useRef, useState } from 'react'; import { Sheet, SheetTrigger, SheetContent } from './ui/sheet.js'; import { cn } from '../utils/cn.js'; import { Button } from './ui/button.js'; import { Input } from './ui/input.js'; import { MessageCircle } from 'lucide-react'; import RetirementChatBar from './RetirementChatBar.js'; async function ensureSupportThread() { // list existing const r = await fetch('/api/chat/threads', { credentials:'include' }); if (!r.ok) throw new Error(`threads list failed: ${r.status}`); const { threads } = await r.json(); if (threads?.length) return threads[0].id; // create new const r2 = await fetch('/api/chat/threads', { method: 'POST', credentials:'include', headers:{ 'Content-Type':'application/json' }, body: JSON.stringify({ title: 'Support chat' }) }); if (!r2.ok) throw new Error(`thread create failed: ${r2.status}`); const { id } = await r2.json(); return id; } /* ------------------------------------------------------------------ */ /* ChatDrawer – support-bot lives in this file (streamed from /api/chat/free) – retirement helper is just a passthrough to ------------------------------------------------------------------ */ export default function ChatDrawer({ /* ─ props from App.js ─ */ pageContext = 'Home', snapshot = null, open: controlledOpen = false, onOpenChange, pane: controlledPane = 'support', setPane: setControlledPane, retireProps = null, // { scenario, financialProfile, … } canShowRetireBot }) { /* ─────────────────────────── internal / fallback state ───────── */ const [openLocal, setOpenLocal] = useState(false); const [paneLocal, setPaneLocal] = useState('support'); const [supportThreadId, setSupportThreadId] = useState(null); /* prefer the controlled props when supplied */ const open = controlledOpen ?? openLocal; const setOpen = onOpenChange ?? setOpenLocal; const pane = controlledPane ?? paneLocal; const setPane = setControlledPane ?? setPaneLocal; /* ────────────────── free-tier support-bot state ─────────────── */ const [prompt, setPrompt] = useState(''); const [messages, setMessages] = useState([]); // { role, content } const listRef = useRef(null); /* auto-scroll on incoming messages */ useEffect(() => { listRef.current && (listRef.current.scrollTop = listRef.current.scrollHeight); }, [messages]); useEffect(() => { (async () => { try { const id = await ensureSupportThread(); setSupportThreadId(id); // preload messages const r = await fetch(`/api/chat/threads/${id}`, { credentials:'include' }); if (r.ok) { const { messages: msgs } = await r.json(); setMessages(msgs || []); } else { // don’t crash UI on preload failure setMessages([]); } } catch (e) { console.error('[Support preload]', e); setMessages([]); } })(); }, []); /* helper: merge chunks while streaming */ const pushAssistant = (chunk) => setMessages((prev) => { const last = prev.at(-1); if (last?.role === 'assistant') { const updated = [...prev]; updated[updated.length - 1] = { ...last, content: last.content + chunk, }; return updated; } return [...prev, { role: 'assistant', content: chunk }]; }); useEffect(() => { if (!canShowRetireBot && pane === 'retire') { setPane('support'); } }, [canShowRetireBot, pane, setPane]); /* ───────────────────────── send support-bot prompt ───────────── */ async function sendPrompt() { const text = prompt.trim(); if (!text || !supportThreadId) return; setMessages(m => [...m, { role:'user', content:text }]); setPrompt(''); try { const resp = await fetch(`/api/chat/threads/${supportThreadId}/stream`, { method: 'POST', credentials: 'include', headers: { 'Content-Type':'application/json', Accept:'text/event-stream' }, body: JSON.stringify({ prompt: text, pageContext, snapshot }) }); if (!resp.ok || !resp.body) throw new Error(`HTTP ${resp.status}`); const reader = resp.body.getReader(); const decoder = new TextDecoder(); let buf = ''; while (true) { const { value, done } = await reader.read(); if (done) break; if (!value) continue; buf += decoder.decode(value, { stream:true }); let nl; while ((nl = buf.indexOf('\n')) !== -1) { const line = buf.slice(0, nl).trim(); buf = buf.slice(nl + 1); if (line) pushAssistant(line + '\n'); } } if (buf.trim()) pushAssistant(buf); } catch (e) { console.error('[Support stream]', e); pushAssistant('Sorry — something went wrong. Please try again later.'); } } const handleKeyDown = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendPrompt(); } }; /* ---------- render ---------- */ return ( {/* floating action button */} {/* side drawer */} {/* header (tabs only if retirement bot is allowed) */}
{canShowRetireBot && ( )}
{/* body */} {pane === 'support' ? ( /* ── Support bot pane ── */ <>
{messages.map((m, i) => (
{m.content}
))}
{ e.preventDefault(); sendPrompt(); }} className="flex gap-2" > setPrompt(e.target.value)} placeholder="Ask me anything…" onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendPrompt(); } }} className="flex-1" />
) : retireProps ? ( /* ── Retirement helper pane ── */ ) : ( /* failsafe (retire tab opened before selecting a scenario) */
Select a scenario in  Retirement Planner
)}
); }