import { useEffect, useState } from 'react'; import useSWR from 'swr'; import { api } from './lib/api'; import { EquityChart } from './components/EquityChart'; import { AITradersPage } from './components/AITradersPage'; import { LoginPage } from './components/LoginPage'; import { RegisterPage } from './components/RegisterPage'; import { CompetitionPage } from './components/CompetitionPage'; import { LandingPage } from './pages/LandingPage'; import HeaderBar from './components/landing/HeaderBar'; import AILearning from './components/AILearning'; import { LanguageProvider, useLanguage } from './contexts/LanguageContext'; import { AuthProvider, useAuth } from './contexts/AuthContext'; import { t, type Language } from './i18n/translations'; import { useSystemConfig } from './hooks/useSystemConfig'; import { Zap } from 'lucide-react'; import type { SystemStatus, AccountInfo, Position, DecisionRecord, Statistics, TraderInfo, } from './types'; type Page = 'competition' | 'traders' | 'trader'; // 获取友好的AI模型名称 function getModelDisplayName(modelId: string): string { switch (modelId.toLowerCase()) { case 'deepseek': return 'DeepSeek'; case 'qwen': return 'Qwen'; case 'claude': return 'Claude'; default: return modelId.toUpperCase(); } } function App() { const { language, setLanguage } = useLanguage(); const { user, token, logout, isLoading } = useAuth(); const { config: systemConfig, loading: configLoading } = useSystemConfig(); const [route, setRoute] = useState(window.location.pathname); // 从URL路径读取初始页面状态(支持刷新保持页面) const getInitialPage = (): Page => { const path = window.location.pathname; const hash = window.location.hash.slice(1); // 去掉 # if (path === '/traders' || hash === 'traders') return 'traders'; if (path === '/dashboard' || hash === 'trader' || hash === 'details') return 'trader'; return 'competition'; // 默认为竞赛页面 }; const [currentPage, setCurrentPage] = useState(getInitialPage()); const [selectedTraderId, setSelectedTraderId] = useState(); const [lastUpdate, setLastUpdate] = useState('--:--:--'); // 监听URL变化,同步页面状态 useEffect(() => { const handleRouteChange = () => { const path = window.location.pathname; const hash = window.location.hash.slice(1); if (path === '/traders' || hash === 'traders') { setCurrentPage('traders'); } else if (path === '/dashboard' || hash === 'trader' || hash === 'details') { setCurrentPage('trader'); } else if (path === '/competition' || hash === 'competition' || hash === '') { setCurrentPage('competition'); } setRoute(path); }; window.addEventListener('hashchange', handleRouteChange); window.addEventListener('popstate', handleRouteChange); return () => { window.removeEventListener('hashchange', handleRouteChange); window.removeEventListener('popstate', handleRouteChange); }; }, []); // 切换页面时更新URL hash (当前通过按钮直接调用setCurrentPage,这个函数暂时保留用于未来扩展) // const navigateToPage = (page: Page) => { // setCurrentPage(page); // window.location.hash = page === 'competition' ? '' : 'trader'; // }; // 获取trader列表(仅在用户登录时) const { data: traders } = useSWR( user && token ? 'traders' : null, api.getTraders, { refreshInterval: 10000, } ); // 当获取到traders后,设置默认选中第一个 useEffect(() => { if (traders && traders.length > 0 && !selectedTraderId) { setSelectedTraderId(traders[0].trader_id); } }, [traders, selectedTraderId]); // 如果在trader页面,获取该trader的数据 const { data: status } = useSWR( currentPage === 'trader' && selectedTraderId ? `status-${selectedTraderId}` : null, () => api.getStatus(selectedTraderId), { refreshInterval: 15000, // 15秒刷新(配合后端15秒缓存) revalidateOnFocus: false, // 禁用聚焦时重新验证,减少请求 dedupingInterval: 10000, // 10秒去重,防止短时间内重复请求 } ); const { data: account } = useSWR( currentPage === 'trader' && selectedTraderId ? `account-${selectedTraderId}` : null, () => api.getAccount(selectedTraderId), { refreshInterval: 15000, // 15秒刷新(配合后端15秒缓存) revalidateOnFocus: false, // 禁用聚焦时重新验证,减少请求 dedupingInterval: 10000, // 10秒去重,防止短时间内重复请求 } ); const { data: positions } = useSWR( currentPage === 'trader' && selectedTraderId ? `positions-${selectedTraderId}` : null, () => api.getPositions(selectedTraderId), { refreshInterval: 15000, // 15秒刷新(配合后端15秒缓存) revalidateOnFocus: false, // 禁用聚焦时重新验证,减少请求 dedupingInterval: 10000, // 10秒去重,防止短时间内重复请求 } ); const { data: decisions } = useSWR( currentPage === 'trader' && selectedTraderId ? `decisions/latest-${selectedTraderId}` : null, () => api.getLatestDecisions(selectedTraderId), { refreshInterval: 30000, // 30秒刷新(决策更新频率较低) revalidateOnFocus: false, dedupingInterval: 20000, } ); const { data: stats } = useSWR( currentPage === 'trader' && selectedTraderId ? `statistics-${selectedTraderId}` : null, () => api.getStatistics(selectedTraderId), { refreshInterval: 30000, // 30秒刷新(统计数据更新频率较低) revalidateOnFocus: false, dedupingInterval: 20000, } ); useEffect(() => { if (account) { const now = new Date().toLocaleTimeString(); setLastUpdate(now); } }, [account]); const selectedTrader = traders?.find((t) => t.trader_id === selectedTraderId); // Handle routing useEffect(() => { const handlePopState = () => { setRoute(window.location.pathname); }; window.addEventListener('popstate', handlePopState); return () => window.removeEventListener('popstate', handlePopState); }, []); // Set current page based on route for consistent navigation state useEffect(() => { if (route === '/competition') { setCurrentPage('competition'); } else if (route === '/traders') { setCurrentPage('traders'); } else if (route === '/dashboard') { setCurrentPage('trader'); } }, [route]); // Show loading spinner while checking auth or config if (isLoading || configLoading) { return (
NoFx Logo

