diff --git a/frontend/components/Dashboard.tsx b/frontend/components/Dashboard.tsx index 423fed9..e827c7d 100644 --- a/frontend/components/Dashboard.tsx +++ b/frontend/components/Dashboard.tsx @@ -56,7 +56,21 @@ export default function Dashboard() { useEffect(() => { fetch('http://localhost:8080/api/v1/agents') .then(res => res.json()) - .then(data => setAgents(data.data || [])); + .then((data: { success?: boolean; data?: unknown }) => { + if (data.data && Array.isArray(data.data)) { + const agents = data.data.filter((agent): agent is Agent => + agent && + typeof agent === 'object' && + typeof agent.id === 'string' && + typeof agent.name === 'string' && + typeof agent.model === 'string' && + typeof agent.current_balance === 'number' && + typeof agent.status === 'string' + ); + setAgents(agents); + } + }) + .catch(() => {}); }, []); // Initialize ref with agents when they change diff --git a/frontend/components/LatestNews.tsx b/frontend/components/LatestNews.tsx index d5db356..59982ff 100644 --- a/frontend/components/LatestNews.tsx +++ b/frontend/components/LatestNews.tsx @@ -1,5 +1,6 @@ 'use client'; +import { type WebSocketMessage } from '@/lib/websocket'; import { formatDistanceToNow } from 'date-fns'; import { tr } from 'date-fns/locale'; import { AlertCircle, ExternalLink, Newspaper, TrendingUp } from 'lucide-react'; @@ -16,12 +17,6 @@ interface NewsArticle { impact_level?: string; } -interface WebSocketMessage { - type: string; - data: NewsArticle[] | Record; - timestamp: number; -} - export default function LatestNews({ lastMessage }: { lastMessage: WebSocketMessage | null }) { const [news, setNews] = useState([]); @@ -29,8 +24,19 @@ export default function LatestNews({ lastMessage }: { lastMessage: WebSocketMess useEffect(() => { if (!lastMessage || lastMessage.type !== 'news_update') return; - const articles = (lastMessage.data as NewsArticle[]) || []; - setNews(articles); + const data = lastMessage.data; + if (Array.isArray(data)) { + const articles = data.filter((article): article is NewsArticle => + article && + typeof article === 'object' && + typeof article.id === 'string' && + typeof article.title === 'string' && + typeof article.source === 'string' && + typeof article.url === 'string' && + typeof article.published_at === 'string' + ); + setNews(articles); + } }, [lastMessage]); const getImpactColor = (level?: string) => { diff --git a/frontend/components/Leaderboard.tsx b/frontend/components/Leaderboard.tsx index fa6a5ef..1b0578a 100644 --- a/frontend/components/Leaderboard.tsx +++ b/frontend/components/Leaderboard.tsx @@ -27,17 +27,24 @@ export default function Leaderboard() { const fetchROIHistory = () => { fetch('http://localhost:8080/api/v1/leaderboard/roi-history?limit=120') .then(r => r.json()) - .then(d => { - if (d.success && d.data) { + .then((d: { success?: boolean; data?: unknown }) => { + if (d.success && d.data && typeof d.data === 'object' && d.data !== null) { type RawPoint = { time: string; roi: number }; const transformed: Record = {}; - Object.entries(d.data as Record) - .forEach(([agentId, points]) => { - const sorted = points + Object.entries(d.data as Record).forEach(([agentId, points]) => { + if (Array.isArray(points)) { + const validPoints = points + .filter((p): p is RawPoint => + p && + typeof p === 'object' && + typeof (p as RawPoint).time === 'string' && + typeof (p as RawPoint).roi === 'number' + ) .map(p => ({ time: p.time, roi: p.roi })) .sort((a, b) => new Date(a.time).getTime() - new Date(b.time).getTime()); - transformed[agentId] = sorted; - }); + transformed[agentId] = validPoints; + } + }); setRoiHistory(transformed); setLastHistoryFetch(Date.now()); } @@ -47,11 +54,29 @@ export default function Leaderboard() { useWebSocket('ws://localhost:8080/ws', { onMessage: (msg) => { if (msg.type === 'leaderboard_updated') { - setEntries(msg.data); - // After a leaderboard update, refresh ROI history if stale (>30s) - const now = Date.now(); - if (now - lastHistoryFetch > 30000) { - fetchROIHistory(); + const data = msg.data; + if (Array.isArray(data)) { + const entries = data.filter((entry): entry is LeaderboardEntry => + entry && + typeof entry === 'object' && + typeof entry.rank === 'number' && + typeof entry.agent_id === 'string' && + typeof entry.agent_name === 'string' && + typeof entry.model === 'string' && + typeof entry.roi === 'number' && + typeof entry.profit_loss === 'number' && + typeof entry.win_rate === 'number' && + typeof entry.total_trades === 'number' && + typeof entry.balance === 'number' && + typeof entry.portfolio_value === 'number' && + typeof entry.total_value === 'number' + ); + setEntries(entries); + // After a leaderboard update, refresh ROI history if stale (>30s) + const now = Date.now(); + if (now - lastHistoryFetch > 30000) { + fetchROIHistory(); + } } } }, @@ -60,7 +85,26 @@ export default function Leaderboard() { useEffect(() => { fetch('http://localhost:8080/api/v1/leaderboard') .then(r => r.json()) - .then(d => setEntries(d.data || [])) + .then((d: { success?: boolean; data?: unknown }) => { + if (d.data && Array.isArray(d.data)) { + const entries = d.data.filter((entry): entry is LeaderboardEntry => + entry && + typeof entry === 'object' && + typeof entry.rank === 'number' && + typeof entry.agent_id === 'string' && + typeof entry.agent_name === 'string' && + typeof entry.model === 'string' && + typeof entry.roi === 'number' && + typeof entry.profit_loss === 'number' && + typeof entry.win_rate === 'number' && + typeof entry.total_trades === 'number' && + typeof entry.balance === 'number' && + typeof entry.portfolio_value === 'number' && + typeof entry.total_value === 'number' + ); + setEntries(entries); + } + }) .catch(() => {}); fetchROIHistory(); }, []); diff --git a/frontend/components/ReasoningFeed.tsx b/frontend/components/ReasoningFeed.tsx index 87c537a..1eff9bc 100644 --- a/frontend/components/ReasoningFeed.tsx +++ b/frontend/components/ReasoningFeed.tsx @@ -1,5 +1,6 @@ 'use client'; +import { type WebSocketMessage } from '@/lib/websocket'; import { formatDistanceToNow } from 'date-fns'; import { tr } from 'date-fns/locale'; import { Brain, Minus, TrendingDown, TrendingUp } from 'lucide-react'; @@ -19,9 +20,9 @@ interface Decision { timestamp: number; } -interface WebSocketMessage { - type: string; - data: Decision | Record; +interface AgentThinkingData { + agent_id: string; + agent_name: string; timestamp: number; } @@ -34,33 +35,55 @@ export default function ReasoningFeed({ lastMessage }: { lastMessage: WebSocketM if (!lastMessage) return; if (lastMessage.type === 'agent_thinking') { - const agentId = lastMessage.data.agent_id; - setThinkingAgents(prev => new Set(prev).add(agentId)); + const data = lastMessage.data; + if (data && typeof data === 'object' && data !== null && 'agent_id' in data) { + const typedData = data as Record; + const agentIdValue = typedData.agent_id; + if (typeof agentIdValue === 'string' && agentIdValue.length > 0) { + const agentId: string = agentIdValue; + setThinkingAgents(prev => new Set(prev).add(agentId)); + } + } } if (lastMessage.type === 'agent_decision') { - const decision: Decision = { - agent_id: lastMessage.data.agent_id, - agent_name: lastMessage.data.agent_name, - decision_id: lastMessage.data.decision_id, - action: lastMessage.data.action, - stock_symbol: lastMessage.data.stock_symbol, - quantity: lastMessage.data.quantity, - reasoning_summary: lastMessage.data.reasoning_summary, - confidence: lastMessage.data.confidence, - risk_level: lastMessage.data.risk_level, - thinking_steps: lastMessage.data.thinking_steps || [], - timestamp: lastMessage.data.timestamp || lastMessage.timestamp, - }; - - setDecisions(prev => [decision, ...prev].slice(0, 10)); - - // Remove from thinking agents - setThinkingAgents(prev => { - const next = new Set(prev); - next.delete(decision.agent_id); - return next; - }); + const data = lastMessage.data as Decision | Record; + if ( + data && + typeof data === 'object' && + typeof data.agent_id === 'string' && + typeof data.agent_name === 'string' && + typeof data.decision_id === 'string' && + typeof data.action === 'string' && + typeof data.stock_symbol === 'string' && + typeof data.quantity === 'number' && + typeof data.reasoning_summary === 'string' && + typeof data.confidence === 'number' && + typeof data.risk_level === 'string' + ) { + const decision: Decision = { + agent_id: data.agent_id, + agent_name: data.agent_name, + decision_id: data.decision_id, + action: data.action as 'BUY' | 'SELL' | 'HOLD', + stock_symbol: data.stock_symbol, + quantity: data.quantity, + reasoning_summary: data.reasoning_summary, + confidence: data.confidence, + risk_level: data.risk_level as 'low' | 'medium' | 'high', + thinking_steps: Array.isArray(data.thinking_steps) ? data.thinking_steps as Array<{ step: string; observation: string }> : [], + timestamp: typeof data.timestamp === 'number' ? data.timestamp : lastMessage.timestamp, + }; + + setDecisions(prev => [decision, ...prev].slice(0, 10)); + + // Remove from thinking agents + setThinkingAgents(prev => { + const next = new Set(prev); + next.delete(decision.agent_id); + return next; + }); + } } }, [lastMessage]); diff --git a/frontend/lib/websocket.ts b/frontend/lib/websocket.ts index ff913f0..0857bff 100644 --- a/frontend/lib/websocket.ts +++ b/frontend/lib/websocket.ts @@ -4,7 +4,7 @@ import { useEffect, useRef, useState } from 'react'; export interface WebSocketMessage { type: string; - data: any; + data: unknown; timestamp: number; }