import { useState, useEffect } from 'react' import useSWR from 'swr' import { api } from '../lib/api' import { notify } from '../lib/notify' import { useLanguage } from '../contexts/LanguageContext' import { PunkAvatar } from '../components/PunkAvatar' import type { DebateSession, DebateSessionWithDetails, DebateMessage, CreateDebateRequest, AIModel, Strategy, DebatePersonality, TraderInfo, } from '../types' import { Plus, X, Trophy, Loader2, TrendingUp, TrendingDown, Minus, Clock, Zap, ChevronDown, ChevronUp, } from 'lucide-react' import { DeepVoidBackground } from '../components/DeepVoidBackground' // Translations const T: Record> = { newDebate: { zh: '新建辩论', en: 'New Debate' }, debateSessions: { zh: '辩论会话', en: 'Sessions' }, onlineTraders: { zh: '在线交易员', en: 'Online Traders' }, offline: { zh: '离线', en: 'Offline' }, noTraders: { zh: '暂无交易员', en: 'No traders' }, start: { zh: '开始', en: 'Start' }, delete: { zh: '删除', en: 'Delete' }, discussionRecords: { zh: '讨论记录', en: 'Discussion' }, finalVotes: { zh: '最终投票', en: 'Final Votes' }, consensus: { zh: '共识', en: 'Consensus' }, confidence: { zh: '信心', en: 'Confidence' }, leverage: { zh: '杠杆', en: 'Leverage' }, position: { zh: '仓位', en: 'Position' }, execute: { zh: '执行', en: 'Execute' }, executed: { zh: '已执行', en: 'Executed' }, selectOrCreate: { zh: '选择或创建辩论', en: 'Select or create a debate' }, clickToStart: { zh: '点击左侧"开始"启动辩论', en: 'Click "Start" to begin' }, waitingAI: { zh: '等待AI发言...', en: 'Waiting for AI...' }, createDebate: { zh: '创建辩论', en: 'Create Debate' }, debateName: { zh: '辩论名称', en: 'Debate Name' }, tradingPair: { zh: '交易对', en: 'Trading Pair' }, strategy: { zh: '策略', en: 'Strategy' }, rounds: { zh: '轮数', en: 'Rounds' }, participants: { zh: '参与者', en: 'Participants' }, addAI: { zh: '添加AI', en: 'Add AI' }, cancel: { zh: '取消', en: 'Cancel' }, create: { zh: '创建', en: 'Create' }, creating: { zh: '创建中...', en: 'Creating...' }, executeTitle: { zh: '执行交易', en: 'Execute Trade' }, selectTrader: { zh: '选择交易员', en: 'Select Trader' }, executing: { zh: '执行中...', en: 'Executing...' }, fillNameAdd2AI: { zh: '请填写名称并添加至少2个AI', en: 'Please fill name and add at least 2 AI' }, } const t = (key: string, lang: string) => T[key]?.[lang] || T[key]?.en || key // Personality config const PERS: Record = { bull: { emoji: '🐂', color: '#22C55E', name: '多头', nameEn: 'Bull' }, bear: { emoji: '🐻', color: '#EF4444', name: '空头', nameEn: 'Bear' }, analyst: { emoji: '📊', color: '#3B82F6', name: '分析', nameEn: 'Analyst' }, contrarian: { emoji: '🔄', color: '#F59E0B', name: '逆势', nameEn: 'Contrarian' }, risk_manager: { emoji: '🛡️', color: '#8B5CF6', name: '风控', nameEn: 'Risk Mgr' }, } // Action config const ACT: Record = { open_long: { color: 'text-green-400', bg: 'bg-green-500/20', icon: , label: 'LONG' }, open_short: { color: 'text-red-400', bg: 'bg-red-500/20', icon: , label: 'SHORT' }, hold: { color: 'text-blue-400', bg: 'bg-blue-500/20', icon: , label: 'HOLD' }, wait: { color: 'text-gray-400', bg: 'bg-gray-500/20', icon: , label: 'WAIT' }, close_long: { color: 'text-yellow-400', bg: 'bg-yellow-500/20', icon: , label: 'CLOSE' }, close_short: { color: 'text-yellow-400', bg: 'bg-yellow-500/20', icon: , label: 'CLOSE' }, } // Status colors const STATUS_COLOR: Record = { pending: 'bg-gray-500', running: 'bg-blue-500 animate-pulse', voting: 'bg-yellow-500 animate-pulse', completed: 'bg-green-500', cancelled: 'bg-red-500', } // AI Provider Avatar function AIAvatar({ name, size = 24 }: { name: string; size?: number }) { const providers: Record = { claude: { bg: 'bg-orange-500', text: 'text-white', letter: 'C' }, deepseek: { bg: 'bg-blue-600', text: 'text-white', letter: 'D' }, gemini: { bg: 'bg-blue-400', text: 'text-white', letter: 'G' }, grok: { bg: 'bg-gray-700', text: 'text-white', letter: 'X' }, kimi: { bg: 'bg-purple-500', text: 'text-white', letter: 'K' }, qwen: { bg: 'bg-indigo-500', text: 'text-white', letter: 'Q' }, openai: { bg: 'bg-emerald-600', text: 'text-white', letter: 'O' }, minimax: { bg: 'bg-red-500', text: 'text-white', letter: 'M' }, gpt: { bg: 'bg-emerald-600', text: 'text-white', letter: 'O' }, } const lower = name.toLowerCase() const p = Object.entries(providers).find(([k]) => lower.includes(k))?.[1] || { bg: 'bg-gray-600', text: 'text-white', letter: name[0]?.toUpperCase() || '?' } return (
{p.letter}
) } // Message Card - Full content display like AI Testing function MessageCard({ msg }: { msg: DebateMessage }) { const [open, setOpen] = useState(false) const p = PERS[msg.personality] || PERS.analyst const a = ACT[msg.decision?.action || 'wait'] || ACT.wait // Parse content into sections const parseContent = (c: string) => { const reasoning = c.match(/([\s\S]*?)<\/reasoning>/i)?.[1]?.trim() const analysis = c.match(/([\s\S]*?)<\/analysis>/i)?.[1]?.trim() const argument = c.match(/([\s\S]*?)<\/argument>/i)?.[1]?.trim() const decision = c.match(/([\s\S]*?)<\/decision>/i)?.[1]?.trim() // Clean content - remove XML tags const cleanContent = c.replace(/<\/?[^>]+(>|$)/g, '').trim() return { reasoning: reasoning || analysis || argument, decision, fullContent: cleanContent } } const parsed = parseContent(msg.content) const previewText = parsed.reasoning?.slice(0, 150) || parsed.fullContent.slice(0, 150) return (
{/* Header - Always visible */}
setOpen(!open)} > {msg.ai_model_name} {p.nameEn}
{msg.decision && ( {a.icon} {msg.decision.symbol || ''} {a.label} )} {msg.decision?.confidence || msg.confidence}% {open ? : }
{/* Preview when collapsed */} {!open && (
{previewText}...
)} {/* Expanded Content - Full display */} {open && (
{/* Reasoning/Analysis Section */} {parsed.reasoning && (
💭 思考过程 / Reasoning
{parsed.reasoning}
)} {/* Decision Section */} {msg.decision && (
📊 交易决策 / Decision
{msg.decision.symbol && (
币种 {msg.decision.symbol}
)}
方向 {a.label}
信心 {msg.decision.confidence}%
{(msg.decision.leverage ?? 0) > 0 && (
杠杆 {msg.decision.leverage}x
)} {(msg.decision.position_pct ?? 0) > 0 && (
仓位 {((msg.decision.position_pct ?? 0) * 100).toFixed(0)}%
)} {(msg.decision.stop_loss ?? 0) > 0 && (
止损 {((msg.decision.stop_loss ?? 0) * 100).toFixed(1)}%
)} {(msg.decision.take_profit ?? 0) > 0 && (
止盈 {((msg.decision.take_profit ?? 0) * 100).toFixed(1)}%
)}
{msg.decision.reasoning && (
{msg.decision.reasoning}
)}
)} {/* Full Raw Content (collapsible) */} {!parsed.reasoning && (
📝 完整输出 / Full Output
{parsed.fullContent}
)} {/* Multi-coin decisions if available */} {msg.decisions && msg.decisions.length > 1 && (
🎯 多币种决策 ({msg.decisions.length})
{msg.decisions.map((d, i) => { const da = ACT[d.action] || ACT.wait return (
{d.symbol} {da.icon} {da.label} {d.confidence}% {d.leverage || 0}x / {((d.position_pct || 0) * 100).toFixed(0)}%
) })}
)}
)}
) } // Vote Card - Beautiful detailed version function VoteCard({ vote }: { vote: { ai_model_name: string; action: string; symbol?: string; confidence: number; leverage?: number; position_pct?: number; stop_loss_pct?: number; take_profit_pct?: number; reasoning: string } }) { const a = ACT[vote.action] || ACT.wait const confColor = vote.confidence >= 70 ? 'bg-green-500' : vote.confidence >= 50 ? 'bg-yellow-500' : 'bg-gray-500' return (
{vote.ai_model_name} {vote.symbol && {vote.symbol}}
{a.icon} {vote.action.replace('_', ' ').toUpperCase()}
Confidence {vote.confidence}%
Leverage{vote.leverage || '-'}x
Position{vote.position_pct ? `${(vote.position_pct * 100).toFixed(0)}%` : '-'}
SL{vote.stop_loss_pct ? `${(vote.stop_loss_pct * 100).toFixed(1)}%` : '-'}
TP{vote.take_profit_pct ? `${(vote.take_profit_pct * 100).toFixed(1)}%` : '-'}
{vote.reasoning && (

{vote.reasoning}

)}
) } // Create Modal (simplified) function CreateModal({ isOpen, onClose, onCreate, aiModels, strategies, language }: { isOpen: boolean; onClose: () => void; onCreate: (r: CreateDebateRequest) => Promise aiModels: AIModel[]; strategies: Strategy[]; language: string }) { const [name, setName] = useState('') const [symbol, setSymbol] = useState('') const [strategyId, setStrategyId] = useState('') const [maxRounds, setMaxRounds] = useState(3) const [participants, setParticipants] = useState<{ ai_model_id: string; personality: DebatePersonality }[]>([]) const [creating, setCreating] = useState(false) // Get the selected strategy's coin source config const selectedStrategy = strategies.find(s => s.id === strategyId) const coinSource = selectedStrategy?.config?.coin_source const sourceType = coinSource?.source_type || 'static' const staticCoins = coinSource?.static_coins || [] // Only show coin selector for static type with coins defined const isStaticWithCoins = sourceType === 'static' && staticCoins.length > 0 useEffect(() => { if (isOpen) { const firstStrategy = strategies[0] const firstStrategyId = firstStrategy?.id || '' const firstCoinSource = firstStrategy?.config?.coin_source const firstSourceType = firstCoinSource?.source_type || 'static' const firstStaticCoins = firstCoinSource?.static_coins || [] setName('') setStrategyId(firstStrategyId) // Only set symbol for static type, otherwise leave empty (backend will choose) setSymbol(firstSourceType === 'static' && firstStaticCoins.length > 0 ? firstStaticCoins[0] : '') setMaxRounds(3) setParticipants([]) } }, [isOpen, strategies]) // Update symbol when strategy changes useEffect(() => { if (isStaticWithCoins) { if (!staticCoins.includes(symbol)) { setSymbol(staticCoins[0]) } } else { // Non-static strategy: clear symbol, backend will auto-select setSymbol('') } }, [strategyId, isStaticWithCoins, staticCoins, symbol]) const addP = () => { if (participants.length >= 10 || aiModels.length === 0) return // Allow same AI model to be used multiple times with different personalities const order: DebatePersonality[] = ['bull', 'bear', 'analyst', 'contrarian', 'risk_manager'] // Cycle through personalities const nextPersonality = order[participants.length % order.length] setParticipants([...participants, { ai_model_id: aiModels[0].id, personality: nextPersonality }]) } const submit = async () => { if (!name || !strategyId || participants.length < 2) { notify.error(t('fillNameAdd2AI', language)) return } setCreating(true) try { await onCreate({ name, symbol, strategy_id: strategyId, max_rounds: maxRounds, participants }) onClose() } finally { setCreating(false) } } if (!isOpen) return null return (