{t('loading', language)}

); } // Handle specific routes regardless of authentication if (route === '/login') { return ; } if (route === '/register') { return ; } if (route === '/competition') { return (
{ console.log('Competition page onPageChange called with:', page); console.log('Current route:', route, 'Current page:', currentPage); if (page === 'competition') { console.log('Navigating to competition'); window.history.pushState({}, '', '/competition'); setRoute('/competition'); setCurrentPage('competition'); } else if (page === 'traders') { console.log('Navigating to traders'); window.history.pushState({}, '', '/traders'); setRoute('/traders'); setCurrentPage('traders'); } else if (page === 'trader') { console.log('Navigating to trader/dashboard'); window.history.pushState({}, '', '/dashboard'); setRoute('/dashboard'); setCurrentPage('trader'); } console.log('After navigation - route:', route, 'currentPage:', currentPage); }} />
); } // Show landing page for root route if (route === '/' || route === '') { return ; } // Show main app for authenticated users on other routes if (!systemConfig?.admin_mode && (!user || !token)) { // Default to landing page when not authenticated and no specific route return ; } return (
{/* Header - Binance Style */}
{/* Desktop Layout */}
{/* Left - Logo and Title */}
NOFX

{t('appTitle', language)}

{t('subtitle', language)}

{/* Center - Page Toggle (absolutely positioned) */}
{/* Right - Actions */}
{/* User Info - Only show if not in admin mode */} {!systemConfig?.admin_mode && user && (
{user.email[0].toUpperCase()}
{user.email}
)} {/* Admin Mode Indicator */} {systemConfig?.admin_mode && (
{t('adminMode', language)}
)} {/* Language Toggle */}
{/* Logout Button - Only show if not in admin mode */} {!systemConfig?.admin_mode && ( )}
{/* Mobile Layout */}
{/* Top Row - Logo, Title and Language */}
NOFX

{t('appTitle', language)}

{t('subtitle', language)}

