Spaces:
Sleeping
Sleeping
| 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> | |
| ) | |
| } | |