Skip to content
16 changes: 15 additions & 1 deletion frontend/components/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 14 additions & 8 deletions frontend/components/LatestNews.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -16,21 +17,26 @@ interface NewsArticle {
impact_level?: string;
}

interface WebSocketMessage {
type: string;
data: NewsArticle[] | Record<string, unknown>;
timestamp: number;
}

export default function LatestNews({ lastMessage }: { lastMessage: WebSocketMessage | null }) {
const [news, setNews] = useState<NewsArticle[]>([]);

// eslint-disable react-hooks/exhaustive-deps
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) => {
Expand Down
70 changes: 57 additions & 13 deletions frontend/components/Leaderboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, RawPoint[]> = {};
Object.entries(d.data as Record<string, RawPoint[]>)
.forEach(([agentId, points]) => {
const sorted = points
Object.entries(d.data as Record<string, unknown>).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());
}
Expand All @@ -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();
}
}
}
},
Expand All @@ -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();
}, []);
Expand Down
77 changes: 50 additions & 27 deletions frontend/components/ReasoningFeed.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -19,9 +20,9 @@ interface Decision {
timestamp: number;
}

interface WebSocketMessage {
type: string;
data: Decision | Record<string, unknown>;
interface AgentThinkingData {
agent_id: string;
agent_name: string;
timestamp: number;
}

Expand All @@ -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<string, unknown>;
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<string, unknown>;
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]);

Expand Down
2 changes: 1 addition & 1 deletion frontend/lib/websocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useEffect, useRef, useState } from 'react';

export interface WebSocketMessage {
type: string;
data: any;
data: unknown;
timestamp: number;
}

Expand Down
Loading