{/* Language Toggle - Right side on mobile */}
{/* Second Row - Page Toggle */}
{/* Third Row - User Info and Logout */}
{/* User Info or Admin Mode */} {!systemConfig?.admin_mode && user && (
{user.email[0].toUpperCase()}
{user.email}
)} {systemConfig?.admin_mode && (
{t('adminMode', language)}
)} {/* Logout Button */} {!systemConfig?.admin_mode && ( )}
{/* Main Content */}
{currentPage === 'competition' ? ( ) : currentPage === 'traders' ? ( { setSelectedTraderId(traderId); window.history.pushState({}, '', '/dashboard'); setRoute('/dashboard'); setCurrentPage('trader'); }} /> ) : ( )}
{/* Footer */}
); } // Trader Details Page Component function TraderDetailsPage({ selectedTrader, status, account, positions, decisions, lastUpdate, language, traders, selectedTraderId, onTraderSelect, }: { selectedTrader?: TraderInfo; traders?: TraderInfo[]; selectedTraderId?: string; onTraderSelect: (traderId: string) => void; status?: SystemStatus; account?: AccountInfo; positions?: Position[]; decisions?: DecisionRecord[]; stats?: Statistics; lastUpdate: string; language: Language; }) { if (!selectedTrader) { return (
{/* Loading Skeleton - Binance Style */}
{[1, 2, 3, 4].map((i) => (
))}
); } return (
{/* Trader Header */}

🤖 {selectedTrader.trader_name}

{/* Trader Selector */} {traders && traders.length > 0 && (
{t('switchTrader', language)}:
)}
AI Model: {getModelDisplayName(selectedTrader.ai_model.split('_').pop() || selectedTrader.ai_model)} {status && ( <> Cycles: {status.call_count} Runtime: {status.runtime_minutes} min )}
{/* Debug Info */} {account && (
🔄 Last Update: {lastUpdate} | Total Equity: {account?.total_equity?.toFixed(2) || '0.00'} | Available: {account?.available_balance?.toFixed(2) || '0.00'} | P&L: {account?.total_pnl?.toFixed(2) || '0.00'}{' '} ({account?.total_pnl_pct?.toFixed(2) || '0.00'}%)
)} {/* Account Overview */}
0} /> = 0 ? '+' : ''}${account?.total_pnl?.toFixed(2) || '0.00'} USDT`} change={account?.total_pnl_pct || 0} positive={(account?.total_pnl ?? 0) >= 0} />
{/* 主要内容区:左右分屏 */}
{/* 左侧:图表 + 持仓 */}
{/* Equity Chart */}
{/* Current Positions */}

📈 {t('currentPositions', language)}

{positions && positions.length > 0 && (
{positions.length} {t('active', language)}
)}
{positions && positions.length > 0 ? (
{positions.map((pos, i) => ( ))}
{t('symbol', language)} {t('side', language)} {t('entryPrice', language)} {t('markPrice', language)} {t('quantity', language)} {t('positionValue', language)} {t('leverage', language)} {t('unrealizedPnL', language)} {t('liqPrice', language)}
{pos.symbol} {t(pos.side === 'long' ? 'long' : 'short', language)} {pos.entry_price.toFixed(4)} {pos.mark_price.toFixed(4)} {pos.quantity.toFixed(4)} {(pos.quantity * pos.mark_price).toFixed(2)} USDT {pos.leverage}x = 0 ? '#0ECB81' : '#F6465D', fontWeight: 'bold' }} > {pos.unrealized_pnl >= 0 ? '+' : ''} {pos.unrealized_pnl.toFixed(2)} ({pos.unrealized_pnl_pct.toFixed(2)}%) {pos.liquidation_price.toFixed(4)}
) : (
📊
{t('noPositions', language)}
{t('noActivePositions', language)}
)}
{/* 左侧结束 */} {/* 右侧:Recent Decisions - 卡片容器 */}
{/* 标题 */}
🧠

{t('recentDecisions', language)}

{decisions && decisions.length > 0 && (
{t('lastCycles', language, { count: decisions.length })}
)}
{/* 决策列表 - 可滚动 */}
{decisions && decisions.length > 0 ? ( decisions.map((decision, i) => ( )) ) : (
🧠
{t('noDecisionsYet', language)}
{t('aiDecisionsWillAppear', language)}
)}
{/* 右侧结束 */}
{/* AI Learning & Performance Analysis */}
); } // Stat Card Component - Binance Style Enhanced function StatCard({ title, value, change, positive, subtitle, }: { title: string; value: string; change?: number; positive?: boolean; subtitle?: string; }) { return (
{title}
{value}
{change !== undefined && (
{positive ? '▲' : '▼'} {positive ? '+' : ''} {change.toFixed(2)}%
)} {subtitle &&
{subtitle}
}
); } // Decision Card Component with CoT Trace - Binance Style function DecisionCard({ decision, language }: { decision: DecisionRecord; language: Language }) { const [showInputPrompt, setShowInputPrompt] = useState(false); const [showCoT, setShowCoT] = useState(false); return (
{/* Header */}
{t('cycle', language)} #{decision.cycle_number}
{new Date(decision.timestamp).toLocaleString()}
{t(decision.success ? 'success' : 'failed', language)}
{/* Input Prompt - Collapsible */} {decision.input_prompt && (
{showInputPrompt && (
{decision.input_prompt}
)}
)} {/* AI Chain of Thought - Collapsible */} {decision.cot_trace && (
{showCoT && (
{decision.cot_trace}
)}
)} {/* Decisions Actions */} {decision.decisions && decision.decisions.length > 0 && (
{decision.decisions.map((action, j) => (
{action.symbol} {action.action} {action.leverage > 0 && {action.leverage}x} {action.price > 0 && ( @{action.price.toFixed(4)} )} {action.success ? '✓' : '✗'} {action.error && {action.error}}
))}
)} {/* Account State Summary */} {decision.account_state && (
净值: {decision.account_state.total_balance.toFixed(2)} USDT 可用: {decision.account_state.available_balance.toFixed(2)} USDT 保证金率: {decision.account_state.margin_used_pct.toFixed(1)}% 持仓: {decision.account_state.position_count}
)} {/* Execution Logs */} {decision.execution_log && decision.execution_log.length > 0 && (
{decision.execution_log.map((log, k) => (
{log}
))}
)} {/* Error Message */} {decision.error_message && (
❌ {decision.error_message}
)}
); } // Wrap App with providers export default function AppWithProviders() { return ( ); }