vjeai's picture
Deploy: all fixes β€” yfinance candles, ml_signal 2y history, no handoff schemas, sequential report phase
3be03dd
Raw
History Blame Contribute Delete
6.12 kB
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 (
<div
ref={ref}
className="agent-message"
style={{ '--agent-color': cfg.color, opacity: 0 }}
>
<div className="agent-msg-header">
<span className="agent-icon">{cfg.icon}</span>
<span className="agent-name" style={{ color: cfg.color }}>
{cfg.label}
</span>
</div>
<p className="agent-msg-content">
{preview}
{truncated && (
<span style={{ color: 'var(--text-faint)' }}> …(truncated)</span>
)}
</p>
</div>
)
}
// ── Agent pipeline progress bar ───────────────────────────────────────────────
const PIPELINE = [
'DataAgent',
'TechnicalAnalyst',
'FundamentalAnalyst',
'ReportWriter',
'RiskAgent',
]
function PipelineBar({ activeAgent, done }) {
return (
<div className="pipeline-bar">
{PIPELINE.map((name, i) => {
const cfg = AGENT_CFG[name]
const isActive = !done && activeAgent === name
const isDone = done || PIPELINE.indexOf(activeAgent) > i
return (
<div
key={name}
className={`pipeline-step ${isActive ? 'active' : ''} ${isDone ? 'done' : ''}`}
style={{ '--step-color': cfg.color }}
>
<span className="pipeline-icon">{cfg.icon}</span>
<span className="pipeline-label">{cfg.label}</span>
</div>
)
})}
</div>
)
}
// ── 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 (
<div className="agent-stream">
{/* Header */}
<div className="agent-stream-header">
<h3>πŸ€– AI Agents Working…</h3>
{streaming ? (
<div
className="active-agent-badge"
style={{ '--agent-color': activeCfg.color }}
>
<span className="thinking-dot" />
<span style={{ color: activeCfg.color }}>
{activeCfg.icon} {activeCfg.label}
</span>
<span className="thinking-text">thinking…</span>
</div>
) : (
<span className="complete-badge">βœ… Analysis complete</span>
)}
</div>
{/* Pipeline progress */}
<PipelineBar activeAgent={activeAgent} done={!streaming} />
{/* Scrollable message feed */}
<div className="agent-stream-feed">
{messages.length === 0 && streaming && (
<p
style={{
color: 'var(--text-faint)',
fontSize: '0.8rem',
padding: '0.5rem',
}}
>
Waiting for first agent response…
</p>
)}
{messages.map((msg, i) => (
<AgentMessage key={i} message={msg} />
))}
<div ref={bottomRef} />
</div>
{/* Final report */}
{report && (
<div className="final-report">
<h3>πŸ“‹ Full Investment Report</h3>
<div className="report-content">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{report.content?.replace('##ANALYSIS_DONE##', '').trim()}</ReactMarkdown>
</div>
</div>
)}
</div>
)
}