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 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 (
{ console.log('Main app onPageChange called with:', page) if (page === 'competition') { window.history.pushState({}, '', '/competition') setRoute('/competition') setCurrentPage('competition') } else if (page === 'traders') { window.history.pushState({}, '', '/traders') setRoute('/traders') setCurrentPage('traders') } else if (page === 'trader') { window.history.pushState({}, '', '/dashboard') setRoute('/dashboard') setCurrentPage('trader') } }} /> {/* 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 ( ) }