import { useEffect, useState, useRef } from 'react' import useSWR, { mutate } from 'swr' import { api } from './lib/api' import { ChartTabs } from './components/ChartTabs' import { AITradersPage } from './components/AITradersPage' import { LoginPage } from './components/LoginPage' import { RegisterPage } from './components/RegisterPage' import { ResetPasswordPage } from './components/ResetPasswordPage' import { CompetitionPage } from './components/CompetitionPage' import { LandingPage } from './pages/LandingPage' import { FAQPage } from './pages/FAQPage' import { StrategyStudioPage } from './pages/StrategyStudioPage' import { DebateArenaPage } from './pages/DebateArenaPage' import HeaderBar from './components/HeaderBar' import { LanguageProvider, useLanguage } from './contexts/LanguageContext' import { AuthProvider, useAuth } from './contexts/AuthContext' import { ConfirmDialogProvider } from './components/ConfirmDialog' import { t, type Language } from './i18n/translations' import { confirmToast, notify } from './lib/notify' import { useSystemConfig } from './hooks/useSystemConfig' import { DecisionCard } from './components/DecisionCard' import { PunkAvatar, getTraderAvatar } from './components/PunkAvatar' import { OFFICIAL_LINKS } from './constants/branding' import { BacktestPage } from './components/BacktestPage' import { LogOut, Loader2 } from 'lucide-react' import type { SystemStatus, AccountInfo, Position, DecisionRecord, Statistics, TraderInfo, Exchange, } from './types' type Page = | 'competition' | 'traders' | 'trader' | 'backtest' | 'strategy' | 'debate' | 'faq' | 'login' | 'register' // 获取友好的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() } } // Helper function to get exchange display name from exchange ID (UUID) function getExchangeDisplayNameFromList(exchangeId: string | undefined, exchanges: Exchange[] | undefined): string { if (!exchangeId) return 'Unknown' const exchange = exchanges?.find(e => e.id === exchangeId) if (!exchange) return exchangeId.substring(0, 8).toUpperCase() + '...' const typeName = exchange.exchange_type?.toUpperCase() || exchange.name return exchange.account_name ? `${typeName} - ${exchange.account_name}` : typeName } // Helper function to get exchange type from exchange ID (UUID) - for TradingView charts function getExchangeTypeFromList(exchangeId: string | undefined, exchanges: Exchange[] | undefined): string { if (!exchangeId) return 'BINANCE' const exchange = exchanges?.find(e => e.id === exchangeId) if (!exchange) return 'BINANCE' // Default to BINANCE for charts return exchange.exchange_type?.toUpperCase() || 'BINANCE' } function App() { const { language, setLanguage } = useLanguage() const { user, token, logout, isLoading } = useAuth() const { 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 === '/backtest' || hash === 'backtest') return 'backtest' if (path === '/strategy' || hash === 'strategy') return 'strategy' if (path === '/debate' || hash === 'debate') return 'debate' if (path === '/dashboard' || hash === 'trader' || hash === 'details') return 'trader' return 'competition' // 默认为竞赛页面 } const [currentPage, setCurrentPage] = useState(getInitialPage()) const [selectedTraderId, setSelectedTraderId] = useState() const [lastUpdate, setLastUpdate] = useState('--:--:--') const [decisionsLimit, setDecisionsLimit] = useState(5) // 监听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 === '/backtest' || hash === 'backtest') { setCurrentPage('backtest') } else if (path === '/strategy' || hash === 'strategy') { setCurrentPage('strategy') } else if (path === '/debate' || hash === 'debate') { setCurrentPage('debate') } 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, error: tradersError } = useSWR( user && token ? 'traders' : null, api.getTraders, { refreshInterval: 10000, shouldRetryOnError: false, // 避免在后端未运行时无限重试 } ) // 获取exchanges列表(用于显示交易所名称) const { data: exchanges } = useSWR( user && token ? 'exchanges' : null, api.getExchangeConfigs, { refreshInterval: 60000, // 1分钟刷新一次 shouldRetryOnError: false, } ) // 当获取到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}-${decisionsLimit}` : null, () => api.getLatestDecisions(selectedTraderId, decisionsLimit), { 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 === '/faq') { return } if (route === '/reset-password') { 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') } else if (page === 'faq') { console.log('Navigating to faq') window.history.pushState({}, '', '/faq') setRoute('/faq') } else if (page === 'backtest') { console.log('Navigating to backtest') window.history.pushState({}, '', '/backtest') setRoute('/backtest') setCurrentPage('backtest') } else if (page === 'strategy') { console.log('Navigating to strategy') window.history.pushState({}, '', '/strategy') setRoute('/strategy') setCurrentPage('strategy') } else if (page === 'debate') { console.log('Navigating to debate') window.history.pushState({}, '', '/debate') setRoute('/debate') setCurrentPage('debate') } console.log( 'After navigation - route:', route, 'currentPage:', currentPage ) }} />
) } // Show landing page for root route if (route === '/' || route === '') { return } // Allow unauthenticated users to open backtest page directly (others仍展示 Landing) if (!user || !token) { if (route === '/backtest' || currentPage === 'backtest') { return (
{ if (page === 'competition') { window.history.pushState({}, '', '/competition') setRoute('/competition') setCurrentPage('competition') } else if (page === 'traders') { window.history.pushState({}, '', '/traders') setRoute('/traders') setCurrentPage('traders') } }} />
) } return } // Show main app for authenticated users on other routes if (!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') } else if (page === 'backtest') { window.history.pushState({}, '', '/backtest') setRoute('/backtest') setCurrentPage('backtest') } else if (page === 'strategy') { window.history.pushState({}, '', '/strategy') setRoute('/strategy') setCurrentPage('strategy') } else if (page === 'faq') { window.history.pushState({}, '', '/faq') setRoute('/faq') } else if (page === 'debate') { window.history.pushState({}, '', '/debate') setRoute('/debate') setCurrentPage('debate') } }} /> {/* Main Content */}
{currentPage === 'competition' ? ( ) : currentPage === 'traders' ? ( { setSelectedTraderId(traderId) window.history.pushState({}, '', '/dashboard') setRoute('/dashboard') setCurrentPage('trader') }} /> ) : currentPage === 'backtest' ? ( ) : currentPage === 'strategy' ? ( ) : currentPage === 'debate' ? ( ) : ( { window.history.pushState({}, '', '/traders') setRoute('/traders') setCurrentPage('traders') }} exchanges={exchanges} /> )}
{/* Footer - Hidden on debate page */} {currentPage !== 'debate' && }
) } // Trader Details Page Component function TraderDetailsPage({ selectedTrader, status, account, positions, decisions, decisionsLimit, onDecisionsLimitChange, lastUpdate, language, traders, tradersError, selectedTraderId, onTraderSelect, onNavigateToTraders, exchanges, }: { selectedTrader?: TraderInfo traders?: TraderInfo[] tradersError?: Error selectedTraderId?: string onTraderSelect: (traderId: string) => void onNavigateToTraders: () => void status?: SystemStatus account?: AccountInfo positions?: Position[] decisions?: DecisionRecord[] decisionsLimit: number onDecisionsLimitChange: (limit: number) => void stats?: Statistics lastUpdate: string language: Language exchanges?: Exchange[] }) { const [closingPosition, setClosingPosition] = useState(null) const [selectedChartSymbol, setSelectedChartSymbol] = useState(undefined) const [chartUpdateKey, setChartUpdateKey] = useState(0) const chartSectionRef = useRef(null) // 平仓操作 const handleClosePosition = async (symbol: string, side: string) => { if (!selectedTraderId) return const confirmMsg = language === 'zh' ? `确定要平仓 ${symbol} ${side === 'LONG' ? '多仓' : '空仓'} 吗?` : `Are you sure you want to close ${symbol} ${side === 'LONG' ? 'LONG' : 'SHORT'} position?` const confirmed = await confirmToast(confirmMsg, { title: language === 'zh' ? '确认平仓' : 'Confirm Close', okText: language === 'zh' ? '确认' : 'Confirm', cancelText: language === 'zh' ? '取消' : 'Cancel', }) if (!confirmed) return setClosingPosition(symbol) try { await api.closePosition(selectedTraderId, symbol, side) notify.success(language === 'zh' ? '平仓成功' : 'Position closed successfully') // 使用 SWR mutate 刷新数据而非重新加载页面 await Promise.all([ mutate(`positions-${selectedTraderId}`), mutate(`account-${selectedTraderId}`), ]) } catch (err: unknown) { const errorMsg = err instanceof Error ? err.message : (language === 'zh' ? '平仓失败' : 'Failed to close position') notify.error(errorMsg) } finally { setClosingPosition(null) } } // If API failed with error, show empty state (likely backend not running) if (tradersError) { return (
{/* Icon */}
{/* Title */}

