import { useEffect, useRef, useState } from 'react' import anime from 'animejs' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' // Colour + label per AutoGen agent name const AGENT_CFG = { DataAgent: { color: '#44aaff', icon: 'πŸ—„οΈ', label: 'Data Agent' }, TechnicalAnalyst: { color: '#00e87a', icon: 'πŸ“Š', label: 'Technical Analyst' }, FundamentalAnalyst: { color: '#ff9944', icon: '🏒', label: 'Fundamental Analyst' }, ReportWriter: { color: '#cc88ff', icon: 'πŸ“', label: 'Report Writer' }, PortfolioAgent: { color: '#44ffcc', icon: 'πŸ’Ό', label: 'Portfolio Agent' }, RiskAgent: { color: '#ff4466', icon: '⚠️', label: 'Risk Agent' }, system: { color: '#666688', icon: 'βš™οΈ', label: 'System' }, } // ── Single message bubble ───────────────────────────────────────────────────── function AgentMessage({ message }) { const ref = useRef(null) const cfg = AGENT_CFG[message.agent] || AGENT_CFG.system useEffect(() => { anime({ targets: ref.current, translateX: [-18, 0], opacity: [0, 1], duration: 380, easing: 'easeOutExpo', }) }, []) // Show up to 350 chars β€” enough to convey what the agent is doing const preview = message.content?.slice(0, 350) ?? '' const truncated = (message.content?.length ?? 0) > 350 return (
{cfg.icon} {cfg.label}

{preview} {truncated && ( …(truncated) )}

) } // ── Agent pipeline progress bar ─────────────────────────────────────────────── const PIPELINE = [ 'DataAgent', 'TechnicalAnalyst', 'FundamentalAnalyst', 'ReportWriter', 'RiskAgent', ] function PipelineBar({ activeAgent, done }) { return (
{PIPELINE.map((name, i) => { const cfg = AGENT_CFG[name] const isActive = !done && activeAgent === name const isDone = done || PIPELINE.indexOf(activeAgent) > i return (
{cfg.icon} {cfg.label}
) })}
) } // ── Main component ──────────────────────────────────────────────────────────── export default function AgentStream({ ticker }) { const [messages, setMessages] = useState([]) const [streaming, setStreaming] = useState(true) const [report, setReport] = useState(null) const [activeAgent, setActiveAgent] = useState('DataAgent') const bottomRef = useRef(null) useEffect(() => { const es = new EventSource(`/stream/${ticker}`) es.onmessage = (e) => { if (e.data === '[DONE]') { setStreaming(false) es.close() return } try { const event = JSON.parse(e.data) if (event.agent === '__complete__') { setReport(event) setStreaming(false) } else { if (event.agent) setActiveAgent(event.agent) if (event.content?.trim()) { setMessages((prev) => [...prev, event]) } } } catch { // ignore parse errors } } es.onerror = () => { setStreaming(false) es.close() } return () => es.close() }, [ticker]) // Auto-scroll feed to bottom on each new message useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages]) const activeCfg = AGENT_CFG[activeAgent] || AGENT_CFG.system return (
{/* Header */}

πŸ€– AI Agents Working…

{streaming ? (
{activeCfg.icon} {activeCfg.label} thinking…
) : ( βœ… Analysis complete )}
{/* Pipeline progress */} {/* Scrollable message feed */}
{messages.length === 0 && streaming && (

Waiting for first agent response…

)} {messages.map((msg, i) => ( ))}
{/* Final report */} {report && (

πŸ“‹ Full Investment Report

{report.content?.replace('##ANALYSIS_DONE##', '').trim()}
)}
) }