{t('createDebate', language)}

setName(e.target.value)} placeholder={t('debateName', language)} className="w-full px-3 py-2 rounded-lg bg-nofx-bg border border-nofx-gold/20 text-nofx-text text-sm outline-none focus:border-nofx-gold" /> {/* Strategy selector - moved up */}
{/* Show dropdown only for static type with coins defined */} {isStaticWithCoins ? ( ) : (
{language === 'zh' ? '根据策略规则自动选择' : 'Auto-selected by strategy'}
)}
{/* Participants */}
{participants.map((p, i) => (
{/* Personality selector */} {/* AI model selector */}
))}
) } // Main Page export function DebateArenaPage() { const { language } = useLanguage() const [selectedId, setSelectedId] = useState(null) const [showCreate, setShowCreate] = useState(false) const [execId, setExecId] = useState(null) const [traderId, setTraderId] = useState('') const [executing, setExecuting] = useState(false) const { data: debates, mutate: mutateList } = useSWR('debates', api.getDebates, { refreshInterval: 5000 }) const { data: aiModels } = useSWR('ai-models', api.getModelConfigs) const { data: strategies } = useSWR('strategies', api.getStrategies) const { data: traders } = useSWR('traders', api.getTraders) const { data: detail, mutate: mutateDetail } = useSWR( selectedId ? `debate-${selectedId}` : null, () => api.getDebate(selectedId!), { refreshInterval: selectedId ? 3000 : 0 } ) useEffect(() => { if (debates?.length && !selectedId) setSelectedId(debates[0].id) }, [debates, selectedId]) const onCreate = async (r: CreateDebateRequest) => { const d = await api.createDebate(r) notify.success('创建成功') mutateList() setSelectedId(d.id) } const onStart = async (id: string) => { await api.startDebate(id) notify.success('已开始') mutateList(); mutateDetail() } const onDelete = async (id: string) => { await api.deleteDebate(id) notify.success('已删除') if (selectedId === id) setSelectedId(null) mutateList() } const onExecute = async () => { if (!execId || !traderId) return setExecuting(true) try { await api.executeDebate(execId, traderId) notify.success('已执行') mutateDetail(); mutateList() setExecId(null); setTraderId('') } catch (e: any) { notify.error(e.message) } finally { setExecuting(false) } } // Process data const messages = detail?.messages || [] const participants = detail?.participants || [] const votes = detail?.votes || [] const decision = detail?.final_decision // Get strategy name const strategyName = strategies?.find(s => s.id === detail?.strategy_id)?.name || '' // Group by round const rounds: Record = {} messages.forEach(m => { if (!rounds[m.round]) rounds[m.round] = []; rounds[m.round].push(m) }) // Vote summary const voteSum = votes.reduce((a, v) => { a[v.action] = (a[v.action] || 0) + 1; return a }, {} as Record) return ( {/* Left - Debate List + Online Traders */}
{/* New Debate Button */} {/* Debate List */}
{t('debateSessions', language)}
{debates?.map(d => (
setSelectedId(d.id)} className={`p-2 cursor-pointer border-l-2 transition-all ${selectedId === d.id ? 'bg-nofx-gold/10 border-nofx-gold shadow-[inset_10px_0_20px_-10px_rgba(240,185,11,0.2)]' : 'border-transparent hover:bg-nofx-bg-lighter/50'}`}>
{d.name}
{d.symbol} · R{d.current_round}/{d.max_rounds}
{d.status === 'pending' && selectedId === d.id && (
)}
))}
{/* Online Traders Section */}
{t('onlineTraders', language)}
{traders?.filter(tr => tr.is_running).map(tr => (
{ setTraderId(tr.trader_id); if (decision && !decision.executed) setExecId(detail?.id || null) }} className={`p-2 rounded-lg cursor-pointer transition-all ${traderId === tr.trader_id ? 'bg-nofx-success/20 ring-1 ring-nofx-success' : 'bg-nofx-bg-lighter hover:bg-nofx-bg-light'}`}>
{tr.trader_name}
{tr.ai_model}
))} {traders?.filter(tr => !tr.is_running).slice(0, 3).map(tr => (
{tr.trader_name}
{t('offline', language)}
))} {(!traders || traders.length === 0) && (
{t('noTraders', language)}
)}
{/* Main Content */}
{detail ? ( <> {/* Header Bar - Compact */}
{detail.name} {detail.symbol} {strategyName && {strategyName}} R{detail.current_round}/{detail.max_rounds} {/* Participants */}
{participants.map(p => { const vote = votes.find(v => v.ai_model_id === p.ai_model_id) const act = vote ? (ACT[vote.action] || ACT.wait) : null return (
{act && {act.icon}}
) })}
{/* Vote Summary */} {votes.length > 0 && (
{Object.entries(voteSum).map(([action, count]) => { const cfg = ACT[action] || ACT.wait return (
{cfg.icon} {cfg.label}×{count}
) })}
)}
{/* Main Content Area - Two Column Layout */}
{Object.keys(rounds).length === 0 ? (
{detail.status === 'pending' ? '🎯' : '⏳'}
{detail.status === 'pending' ? t('clickToStart', language) : t('waitingAI', language)}
) : ( <> {/* Left - Rounds */}
{t('discussionRecords', language)}
{Object.entries(rounds).map(([round, msgs]) => (
Round {round}
{msgs.map(m => )}
))}
{/* Right - Votes */} {votes.length > 0 && (
{t('finalVotes', language)}
{votes.map(v => ( ))}
)} )}
{/* Consensus Bar - Show when votes exist */} {(decision || votes.length > 0) && (
{t('consensus', language)}: {decision ? ( <> {decision.symbol && {decision.symbol}} {(ACT[decision.action] || ACT.wait).icon} {decision.action.replace('_', ' ').toUpperCase()} ) : ( VOTING... )}
{decision && (
{t('confidence', language)} {decision.confidence || 0}% {(decision.leverage ?? 0) > 0 && {t('leverage', language)} {decision.leverage}x} {(decision.position_pct ?? 0) > 0 && {t('position', language)} {((decision.position_pct ?? 0) * 100).toFixed(0)}%} {(decision.stop_loss ?? 0) > 0 && SL {((decision.stop_loss ?? 0) * 100).toFixed(1)}%} {(decision.take_profit ?? 0) > 0 && TP {((decision.take_profit ?? 0) * 100).toFixed(1)}%}
)}
{decision && !decision.executed && (decision.action === 'open_long' || decision.action === 'open_short') && ( )} {decision?.executed && ✓ {t('executed', language)}}
)} ) : (
🗳️
{t('selectOrCreate', language)}
)}
{/* Create Modal */} setShowCreate(false)} onCreate={onCreate} aiModels={aiModels || []} strategies={strategies || []} language={language} /> {/* Execute Modal */} {execId && (

{t('executeTitle', language)}

⚠️ {language === 'zh' ? '将使用账户余额执行真实交易' : 'Will execute real trade with account balance'}
)} ) }