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()}
)}
)
}