import { useState, useEffect, useRef, useCallback } from 'react' import { useSearchParams } from 'react-router-dom' import { Card, Progress, Timeline, Badge, Empty, Button, Tag, Result, message } from 'antd' import { CheckCircleOutlined, SyncOutlined, CloseCircleOutlined } from '@ant-design/icons' const ANALYSIS_STAGES = [ { key: 'analysts', label: '分析师团队', description: 'Market / Social / News / Fundamentals' }, { key: 'research', label: '研究员辩论', description: 'Bull vs Bear Researcher debate' }, { key: 'trader', label: '交易员', description: 'Compose investment plan' }, { key: 'risk', label: '风险管理', description: 'Aggressive vs Conservative vs Neutral' }, { key: 'portfolio', label: '组合经理', description: 'Final BUY/HOLD/SELL decision' }, ] export default function AnalysisMonitor() { const [searchParams] = useSearchParams() const taskId = searchParams.get('task_id') const [task, setTask] = useState(null) const [wsConnected, setWsConnected] = useState(false) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const wsRef = useRef(null) const fetchInitialState = useCallback(async () => { setLoading(true) try { const res = await fetch(`/api/analysis/status/${taskId}`) if (!res.ok) throw new Error('获取任务状态失败') const data = await res.json() setTask(data) } catch (err) { setError(err.message) } finally { setLoading(false) } }, [taskId]) const connectWebSocket = useCallback(() => { if (wsRef.current) wsRef.current.close() const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' const host = window.location.host const ws = new WebSocket(`${protocol}//${host}/ws/analysis/${taskId}`) ws.onopen = () => { setWsConnected(true) setError(null) } ws.onmessage = (event) => { try { const data = JSON.parse(event.data) if (data.type === 'progress') { const { type, ...taskData } = data setTask(taskData) } } catch (e) { // Ignore parse errors } } ws.onerror = () => { setError('WebSocket连接失败') setWsConnected(false) } ws.onclose = () => { setWsConnected(false) } wsRef.current = ws }, [taskId]) useEffect(() => { if (!taskId) return fetchInitialState() connectWebSocket() return () => { if (wsRef.current) wsRef.current.close() } }, [taskId, fetchInitialState, connectWebSocket]) const formatTime = (seconds) => { const mins = Math.floor(seconds / 60) const secs = seconds % 60 return `${mins}:${secs.toString().padStart(2, '0')}` } const getStageStatusIcon = (status) => { switch (status) { case 'completed': return case 'running': return case 'failed': return default: return } } const getDecisionBadge = (decision) => { if (!decision) return null const colorMap = { BUY: 'var(--color-buy)', SELL: 'var(--color-sell)', HOLD: 'var(--color-hold)', } return ( {decision} ) } return (
{/* Current Task Card */} 当前分析任务
} > {loading ? (
连接中...
) : error ? ( { fetchInitialState() connectWebSocket() }} aria-label="重新连接" > 重新连接 } /> ) : task ? ( <> {/* Task Header */}
{task.name} {task.ticker} {getDecisionBadge(task.decision)}
{/* Progress */}
{formatTime(task.elapsed)}
{/* Stages */}
{ANALYSIS_STAGES.map((stage, index) => (
{getStageStatusIcon(task.stages[index]?.status)} {stage.label}
))}
{/* Logs */}
实时日志
{task.logs.map((log, i) => (
[{log.time}]{' '} {log.stage}:{' '} {log.message}
))}
) : ( } /> )} {/* No Active Task */} {!task && (
暂无进行中的分析
在股票筛选页面选择股票并点击"分析"开始
)} ) }