{t('dashboardEmptyTitle', language)}

{/* Description */}

{t('dashboardEmptyDescription', language)}

{/* CTA Button */}
) } // If traders is loaded and empty, show empty state if (traders && traders.length === 0) { return (
{/* Icon */}
{/* Title */}

{t('dashboardEmptyTitle', language)}

{/* Description */}

{t('dashboardEmptyDescription', language)}

{/* CTA Button */}
) } // If traders is still loading or selectedTrader is not ready, show skeleton 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 )} Exchange:{' '} {getExchangeDisplayNameFromList(selectedTrader.exchange_id, exchanges)} Strategy:{' '} {selectedTrader.strategy_name || 'No Strategy'} {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} />
{/* 主要内容区:左右分屏 */}
{/* 左侧:图表 + 持仓 */}
{/* Chart Tabs (Equity / K-line) */}
{/* Current Positions */}

📈 {t('currentPositions', language)}

{positions && positions.length > 0 && (
{positions.length} {t('active', language)}
)}
{positions && positions.length > 0 ? (
{positions.map((pos, i) => ( { setSelectedChartSymbol(pos.symbol) setChartUpdateKey(Date.now()) // Smooth scroll to chart with ref if (chartSectionRef.current) { chartSectionRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' }) } }} > ))}
{t('symbol', language)} {t('side', language)} {language === 'zh' ? '操作' : 'Action'} {language === 'zh' ? '入场价' : 'Entry'} {language === 'zh' ? '标记价' : 'Mark'} {language === 'zh' ? '数量' : 'Qty'} {language === 'zh' ? '价值' : 'Value'} {language === 'zh' ? '杠杆' : 'Lev.'} {language === 'zh' ? '未实现盈亏' : 'uPnL'} {language === 'zh' ? '强平价' : 'Liq.'}
{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)} {pos.leverage}x = 0 ? '#0ECB81' : '#F6465D', fontWeight: 'bold', }} > {pos.unrealized_pnl >= 0 ? '+' : ''} {pos.unrealized_pnl.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)}
)}
{/* 右侧结束 */}
) } // 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}
)}
) } // Wrap App with providers export default function AppWithProviders() { return ( ) }