// Hedge Fund Team overlay — opens a WS to /ws/team, streams events from N // parallel framework analysts + a synthesizer, shows each agent's progress // and final markdown side by side, then the Managing Director's memo. const { useState: tUseState, useEffect: tUseEffect, useRef: tUseRef, useCallback: tUseCallback } = React; function TeamOverlay({ open, onClose, topic, hypothesis }) { const [members, setMembers] = tUseState([]); const [agentState, setAgentState] = tUseState({}); // { role: { status, title, toolCalls, lastTool, markdown, error } } const [synthesis, setSynthesis] = tUseState(null); // Phase 9: 'idle' (just opened) | 'last-memo' (showing stored verdict) // | 'running' | 'synthesizing' | 'done' | 'error' const [phase, setPhase] = tUseState('idle'); const [lastMemo, setLastMemo] = tUseState(null); // {date, topic, body, model, path} const [error, setError] = tUseState(''); const [expanded, setExpanded] = tUseState({}); const wsRef = tUseRef(null); const startRun = tUseCallback(() => { if (!topic && !(hypothesis && hypothesis.question)) { setError('Need a hypothesis question or topic before running the team.'); setPhase('error'); return; } setMembers([]); setAgentState({}); setSynthesis(null); setError(''); setPhase('running'); const proto = location.protocol === 'https:' ? 'wss' : 'ws'; // Phase 2C.5 — append the Supabase JWT as ?token= so the WS handshake // can authenticate (browsers can't set Authorization headers on WS). // tibebReadToken is installed by the boot gate at index.html. const tok = (window.tibebReadToken && window.tibebReadToken()) || ''; const wsUrl = `${proto}://${location.host}/ws/team${tok ? `?token=${encodeURIComponent(tok)}` : ''}`; const ws = new WebSocket(wsUrl); wsRef.current = ws; ws.onopen = () => { ws.send(JSON.stringify({ topic: topic || hypothesis.question, hypothesis: hypothesis || null, })); }; ws.onmessage = (ev) => { let event; try { event = JSON.parse(ev.data); } catch { return; } handleEvent(event); }; ws.onerror = () => { setError("Couldn't reach the analyst team right now. Try again in a few seconds."); setPhase('error'); }; ws.onclose = () => { if (phase === 'running' || phase === 'synthesizing') setPhase('done'); }; }, [topic, hypothesis]); tUseEffect(() => { if (!open) { setMembers([]); setAgentState({}); setSynthesis(null); setPhase('idle'); setError(''); setExpanded({}); setLastMemo(null); if (wsRef.current) try { wsRef.current.close(); } catch {} return; } // On open, fetch the most recent Managing Director memo for this topic. // If found, show it as the default view with a 'Run fresh' button. // If not found, auto-start a run (first time on this thesis). const t = topic || (hypothesis && hypothesis.question) || ''; if (!t) { setError('Need a hypothesis question or topic before running the team.'); setPhase('error'); return; } setPhase('idle'); fetch(`/api/team/last-memo?topic=${encodeURIComponent(t)}`) .then(r => r.json()) .then(j => { if (j.found) { setLastMemo(j); setPhase('last-memo'); } else { // No prior memo — kick off a fresh run immediately startRun(); } }) .catch(() => startRun()); return () => { try { wsRef.current?.close(); } catch {} }; }, [open, topic, hypothesis, startRun]); const handleEvent = tUseCallback((event) => { const t = event.type; if (t === 'team_started') { setMembers(event.members || []); const init = {}; const expandedInit = {}; (event.members || []).forEach(m => { init[m.role] = { title: m.title, model: m.model, status: 'queued', toolCalls: 0, lastTool: '', markdown: '', error: '', priorMemories: 0, memoryPath: '', memoryError: '', }; // Auto-expand every agent card on team start so the user sees // each analyst's thinking + final memo without having to click // through. The user explicitly asked: 'I want each individual // agent's thoughts on the thesis' — auto-expand makes that the // default view, not a hidden detail. expandedInit[m.role] = true; }); setExpanded(expandedInit); // Pre-register synthesizer slot too so its memory chip can render if (event.has_synthesizer) { init['managing-director'] = { title: 'Managing Director', model: event.synthesizer_model, status: 'queued', isSynthesizer: true, priorMemories: 0, memoryPath: '', memoryError: '', }; } setAgentState(init); } else if (t === 'agent_memory_loaded') { setAgentState(s => ({ ...s, [event.role]: { ...(s[event.role] || {}), priorMemories: event.count || 0 }, })); } else if (t === 'agent_memory_written') { setAgentState(s => ({ ...s, [event.role]: { ...(s[event.role] || {}), memoryPath: event.path || '' }, })); } else if (t === 'agent_memory_error') { setAgentState(s => ({ ...s, [event.role]: { ...(s[event.role] || {}), memoryError: event.error || '' }, })); } else if (t === 'agent_started') { setAgentState(s => ({ ...s, [event.role]: { ...(s[event.role] || {}), title: event.title, model: event.model, status: 'thinking' }, })); } else if (t === 'agent_text') { // Phase 14 — live token streaming. Each delta arrives as a small // string fragment; we append into the agent's `streaming` buffer // so the UI types out the memo in real time before agent_done // replaces it with the final markdown. const delta = event.delta || ''; if (!delta) return; setAgentState(s => ({ ...s, [event.role]: { ...(s[event.role] || {}), status: 'thinking', streaming: ((s[event.role] || {}).streaming || '') + delta, }, })); } else if (t === 'agent_tool_use') { setAgentState(s => ({ ...s, [event.role]: { ...(s[event.role] || {}), toolCalls: ((s[event.role] || {}).toolCalls || 0) + 1, lastTool: `${event.tool}${event.input_summary ? ' · ' + event.input_summary : ''}`, }, })); } else if (t === 'agent_done') { setAgentState(s => ({ ...s, [event.role]: { ...(s[event.role] || {}), status: 'done', markdown: event.markdown || '' }, })); } else if (t === 'agent_error') { setAgentState(s => ({ ...s, [event.role]: { ...(s[event.role] || {}), status: 'error', error: event.error || '' }, })); } else if (t === 'synthesis_started') { setPhase('synthesizing'); setAgentState(s => ({ ...s, 'managing-director': { ...(s['managing-director'] || {}), status: 'thinking', model: event.model }, })); } else if (t === 'synthesis_done') { setSynthesis(event.memo || ''); setAgentState(s => ({ ...s, 'managing-director': { ...(s['managing-director'] || {}), status: 'done' }, })); } else if (t === 'synthesis_error') { setError(`Synthesis failed: ${event.error}`); } else if (t === 'team_done') { setPhase('done'); } else if (t === 'error') { setError(event.error || 'Unknown error.'); setPhase('error'); } }, [phase]); if (!open) return null; const statusColor = (s) => ({ queued: 'var(--text-dim)', thinking: 'var(--catalyst)', done: 'var(--accent)', error: 'var(--danger)', }[s] || 'var(--text-dim)'); const modelChip = (model) => { if (!model) return null; const lower = model.toLowerCase(); let label = model, tier = 'sonnet'; if (lower.includes('opus')) { label = 'OPUS 4.7'; tier = 'opus'; } else if (lower.includes('sonnet')) { label = 'SONNET 4.6'; tier = 'sonnet'; } else if (lower.includes('haiku')) { label = 'HAIKU 4.5'; tier = 'haiku'; } else { label = model.replace(/^claude-/, '').toUpperCase(); } return {label}; }; return (