feat: add DeepVoidBackground and update UI theme across pages

- Add DeepVoidBackground component with animated gradient effects
- Apply nofx theme classes to StrategyStudioPage, AITradersPage, etc.
- Update styling for consistent dark theme with gold accents
- Add PageNotFound and TraderDashboardPage components
This commit is contained in:
tinkle-community
2026-01-04 17:49:59 +08:00
parent bdfd8dc0d0
commit 50923f6a2e
22 changed files with 3493 additions and 3960 deletions

View File

@@ -27,6 +27,7 @@ import {
ChevronDown,
ChevronUp,
} from 'lucide-react'
import { DeepVoidBackground } from '../components/DeepVoidBackground'
// Translations
const T: Record<string, Record<string, string>> = {
@@ -144,7 +145,7 @@ function MessageCard({ msg }: { msg: DebateMessage }) {
return (
<div
className="p-3 rounded-lg hover:bg-white/5 transition-all border border-white/5"
className="p-3 rounded-lg hover:bg-nofx-bg-lighter/60 transition-all border border-nofx-gold/20 backdrop-blur-sm bg-nofx-bg-lighter/20"
style={{ borderLeft: `3px solid ${p.color}` }}
>
{/* Header - Always visible */}
@@ -153,16 +154,16 @@ function MessageCard({ msg }: { msg: DebateMessage }) {
onClick={() => setOpen(!open)}
>
<AIAvatar name={msg.ai_model_name} size={24} />
<span className="text-sm text-white font-medium">{msg.ai_model_name}</span>
<span className="text-xs text-gray-500">{p.nameEn}</span>
<span className="text-sm text-nofx-text font-medium">{msg.ai_model_name}</span>
<span className="text-xs text-nofx-text-muted">{p.nameEn}</span>
<div className="flex-1" />
{msg.decision && (
<span className={`flex items-center gap-1 text-xs px-2 py-0.5 rounded ${a.bg} ${a.color}`}>
{a.icon} {msg.decision.symbol || ''} {a.label}
</span>
)}
<span className="text-xs text-yellow-400 font-medium">{msg.decision?.confidence || msg.confidence}%</span>
{open ? <ChevronUp size={14} className="text-gray-500" /> : <ChevronDown size={14} className="text-gray-500" />}
<span className="text-xs text-nofx-gold font-medium">{msg.decision?.confidence || msg.confidence}%</span>
{open ? <ChevronUp size={14} className="text-nofx-text-muted" /> : <ChevronDown size={14} className="text-nofx-text-muted" />}
</div>
{/* Preview when collapsed */}
@@ -277,13 +278,13 @@ function VoteCard({ vote }: { vote: { ai_model_name: string; action: string; sym
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 (
<div className="bg-[#1a1f2e] rounded-xl p-4 border border-white/10 hover:border-white/20 transition-all">
<div className="bg-nofx-bg-lighter/40 backdrop-blur-md rounded-xl p-4 border border-nofx-gold/20 hover:border-nofx-gold/50 transition-all shadow-lg hover:shadow-[0_0_20px_rgba(240,185,11,0.1)]">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<AIAvatar name={vote.ai_model_name} size={28} />
<div>
<span className="text-white font-semibold block">{vote.ai_model_name}</span>
{vote.symbol && <span className="text-xs text-gray-400">{vote.symbol}</span>}
<span className="text-nofx-text font-semibold block">{vote.ai_model_name}</span>
{vote.symbol && <span className="text-xs text-nofx-text-muted">{vote.symbol}</span>}
</div>
</div>
<span className={`flex items-center gap-1 px-2.5 py-1 rounded-lg text-xs font-bold ${a.bg} ${a.color}`}>
@@ -300,13 +301,13 @@ function VoteCard({ vote }: { vote: { ai_model_name: string; action: string; sym
</div>
</div>
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-sm">
<div className="flex justify-between"><span className="text-gray-500">Leverage</span><span className="text-white font-semibold">{vote.leverage || '-'}x</span></div>
<div className="flex justify-between"><span className="text-gray-500">Position</span><span className="text-white font-semibold">{vote.position_pct ? `${(vote.position_pct * 100).toFixed(0)}%` : '-'}</span></div>
<div className="flex justify-between"><span className="text-gray-500">SL</span><span className="text-red-400 font-semibold">{vote.stop_loss_pct ? `${(vote.stop_loss_pct * 100).toFixed(1)}%` : '-'}</span></div>
<div className="flex justify-between"><span className="text-gray-500">TP</span><span className="text-green-400 font-semibold">{vote.take_profit_pct ? `${(vote.take_profit_pct * 100).toFixed(1)}%` : '-'}</span></div>
<div className="flex justify-between"><span className="text-nofx-text-muted">Leverage</span><span className="text-nofx-text font-semibold">{vote.leverage || '-'}x</span></div>
<div className="flex justify-between"><span className="text-nofx-text-muted">Position</span><span className="text-nofx-text font-semibold">{vote.position_pct ? `${(vote.position_pct * 100).toFixed(0)}%` : '-'}</span></div>
<div className="flex justify-between"><span className="text-nofx-text-muted">SL</span><span className="text-red-400 font-semibold">{vote.stop_loss_pct ? `${(vote.stop_loss_pct * 100).toFixed(1)}%` : '-'}</span></div>
<div className="flex justify-between"><span className="text-nofx-text-muted">TP</span><span className="text-green-400 font-semibold">{vote.take_profit_pct ? `${(vote.take_profit_pct * 100).toFixed(1)}%` : '-'}</span></div>
</div>
{vote.reasoning && (
<p className="mt-3 text-xs text-gray-400 leading-relaxed line-clamp-2 border-t border-white/5 pt-2">{vote.reasoning}</p>
<p className="mt-3 text-xs text-nofx-text-muted leading-relaxed line-clamp-2 border-t border-nofx-gold/10 pt-2">{vote.reasoning}</p>
)}
</div>
)
@@ -386,22 +387,22 @@ function CreateModal({
if (!isOpen) return null
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80">
<div className="bg-[#1a1d24] rounded-xl w-full max-w-md p-4 border border-white/10">
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
<div className="bg-nofx-bg-lighter/90 backdrop-blur-xl rounded-xl w-full max-w-md p-6 border border-nofx-gold/30 shadow-2xl shadow-nofx-gold/10">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-bold text-white">{t('createDebate', language)}</h3>
<button onClick={onClose}><X size={20} className="text-gray-400" /></button>
<h3 className="text-lg font-bold text-nofx-text">{t('createDebate', language)}</h3>
<button onClick={onClose}><X size={20} className="text-nofx-text-muted" /></button>
</div>
<div className="space-y-3">
<input
value={name} onChange={e => setName(e.target.value)}
placeholder={t('debateName', language)} className="w-full px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white text-sm"
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 */}
<select value={strategyId} onChange={e => setStrategyId(e.target.value)}
className="w-full px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white text-sm">
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">
{strategies.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
</select>
@@ -409,17 +410,17 @@ function CreateModal({
{/* Show dropdown only for static type with coins defined */}
{isStaticWithCoins ? (
<select value={symbol} onChange={e => setSymbol(e.target.value)}
className="flex-1 px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white text-sm">
className="flex-1 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">
{staticCoins.map(coin => <option key={coin} value={coin}>{coin}</option>)}
</select>
) : (
<div className="flex-1 px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-gray-400 text-sm">
<div className="flex-1 px-3 py-2 rounded-lg bg-nofx-bg border border-nofx-gold/20 text-nofx-text-muted text-sm">
{language === 'zh' ? '根据策略规则自动选择' : 'Auto-selected by strategy'}
</div>
)}
<select value={maxRounds} onChange={e => setMaxRounds(+e.target.value)}
className="px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white text-sm">
{[2,3,4,5].map(n => <option key={n} value={n}>{n} {language === 'zh' ? '轮' : 'rounds'}</option>)}
className="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">
{[2, 3, 4, 5].map(n => <option key={n} value={n}>{n} {language === 'zh' ? '轮' : 'rounds'}</option>)}
</select>
</div>
@@ -431,7 +432,7 @@ function CreateModal({
{/* Personality selector */}
<select value={p.personality} onChange={e => {
const up = [...participants]; up[i].personality = e.target.value as DebatePersonality; setParticipants(up)
}} className="bg-transparent text-white text-xs border-0 outline-none cursor-pointer">
}} className="bg-transparent text-nofx-text text-xs border-0 outline-none cursor-pointer">
{Object.entries(PERS).map(([k, v]) => (
<option key={k} value={k}>{v.emoji} {language === 'zh' ? v.name : v.nameEn}</option>
))}
@@ -439,23 +440,23 @@ function CreateModal({
{/* AI model selector */}
<select value={p.ai_model_id} onChange={e => {
const up = [...participants]; up[i].ai_model_id = e.target.value; setParticipants(up)
}} className="bg-transparent text-white text-xs border-0 outline-none">
}} className="bg-transparent text-nofx-text text-xs border-0 outline-none">
{aiModels.map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
</select>
<button onClick={() => setParticipants(participants.filter((_, j) => j !== i))}
className="text-red-400 hover:text-red-300"><X size={12} /></button>
className="text-nofx-danger hover:text-red-300"><X size={12} /></button>
</div>
))}
<button onClick={addP} className="px-2 py-1 text-xs text-yellow-400 hover:bg-yellow-500/10 rounded">
<button onClick={addP} className="px-2 py-1 text-xs text-nofx-gold hover:bg-nofx-gold/10 rounded">
+ {t('addAI', language)}
</button>
</div>
</div>
<div className="flex gap-2 mt-4">
<button onClick={onClose} className="flex-1 py-2 rounded-lg bg-white/5 text-white text-sm">{t('cancel', language)}</button>
<button onClick={onClose} className="flex-1 py-2 rounded-lg bg-nofx-bg border border-nofx-gold/20 text-nofx-text text-sm hover:bg-nofx-bg-lighter transition-colors">{t('cancel', language)}</button>
<button onClick={submit} disabled={creating}
className="flex-1 py-2 rounded-lg bg-yellow-500 text-black font-semibold text-sm disabled:opacity-50">
className="flex-1 py-2 rounded-lg bg-nofx-gold text-black font-semibold text-sm disabled:opacity-50 hover:bg-yellow-500 transition-colors">
{creating ? <Loader2 size={16} className="animate-spin mx-auto" /> : t('create', language)}
</button>
</div>
@@ -536,26 +537,27 @@ export function DebateArenaPage() {
const voteSum = votes.reduce((a, v) => { a[v.action] = (a[v.action] || 0) + 1; return a }, {} as Record<string, number>)
return (
<div className="h-full bg-[#0a0c10] flex overflow-hidden">
<DeepVoidBackground className="h-full flex overflow-hidden relative" disableAnimation>
{/* Left - Debate List + Online Traders */}
<div className="w-56 flex-shrink-0 bg-[#0d1017] border-r border-white/5 flex flex-col">
<div className="w-56 flex-shrink-0 bg-nofx-bg/80 backdrop-blur-md border-r border-nofx-gold/20 flex flex-col z-10">
{/* New Debate Button */}
<button onClick={() => setShowCreate(true)}
className="m-2 py-2 rounded-lg bg-yellow-500 text-black font-semibold text-sm flex items-center justify-center gap-1">
className="m-2 py-2 rounded-lg bg-nofx-gold text-black font-semibold text-sm flex items-center justify-center gap-1 hover:bg-yellow-500 transition-colors">
<Plus size={16} /> {t('newDebate', language)}
</button>
{/* Debate List */}
<div className="px-2 py-1 text-xs text-gray-500 font-semibold">{t('debateSessions', language)}</div>
<div className="px-2 py-1 text-xs text-nofx-text-muted font-semibold">{t('debateSessions', language)}</div>
<div className="overflow-y-auto" style={{ maxHeight: '30%' }}>
{debates?.map(d => (
<div key={d.id} onClick={() => setSelectedId(d.id)}
className={`p-2 cursor-pointer border-l-2 ${selectedId === d.id ? 'bg-yellow-500/10 border-yellow-500' : 'border-transparent hover:bg-white/5'}`}>
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'}`}>
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${STATUS_COLOR[d.status]}`} />
<span className="text-sm text-white truncate flex-1">{d.name}</span>
<span className="text-sm text-nofx-text truncate flex-1">{d.name}</span>
</div>
<div className="text-xs text-gray-500 mt-1">{d.symbol} · R{d.current_round}/{d.max_rounds}</div>
<div className="text-xs text-nofx-text-muted mt-1">{d.symbol} · R{d.current_round}/{d.max_rounds}</div>
{d.status === 'pending' && selectedId === d.id && (
<div className="flex gap-1 mt-1">
<button onClick={e => { e.stopPropagation(); onStart(d.id) }}
@@ -569,41 +571,41 @@ export function DebateArenaPage() {
</div>
{/* Online Traders Section */}
<div className="flex-1 border-t border-white/5 mt-2 overflow-hidden flex flex-col">
<div className="px-2 py-2 text-xs text-gray-500 font-semibold flex items-center gap-1">
<Zap size={12} className="text-green-400" />
<div className="flex-1 border-t border-nofx-gold/20 mt-2 overflow-hidden flex flex-col">
<div className="px-2 py-2 text-xs text-nofx-text-muted font-semibold flex items-center gap-1">
<Zap size={12} className="text-nofx-success" />
{t('onlineTraders', language)}
</div>
<div className="flex-1 overflow-y-auto px-2 space-y-2">
{traders?.filter(tr => tr.is_running).map(tr => (
<div key={tr.trader_id}
onClick={() => { 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-green-500/20 ring-1 ring-green-500' : 'bg-white/5 hover:bg-white/10'}`}>
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'}`}>
<div className="flex items-center gap-2">
<PunkAvatar seed={tr.trader_id} size={32} className="rounded-lg" />
<div className="flex-1 min-w-0">
<div className="text-sm text-white font-medium truncate">{tr.trader_name}</div>
<div className="text-xs text-gray-500 truncate">{tr.ai_model}</div>
<div className="text-sm text-nofx-text font-medium truncate">{tr.trader_name}</div>
<div className="text-xs text-nofx-text-muted truncate">{tr.ai_model}</div>
</div>
<span className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
<span className="w-2 h-2 rounded-full bg-nofx-success animate-pulse" />
</div>
</div>
))}
{traders?.filter(tr => !tr.is_running).slice(0, 3).map(tr => (
<div key={tr.trader_id} className="p-2 rounded-lg bg-white/5 opacity-50">
<div key={tr.trader_id} className="p-2 rounded-lg bg-nofx-bg-lighter opacity-50">
<div className="flex items-center gap-2">
<div className="grayscale">
<PunkAvatar seed={tr.trader_id} size={32} className="rounded-lg" />
</div>
<div className="flex-1 min-w-0">
<div className="text-sm text-white font-medium truncate">{tr.trader_name}</div>
<div className="text-xs text-gray-500">{t('offline', language)}</div>
<div className="text-sm text-nofx-text font-medium truncate">{tr.trader_name}</div>
<div className="text-xs text-nofx-text-muted">{t('offline', language)}</div>
</div>
</div>
</div>
))}
{(!traders || traders.length === 0) && (
<div className="text-xs text-gray-500 text-center py-4">{t('noTraders', language)}</div>
<div className="text-xs text-nofx-text-muted text-center py-4">{t('noTraders', language)}</div>
)}
</div>
</div>
@@ -614,12 +616,12 @@ export function DebateArenaPage() {
{detail ? (
<>
{/* Header Bar - Compact */}
<div className="px-3 py-2 border-b border-white/5 bg-[#0d1017]/50 flex items-center gap-3 flex-shrink-0">
<div className="px-3 py-2 border-b border-nofx-gold/20 bg-nofx-bg/60 backdrop-blur-md flex items-center gap-3 flex-shrink-0 shadow-sm">
<span className={`w-2 h-2 rounded-full flex-shrink-0 ${STATUS_COLOR[detail.status]}`} />
<span className="font-bold text-white truncate">{detail.name}</span>
<span className="text-yellow-400 font-semibold">{detail.symbol}</span>
<span className="font-bold text-nofx-text truncate">{detail.name}</span>
<span className="text-nofx-gold font-semibold">{detail.symbol}</span>
{strategyName && <span className="text-xs px-1.5 py-0.5 bg-purple-500/20 text-purple-400 rounded">{strategyName}</span>}
<span className="text-xs text-gray-500">R{detail.current_round}/{detail.max_rounds}</span>
<span className="text-xs text-nofx-text-muted">R{detail.current_round}/{detail.max_rounds}</span>
{/* Participants */}
<div className="flex gap-1 ml-2">
@@ -627,7 +629,7 @@ export function DebateArenaPage() {
const vote = votes.find(v => v.ai_model_id === p.ai_model_id)
const act = vote ? (ACT[vote.action] || ACT.wait) : null
return (
<div key={p.id} className="flex items-center gap-1 px-1 py-0.5 rounded bg-white/5 text-xs">
<div key={p.id} className="flex items-center gap-1 px-1 py-0.5 rounded bg-nofx-bg-lighter text-xs">
<AIAvatar name={p.ai_model_name} size={14} />
{act && <span className={`${act.color}`}>{act.icon}</span>}
</div>
@@ -662,8 +664,8 @@ export function DebateArenaPage() {
) : (
<>
{/* Left - Rounds */}
<div className="flex-1 overflow-y-auto p-4 border-r border-white/5">
<div className="text-sm text-gray-400 font-semibold mb-3 flex items-center gap-2">
<div className="flex-1 overflow-y-auto p-4 border-r border-nofx-gold/20">
<div className="text-sm text-nofx-text-muted font-semibold mb-3 flex items-center gap-2">
<span className="w-2 h-2 bg-blue-500 rounded-full"></span>
{t('discussionRecords', language)}
</div>
@@ -681,9 +683,9 @@ export function DebateArenaPage() {
{/* Right - Votes */}
{votes.length > 0 && (
<div className="w-[420px] flex-shrink-0 overflow-y-auto p-4 bg-[#0d1017]/50">
<div className="text-sm text-gray-400 font-semibold mb-3 flex items-center gap-2">
<Trophy size={16} className="text-yellow-400" />
<div className="w-[420px] flex-shrink-0 overflow-y-auto p-4 bg-nofx-bg/30 backdrop-blur-sm">
<div className="text-sm text-nofx-text-muted font-semibold mb-3 flex items-center gap-2">
<Trophy size={16} className="text-nofx-gold" />
{t('finalVotes', language)}
</div>
<div className="space-y-3">
@@ -709,37 +711,37 @@ export function DebateArenaPage() {
{/* Consensus Bar - Show when votes exist */}
{(decision || votes.length > 0) && (
<div className="p-3 border-t border-white/5 bg-gradient-to-r from-yellow-500/10 to-orange-500/10 flex items-center gap-4 flex-shrink-0">
<div className="p-3 border-t border-nofx-gold/20 bg-gradient-to-r from-nofx-gold/10 via-nofx-bg-lighter/50 to-orange-500/10 backdrop-blur-md flex items-center gap-4 flex-shrink-0">
<div className="flex items-center gap-2">
<Trophy size={20} className="text-yellow-400" />
<span className="text-sm text-gray-400">{t('consensus', language)}:</span>
<Trophy size={20} className="text-nofx-gold" />
<span className="text-sm text-nofx-text-muted">{t('consensus', language)}:</span>
{decision ? (
<>
{decision.symbol && <span className="text-yellow-400 font-bold mr-1">{decision.symbol}</span>}
{decision.symbol && <span className="text-nofx-gold font-bold mr-1">{decision.symbol}</span>}
<span className={`flex items-center gap-1 px-2 py-1 rounded font-bold ${(ACT[decision.action] || ACT.wait).bg} ${(ACT[decision.action] || ACT.wait).color}`}>
{(ACT[decision.action] || ACT.wait).icon}
{decision.action.replace('_', ' ').toUpperCase()}
</span>
</>
) : (
<span className="flex items-center gap-1 px-2 py-1 rounded font-bold bg-gray-500/20 text-gray-400">
<span className="flex items-center gap-1 px-2 py-1 rounded font-bold bg-nofx-text-muted/20 text-nofx-text-muted">
<Clock size={14} /> VOTING...
</span>
)}
</div>
{decision && (
<div className="flex items-center gap-4 text-sm">
<span><span className="text-gray-500">{t('confidence', language)}</span> <span className="text-yellow-400 font-bold">{decision.confidence || 0}%</span></span>
{(decision.leverage ?? 0) > 0 && <span><span className="text-gray-500">{t('leverage', language)}</span> <span className="text-white font-bold">{decision.leverage}x</span></span>}
{(decision.position_pct ?? 0) > 0 && <span><span className="text-gray-500">{t('position', language)}</span> <span className="text-white font-bold">{((decision.position_pct ?? 0) * 100).toFixed(0)}%</span></span>}
{(decision.stop_loss ?? 0) > 0 && <span><span className="text-gray-500">SL</span> <span className="text-red-400 font-bold">{((decision.stop_loss ?? 0) * 100).toFixed(1)}%</span></span>}
{(decision.take_profit ?? 0) > 0 && <span><span className="text-gray-500">TP</span> <span className="text-green-400 font-bold">{((decision.take_profit ?? 0) * 100).toFixed(1)}%</span></span>}
<span><span className="text-nofx-text-muted">{t('confidence', language)}</span> <span className="text-nofx-gold font-bold">{decision.confidence || 0}%</span></span>
{(decision.leverage ?? 0) > 0 && <span><span className="text-nofx-text-muted">{t('leverage', language)}</span> <span className="text-nofx-text font-bold">{decision.leverage}x</span></span>}
{(decision.position_pct ?? 0) > 0 && <span><span className="text-nofx-text-muted">{t('position', language)}</span> <span className="text-nofx-text font-bold">{((decision.position_pct ?? 0) * 100).toFixed(0)}%</span></span>}
{(decision.stop_loss ?? 0) > 0 && <span><span className="text-nofx-text-muted">SL</span> <span className="text-red-400 font-bold">{((decision.stop_loss ?? 0) * 100).toFixed(1)}%</span></span>}
{(decision.take_profit ?? 0) > 0 && <span><span className="text-nofx-text-muted">TP</span> <span className="text-green-400 font-bold">{((decision.take_profit ?? 0) * 100).toFixed(1)}%</span></span>}
</div>
)}
<div className="flex-1" />
{decision && !decision.executed && (decision.action === 'open_long' || decision.action === 'open_short') && (
<button onClick={() => setExecId(detail.id)}
className="px-4 py-1.5 rounded-lg bg-yellow-500 text-black font-semibold text-sm flex items-center gap-1">
className="px-4 py-1.5 rounded-lg bg-nofx-gold text-black font-semibold text-sm flex items-center gap-1 hover:bg-yellow-500 transition-colors">
<Zap size={14} /> {t('execute', language)}
</button>
)}
@@ -748,7 +750,7 @@ export function DebateArenaPage() {
)}
</>
) : (
<div className="flex-1 flex items-center justify-center text-gray-500">
<div className="flex-1 flex items-center justify-center text-nofx-text-muted">
<div className="text-center">
<div className="text-4xl mb-2">🗳</div>
<div>{t('selectOrCreate', language)}</div>
@@ -763,13 +765,13 @@ export function DebateArenaPage() {
{/* Execute Modal */}
{execId && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80">
<div className="bg-[#1a1d24] rounded-xl w-full max-w-sm p-4 border border-white/10">
<h3 className="text-lg font-bold text-white mb-4 flex items-center gap-2">
<Zap className="text-yellow-400" /> {t('executeTitle', language)}
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
<div className="bg-nofx-bg-lighter/90 backdrop-blur-xl rounded-xl w-full max-w-sm p-6 border border-nofx-gold/30 shadow-2xl shadow-nofx-gold/10">
<h3 className="text-lg font-bold text-nofx-text mb-4 flex items-center gap-2">
<Zap className="text-nofx-gold" /> {t('executeTitle', language)}
</h3>
<select value={traderId} onChange={e => setTraderId(e.target.value)}
className="w-full px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white text-sm mb-3">
className="w-full px-3 py-2 rounded-lg bg-nofx-bg border border-nofx-gold/20 text-nofx-text text-sm mb-3">
<option value="">{t('selectTrader', language)}...</option>
{traders?.filter(tr => tr.is_running).map(tr => (
<option key={tr.trader_id} value={tr.trader_id}> {tr.trader_name}</option>
@@ -778,20 +780,20 @@ export function DebateArenaPage() {
<option key={tr.trader_id} value={tr.trader_id} disabled> {tr.trader_name} ({t('offline', language)})</option>
))}
</select>
<div className="text-xs text-yellow-300 bg-yellow-500/10 p-2 rounded mb-3">
<div className="text-xs text-yellow-300 bg-nofx-gold/10 p-2 rounded mb-3">
{language === 'zh' ? '将使用账户余额执行真实交易' : 'Will execute real trade with account balance'}
</div>
<div className="flex gap-2">
<button onClick={() => { setExecId(null); setTraderId('') }}
className="flex-1 py-2 rounded-lg bg-white/5 text-white text-sm">{t('cancel', language)}</button>
className="flex-1 py-2 rounded-lg bg-nofx-bg text-nofx-text text-sm hover:bg-nofx-bg-light transition-colors">{t('cancel', language)}</button>
<button onClick={onExecute} disabled={!traderId || executing || !traders?.find(tr => tr.trader_id === traderId)?.is_running}
className="flex-1 py-2 rounded-lg bg-yellow-500 text-black font-semibold text-sm disabled:opacity-50">
className="flex-1 py-2 rounded-lg bg-nofx-gold text-black font-semibold text-sm disabled:opacity-50 hover:bg-yellow-500 transition-colors">
{executing ? <Loader2 size={16} className="animate-spin mx-auto" /> : t('execute', language)}
</button>
</div>
</div>
</div>
)}
</div>
</DeepVoidBackground>
)
}

View File

@@ -0,0 +1,43 @@
import { DeepVoidBackground } from '../components/DeepVoidBackground'
import { AlertCircle, Home } from 'lucide-react'
export function PageNotFound() {
return (
<DeepVoidBackground className="flex items-center justify-center text-center p-4">
<div className="bg-nofx-bg border border-nofx-gold/20 p-8 rounded-lg max-w-md w-full relative overflow-hidden group">
{/* Background Grid inside Card */}
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808008_1px,transparent_1px),linear-gradient(to_bottom,#80808008_1px,transparent_1px)] bg-[size:16px_16px] pointer-events-none"></div>
<div className="relative z-10 flex flex-col items-center gap-6">
<div className="relative">
<div className="absolute inset-0 bg-red-500/20 blur-xl animate-pulse"></div>
<AlertCircle size={64} className="text-nofx-danger relative z-10" />
</div>
<div className="space-y-2">
<h1 className="text-4xl font-bold font-mono tracking-tighter text-white">
404
</h1>
<div className="text-xs uppercase tracking-[0.3em] text-nofx-danger font-mono border-b border-nofx-danger/30 pb-2 inline-block">
SIGNAL_LOST
</div>
</div>
<p className="text-sm text-nofx-text-muted font-mono leading-relaxed">
The requested coordinates do not exist in the current sector. The page may have been moved, deleted, or never existed in this timeline.
</p>
<a
href="/"
className="flex items-center gap-2 px-6 py-3 bg-nofx-gold text-black font-bold text-sm uppercase tracking-widest rounded hover:bg-yellow-400 transition-all shadow-neon hover:shadow-[0_0_20px_rgba(240,185,11,0.4)] group mt-4"
>
<Home size={16} />
<span>RETURN_BASE</span>
<span className="opacity-0 group-hover:opacity-100 transition-opacity -ml-2 group-hover:ml-0">-&gt;</span>
</a>
</div>
</div>
</DeepVoidBackground>
)
}

View File

@@ -19,7 +19,8 @@ import {
} from 'lucide-react'
import { useLanguage } from '../contexts/LanguageContext'
import { useAuth } from '../contexts/AuthContext'
import { toast } from 'sonner' // Ensure sonner is installed or stick to custom toast if preferred
import { toast } from 'sonner'
import { DeepVoidBackground } from '../components/DeepVoidBackground'
interface PublicStrategy {
id: string
@@ -225,291 +226,289 @@ export function StrategyMarketPage() {
}
return (
<div className="min-h-screen bg-black text-white font-mono relative overflow-hidden flex flex-col items-center py-12">
{/* Background Grid & Scanlines */}
<div className="fixed inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px] pointer-events-none"></div>
<div className="fixed inset-0 bg-gradient-to-t from-black via-transparent to-transparent pointer-events-none"></div>
<div className="fixed inset-0 pointer-events-none opacity-[0.03] bg-[linear-gradient(transparent_50%,rgba(0,0,0,0.5)_50%)] bg-[length:100%_4px]"></div>
<DeepVoidBackground className="min-h-screen text-white font-mono py-12">
<div className="w-full px-4 md:px-8 space-y-8">
<div className="w-full max-w-7xl relative z-10 px-6">
<div className="w-full relative z-10">
{/* Header Section */}
<div className="mb-12 border-b border-zinc-800 pb-8 relative">
<div className="absolute top-0 right-0 p-2 border border-zinc-800 rounded bg-black/50 text-xs text-zinc-500 font-mono hidden md:block">
SYSTEM_STATUS: <span className="text-emerald-500 animate-pulse">ONLINE</span>
<br />
MARKET_UPLINK: <span className="text-emerald-500">ESTABLISHED</span>
</div>
<div className="flex items-center gap-4 mb-4">
<div className="bg-zinc-900 border border-zinc-700 p-3 rounded-none relative group overflow-hidden">
<div className="absolute inset-0 bg-nofx-gold/20 opacity-0 group-hover:opacity-100 transition-opacity"></div>
<Database className="w-8 h-8 text-nofx-gold relative z-10" />
{/* Header Section */}
<div className="mb-12 border-b border-zinc-800 pb-8 relative">
<div className="absolute top-0 right-0 p-2 border border-zinc-800 rounded bg-black/50 text-xs text-zinc-500 font-mono hidden md:block">
SYSTEM_STATUS: <span className="text-emerald-500 animate-pulse">ONLINE</span>
<br />
MARKET_UPLINK: <span className="text-emerald-500">ESTABLISHED</span>
</div>
<div>
<h1 className="text-4xl font-bold tracking-tighter text-white uppercase glitch-text" data-text={t.title}>
{t.title}
</h1>
<p className="text-xs text-nofx-gold tracking-[0.3em] font-bold mt-1">
<div className="flex items-center gap-4 mb-4">
<div className="bg-zinc-900 border border-zinc-700 p-3 rounded-none relative group overflow-hidden">
<div className="absolute inset-0 bg-nofx-gold/20 opacity-0 group-hover:opacity-100 transition-opacity"></div>
<Database className="w-8 h-8 text-nofx-gold relative z-10" />
</div>
<div>
<h1 className="text-4xl font-bold tracking-tighter text-white uppercase glitch-text" data-text={t.title}>
{t.title}
</h1>
<p className="text-xs text-nofx-gold tracking-[0.3em] font-bold mt-1">
// {t.subtitle}
</p>
</div>
</div>
<p className="text-sm text-zinc-500 max-w-2xl border-l-2 border-zinc-800 pl-4">
{t.description}
</p>
</div>
{/* Search and Filter Bar */}
<div className="flex flex-col md:flex-row gap-4 mb-8">
{/* Search */}
<div className="relative flex-1 group">
<div className="absolute -inset-0.5 bg-gradient-to-r from-nofx-gold/20 to-zinc-800/20 rounded opacity-0 group-hover:opacity-100 transition duration-500 blur"></div>
<div className="relative bg-black flex items-center border border-zinc-800 group-hover:border-nofx-gold/50 transition-colors">
<div className="pl-4 pr-3 text-zinc-500 group-hover:text-nofx-gold transition-colors">
<Terminal size={16} />
</div>
<input
type="text"
placeholder={t.search}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-transparent py-3 text-sm focus:outline-none placeholder-zinc-700 text-nofx-gold font-mono"
/>
<div className="pr-4">
<div className="w-2 h-4 bg-nofx-gold animate-pulse"></div>
</p>
</div>
</div>
<p className="text-sm text-zinc-500 max-w-2xl border-l-2 border-zinc-800 pl-4">
{t.description}
</p>
</div>
{/* Category Filter */}
<div className="flex gap-2 bg-zinc-900/50 p-1 border border-zinc-800">
{['all', 'popular', 'recent'].map((cat) => (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
className={`px-4 py-2 text-xs font-mono uppercase tracking-wider transition-all relative overflow-hidden ${selectedCategory === cat
? 'text-black font-bold'
: 'text-zinc-500 hover:text-white'
}`}
>
{selectedCategory === cat && (
<motion.div
layoutId="filter-highlight"
className="absolute inset-0 bg-nofx-gold"
transition={{ type: "spring", bounce: 0.2, duration: 0.6 }}
/>
)}
<span className="relative z-10">{t[cat as keyof typeof t]}</span>
</button>
))}
</div>
</div>
{/* Loading State */}
{isLoading && (
<div className="flex flex-col items-center justify-center py-32 space-y-4">
<div className="relative w-16 h-16">
<div className="absolute inset-0 border-2 border-zinc-800 rounded-full"></div>
<div className="absolute inset-0 border-2 border-nofx-gold rounded-full border-t-transparent animate-spin"></div>
<div className="absolute inset-0 flex items-center justify-center">
<Cpu size={24} className="text-nofx-gold/50" />
{/* Search and Filter Bar */}
<div className="flex flex-col md:flex-row gap-4 mb-8">
{/* Search */}
<div className="relative flex-1 group">
<div className="absolute -inset-0.5 bg-gradient-to-r from-nofx-gold/20 to-zinc-800/20 rounded opacity-0 group-hover:opacity-100 transition duration-500 blur"></div>
<div className="relative bg-black flex items-center border border-zinc-800 group-hover:border-nofx-gold/50 transition-colors">
<div className="pl-4 pr-3 text-zinc-500 group-hover:text-nofx-gold transition-colors">
<Terminal size={16} />
</div>
<input
type="text"
placeholder={t.search}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-transparent py-3 text-sm focus:outline-none placeholder-zinc-700 text-nofx-gold font-mono"
/>
<div className="pr-4">
<div className="w-2 h-4 bg-nofx-gold animate-pulse"></div>
</div>
</div>
</div>
<p className="text-nofx-gold text-xs tracking-widest animate-pulse">{t.loading}</p>
<div className="flex gap-1">
<div className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce" style={{ animationDelay: '0s' }}></div>
<div className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
<div className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce" style={{ animationDelay: '0.4s' }}></div>
{/* Category Filter */}
<div className="flex gap-2 bg-zinc-900/50 p-1 border border-zinc-800">
{['all', 'popular', 'recent'].map((cat) => (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
className={`px-4 py-2 text-xs font-mono uppercase tracking-wider transition-all relative overflow-hidden ${selectedCategory === cat
? 'text-black font-bold'
: 'text-zinc-500 hover:text-white'
}`}
>
{selectedCategory === cat && (
<motion.div
layoutId="filter-highlight"
className="absolute inset-0 bg-nofx-gold"
transition={{ type: "spring", bounce: 0.2, duration: 0.6 }}
/>
)}
<span className="relative z-10">{t[cat as keyof typeof t]}</span>
</button>
))}
</div>
</div>
)}
{/* Empty State */}
{!isLoading && filteredStrategies.length === 0 && (
<div className="flex flex-col items-center justify-center py-32 border border-zinc-800 border-dashed bg-zinc-900/20 rounded">
<div className="relative mb-6">
<div className="absolute -inset-4 bg-red-500/10 rounded-full blur-xl animate-pulse"></div>
<Activity className="w-16 h-16 text-zinc-700 relative z-10" />
{/* Loading State */}
{isLoading && (
<div className="flex flex-col items-center justify-center py-32 space-y-4">
<div className="relative w-16 h-16">
<div className="absolute inset-0 border-2 border-zinc-800 rounded-full"></div>
<div className="absolute inset-0 border-2 border-nofx-gold rounded-full border-t-transparent animate-spin"></div>
<div className="absolute inset-0 flex items-center justify-center">
<Cpu size={24} className="text-nofx-gold/50" />
</div>
</div>
<p className="text-nofx-gold text-xs tracking-widest animate-pulse">{t.loading}</p>
<div className="flex gap-1">
<div className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce" style={{ animationDelay: '0s' }}></div>
<div className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
<div className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce" style={{ animationDelay: '0.4s' }}></div>
</div>
</div>
<h3 className="text-xl font-bold text-zinc-300 font-mono tracking-tight mb-2">
[{t.noStrategies}]
</h3>
<p className="text-zinc-600 text-xs tracking-wide uppercase">{t.noStrategiesDesc}</p>
</div>
)}
)}
{/* Strategy Grid */}
{!isLoading && filteredStrategies.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<AnimatePresence>
{filteredStrategies.map((strategy, i) => {
const style = getStrategyStyle(strategy.name)
const Icon = style.icon
const indicators = strategy.config_visible && strategy.config
? getIndicatorList(strategy.config)
: []
{/* Empty State */}
{!isLoading && filteredStrategies.length === 0 && (
<div className="flex flex-col items-center justify-center py-32 border border-zinc-800 border-dashed bg-zinc-900/20 rounded">
<div className="relative mb-6">
<div className="absolute -inset-4 bg-red-500/10 rounded-full blur-xl animate-pulse"></div>
<Activity className="w-16 h-16 text-zinc-700 relative z-10" />
</div>
<h3 className="text-xl font-bold text-zinc-300 font-mono tracking-tight mb-2">
[{t.noStrategies}]
</h3>
<p className="text-zinc-600 text-xs tracking-wide uppercase">{t.noStrategiesDesc}</p>
</div>
)}
return (
<motion.div
key={strategy.id}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ delay: i * 0.05 }}
className={`group relative bg-black border border-zinc-800 hover:border-zinc-600 transition-all duration-300 ${style.shadow}`}
>
{/* Holographic Border Highlight */}
<div className={`absolute top-0 left-0 w-full h-[1px] bg-gradient-to-r from-transparent via-${style.color.split('-')[1]}-500 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500`}></div>
<div className={`absolute bottom-0 right-0 w-full h-[1px] bg-gradient-to-r from-transparent via-${style.color.split('-')[1]}-500 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500`}></div>
{/* Strategy Grid */}
{!isLoading && filteredStrategies.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<AnimatePresence>
{filteredStrategies.map((strategy, i) => {
const style = getStrategyStyle(strategy.name)
const Icon = style.icon
const indicators = strategy.config_visible && strategy.config
? getIndicatorList(strategy.config)
: []
{/* Category Side Strip */}
<div className={`absolute left-0 top-0 bottom-0 w-[2px] ${style.bg.replace('/5', '/50')}`}></div>
return (
<motion.div
key={strategy.id}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ delay: i * 0.05 }}
className={`group relative bg-black border border-zinc-800 hover:border-zinc-600 transition-all duration-300 ${style.shadow}`}
>
{/* Holographic Border Highlight */}
<div className={`absolute top-0 left-0 w-full h-[1px] bg-gradient-to-r from-transparent via-${style.color.split('-')[1]}-500 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500`}></div>
<div className={`absolute bottom-0 right-0 w-full h-[1px] bg-gradient-to-r from-transparent via-${style.color.split('-')[1]}-500 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500`}></div>
<div className="p-6 relative">
{/* Header */}
<div className="flex justify-between items-start mb-6">
<div className={`p-2 rounded-none border ${style.border} ${style.bg}`}>
<Icon className={`w-5 h-5 ${style.color}`} />
</div>
<div className="text-[10px] font-mono">
{strategy.config_visible ? (
<div className="flex items-center gap-1.5 text-emerald-500 border border-emerald-500/20 bg-emerald-500/10 px-2 py-1">
<Eye size={10} />
PUBLIC_ACCESS
</div>
) : (
<div className="flex items-center gap-1.5 text-zinc-500 border border-zinc-800 bg-zinc-900 px-2 py-1">
<EyeOff size={10} />
RESTRICTED
</div>
)}
</div>
</div>
{/* Category Side Strip */}
<div className={`absolute left-0 top-0 bottom-0 w-[2px] ${style.bg.replace('/5', '/50')}`}></div>
{/* Name and Description */}
<h3 className={`text-lg font-bold mb-2 tracking-tight group-hover:${style.color} transition-colors uppercase truncate relative`}>
{strategy.name}
<span className="absolute -bottom-1 left-0 w-8 h-[2px] bg-zinc-800 group-hover:bg-nofx-gold transition-colors"></span>
</h3>
<p className="text-xs text-zinc-500 mb-6 line-clamp-2 h-8 leading-relaxed font-sans">
{strategy.description || 'NO_DESCRIPTION_AVAILABLE'}
</p>
{/* Meta Data */}
<div className="grid grid-cols-2 gap-y-2 mb-6 text-[10px] font-mono text-zinc-600">
<div className="flex flex-col">
<span className="text-zinc-700 uppercase">{t.author}</span>
<span className="text-zinc-400 group-hover:text-white transition-colors">@{strategy.author_email?.split('@')[0] || 'UNKNOWN'}</span>
</div>
<div className="flex flex-col text-right">
<span className="text-zinc-700 uppercase">{t.createdAt}</span>
<span className="text-zinc-400">{formatDate(strategy.created_at)}</span>
</div>
</div>
{/* Config / Indicators */}
<div className="bg-zinc-900/30 border border-zinc-800/50 p-3 mb-4 backdrop-blur-sm min-h-[90px]">
{strategy.config_visible && strategy.config ? (
<div className="space-y-3">
{/* Indicators */}
<div className="flex items-center gap-2 overflow-x-auto scrollbar-hide pb-1">
{indicators.length > 0 ? indicators.map((ind) => (
<span
key={ind}
className="px-1.5 py-0.5 border border-zinc-700 bg-zinc-800 text-[9px] text-zinc-300 font-mono whitespace-nowrap"
>
{ind}
</span>
)) : <span className="text-[9px] text-zinc-600">NO_INDICATORS</span>}
</div>
{/* Risk Control */}
{strategy.config.risk_control && (
<div className="flex justify-between items-center text-[10px]">
<div className="flex gap-3">
<div className="flex flex-col">
<span className="text-zinc-600 scale-90 origin-left">LEV</span>
<span className="text-zinc-300 font-bold">{strategy.config.risk_control.btc_eth_max_leverage || '-'}x</span>
</div>
<div className="flex flex-col">
<span className="text-zinc-600 scale-90 origin-left">POS</span>
<span className="text-zinc-300 font-bold">{strategy.config.risk_control.max_positions || '-'}</span>
</div>
</div>
<Activity size={12} className="text-zinc-700" />
<div className="p-6 relative">
{/* Header */}
<div className="flex justify-between items-start mb-6">
<div className={`p-2 rounded-none border ${style.border} ${style.bg}`}>
<Icon className={`w-5 h-5 ${style.color}`} />
</div>
<div className="text-[10px] font-mono">
{strategy.config_visible ? (
<div className="flex items-center gap-1.5 text-emerald-500 border border-emerald-500/20 bg-emerald-500/10 px-2 py-1">
<Eye size={10} />
PUBLIC_ACCESS
</div>
) : (
<div className="flex items-center gap-1.5 text-zinc-500 border border-zinc-800 bg-zinc-900 px-2 py-1">
<EyeOff size={10} />
RESTRICTED
</div>
)}
</div>
) : (
<div className="flex flex-col items-center justify-center h-full text-zinc-600">
<EyeOff size={16} className="mb-1 opacity-50" />
<span className="text-[9px] uppercase tracking-widest">{t.configHiddenDesc}</span>
</div>
{/* Name and Description */}
<h3 className={`text-lg font-bold mb-2 tracking-tight group-hover:${style.color} transition-colors uppercase truncate relative`}>
{strategy.name}
<span className="absolute -bottom-1 left-0 w-8 h-[2px] bg-zinc-800 group-hover:bg-nofx-gold transition-colors"></span>
</h3>
<p className="text-xs text-zinc-500 mb-6 line-clamp-2 h-8 leading-relaxed font-sans">
{strategy.description || 'NO_DESCRIPTION_AVAILABLE'}
</p>
{/* Meta Data */}
<div className="grid grid-cols-2 gap-y-2 mb-6 text-[10px] font-mono text-zinc-600">
<div className="flex flex-col">
<span className="text-zinc-700 uppercase">{t.author}</span>
<span className="text-zinc-400 group-hover:text-white transition-colors">@{strategy.author_email?.split('@')[0] || 'UNKNOWN'}</span>
</div>
)}
<div className="flex flex-col text-right">
<span className="text-zinc-700 uppercase">{t.createdAt}</span>
<span className="text-zinc-400">{formatDate(strategy.created_at)}</span>
</div>
</div>
{/* Config / Indicators */}
<div className="bg-zinc-900/30 border border-zinc-800/50 p-3 mb-4 backdrop-blur-sm min-h-[90px]">
{strategy.config_visible && strategy.config ? (
<div className="space-y-3">
{/* Indicators */}
<div className="flex items-center gap-2 overflow-x-auto scrollbar-hide pb-1">
{indicators.length > 0 ? indicators.map((ind) => (
<span
key={ind}
className="px-1.5 py-0.5 border border-zinc-700 bg-zinc-800 text-[9px] text-zinc-300 font-mono whitespace-nowrap"
>
{ind}
</span>
)) : <span className="text-[9px] text-zinc-600">NO_INDICATORS</span>}
</div>
{/* Risk Control */}
{strategy.config.risk_control && (
<div className="flex justify-between items-center text-[10px]">
<div className="flex gap-3">
<div className="flex flex-col">
<span className="text-zinc-600 scale-90 origin-left">LEV</span>
<span className="text-zinc-300 font-bold">{strategy.config.risk_control.btc_eth_max_leverage || '-'}x</span>
</div>
<div className="flex flex-col">
<span className="text-zinc-600 scale-90 origin-left">POS</span>
<span className="text-zinc-300 font-bold">{strategy.config.risk_control.max_positions || '-'}</span>
</div>
</div>
<Activity size={12} className="text-zinc-700" />
</div>
)}
</div>
) : (
<div className="flex flex-col items-center justify-center h-full text-zinc-600">
<EyeOff size={16} className="mb-1 opacity-50" />
<span className="text-[9px] uppercase tracking-widest">{t.configHiddenDesc}</span>
</div>
)}
</div>
{/* Action Button */}
<div>
{strategy.config_visible && strategy.config ? (
<button
onClick={() => handleCopyConfig(strategy)}
className="w-full py-2.5 text-[10px] font-bold font-mono uppercase tracking-widest border border-zinc-700 bg-black hover:bg-zinc-900 text-zinc-300 hover:text-nofx-gold hover:border-nofx-gold transition-all flex items-center justify-center gap-2 group/btn"
>
{copiedId === strategy.id ? (
<>
<Check className="w-3 h-3 text-emerald-500" />
<span className="text-emerald-500">{t.copied}</span>
</>
) : (
<>
<Copy className="w-3 h-3 group-hover/btn:scale-110 transition-transform" />
{t.copyConfig}
</>
)}
</button>
) : (
<button disabled className="w-full py-2.5 text-[10px] font-bold font-mono uppercase tracking-widest border border-zinc-800 bg-black text-zinc-700 cursor-not-allowed flex items-center justify-center gap-2">
<Shield size={12} />
{t.hideConfig}
</button>
)}
</div>
</div>
</motion.div>
)
})}
</AnimatePresence>
</div>
)}
{/* Action Button */}
<div>
{strategy.config_visible && strategy.config ? (
<button
onClick={() => handleCopyConfig(strategy)}
className="w-full py-2.5 text-[10px] font-bold font-mono uppercase tracking-widest border border-zinc-700 bg-black hover:bg-zinc-900 text-zinc-300 hover:text-nofx-gold hover:border-nofx-gold transition-all flex items-center justify-center gap-2 group/btn"
>
{copiedId === strategy.id ? (
<>
<Check className="w-3 h-3 text-emerald-500" />
<span className="text-emerald-500">{t.copied}</span>
</>
) : (
<>
<Copy className="w-3 h-3 group-hover/btn:scale-110 transition-transform" />
{t.copyConfig}
</>
)}
</button>
) : (
<button disabled className="w-full py-2.5 text-[10px] font-bold font-mono uppercase tracking-widest border border-zinc-800 bg-black text-zinc-700 cursor-not-allowed flex items-center justify-center gap-2">
<Shield size={12} />
{t.hideConfig}
</button>
)}
</div>
</div>
</motion.div>
)
})}
</AnimatePresence>
</div>
)}
{/* CTA - Share Strategy */}
{user && token && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="mt-16 mb-20 flex justify-center"
>
<div className="relative group cursor-pointer" onClick={() => window.location.href = '/strategy'}>
<div className="absolute -inset-1 bg-gradient-to-r from-nofx-gold to-yellow-600 rounded blur opacity-25 group-hover:opacity-75 transition duration-1000 group-hover:duration-200"></div>
<div className="relative px-8 py-4 bg-black border border-zinc-800 hover:border-nofx-gold/50 flex items-center gap-4 transition-all">
<Hexagon className="text-nofx-gold animate-spin-slow" size={24} />
<div className="text-left">
<div className="text-sm font-bold text-white uppercase tracking-wider group-hover:text-nofx-gold transition-colors">{t.shareYours}</div>
<div className="text-[10px] text-zinc-500 font-mono">CONTRIBUTE TO THE GLOBAL DATABASE</div>
</div>
<div className="w-[1px] h-8 bg-zinc-800 mx-2"></div>
<div className="text-xs font-mono text-zinc-400 group-hover:translate-x-1 transition-transform">
INITIALIZE_UPLOAD -&gt;
{/* CTA - Share Strategy */}
{user && token && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="mt-16 mb-20 flex justify-center"
>
<div className="relative group cursor-pointer" onClick={() => window.location.href = '/strategy'}>
<div className="absolute -inset-1 bg-gradient-to-r from-nofx-gold to-yellow-600 rounded blur opacity-25 group-hover:opacity-75 transition duration-1000 group-hover:duration-200"></div>
<div className="relative px-8 py-4 bg-black border border-zinc-800 hover:border-nofx-gold/50 flex items-center gap-4 transition-all">
<Hexagon className="text-nofx-gold animate-spin-slow" size={24} />
<div className="text-left">
<div className="text-sm font-bold text-white uppercase tracking-wider group-hover:text-nofx-gold transition-colors">{t.shareYours}</div>
<div className="text-[10px] text-zinc-500 font-mono">CONTRIBUTE TO THE GLOBAL DATABASE</div>
</div>
<div className="w-[1px] h-8 bg-zinc-800 mx-2"></div>
<div className="text-xs font-mono text-zinc-400 group-hover:translate-x-1 transition-transform">
INITIALIZE_UPLOAD -&gt;
</div>
</div>
</div>
</div>
</motion.div>
)}
</motion.div>
)}
</div>
</div>
</div>
</DeepVoidBackground>
)
}

View File

@@ -37,6 +37,7 @@ import { IndicatorEditor } from '../components/strategy/IndicatorEditor'
import { RiskControlEditor } from '../components/strategy/RiskControlEditor'
import { PromptSectionsEditor } from '../components/strategy/PromptSectionsEditor'
import { PublishSettingsEditor } from '../components/strategy/PublishSettingsEditor'
import { DeepVoidBackground } from '../components/DeepVoidBackground'
const API_BASE = import.meta.env.VITE_API_BASE || ''
@@ -635,21 +636,23 @@ export function StrategyStudioPage() {
]
return (
<div className="h-[calc(100vh-64px)] flex flex-col" style={{ background: '#0B0E11' }}>
<DeepVoidBackground className="h-[calc(100vh-64px)] flex flex-col bg-nofx-bg relative overflow-hidden">
{/* Header */}
<div className="flex-shrink-0 px-4 py-3 border-b" style={{ borderColor: '#2B3139' }}>
{/* Header */}
<div className="flex-shrink-0 px-4 py-3 border-b border-nofx-gold/20 bg-nofx-bg/60 backdrop-blur-md z-10">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg" style={{ background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)' }}>
<div className="p-2 rounded-lg bg-gradient-to-br from-nofx-gold to-yellow-500">
<Sparkles className="w-5 h-5 text-black" />
</div>
<div>
<h1 className="text-lg font-bold" style={{ color: '#EAECEF' }}>{t('strategyStudio')}</h1>
<p className="text-xs" style={{ color: '#848E9C' }}>{t('subtitle')}</p>
<h1 className="text-lg font-bold text-nofx-text">{t('strategyStudio')}</h1>
<p className="text-xs text-nofx-text-muted">{t('subtitle')}</p>
</div>
</div>
{error && (
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs" style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}>
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs bg-nofx-danger/10 text-nofx-danger">
{error}
<button onClick={() => setError(null)} className="hover:underline">×</button>
</div>
@@ -660,13 +663,13 @@ export function StrategyStudioPage() {
{/* Main Content - Three Columns */}
<div className="flex-1 flex overflow-hidden">
{/* Left Column - Strategy List */}
<div className="w-48 flex-shrink-0 border-r overflow-y-auto" style={{ borderColor: '#2B3139' }}>
<div className="w-48 flex-shrink-0 border-r border-nofx-gold/20 overflow-y-auto bg-nofx-bg/30 backdrop-blur-sm z-10">
<div className="p-2">
<div className="flex items-center justify-between mb-2 px-2">
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{t('strategies')}</span>
<span className="text-xs font-medium text-nofx-text-muted">{t('strategies')}</span>
<div className="flex items-center gap-1">
{/* Import button with hidden file input */}
<label className="p-1 rounded hover:bg-white/10 transition-colors cursor-pointer" style={{ color: '#848E9C' }} title={language === 'zh' ? '导入策略' : 'Import Strategy'}>
<label className="p-1 rounded hover:bg-white/10 transition-colors cursor-pointer text-nofx-text-muted hover:text-white" title={language === 'zh' ? '导入策略' : 'Import Strategy'}>
<Upload className="w-4 h-4" />
<input
type="file"
@@ -677,8 +680,7 @@ export function StrategyStudioPage() {
</label>
<button
onClick={handleCreateStrategy}
className="p-1 rounded hover:bg-white/10 transition-colors"
style={{ color: '#F0B90B' }}
className="p-1 rounded hover:bg-white/10 transition-colors text-nofx-gold"
title={language === 'zh' ? '新建策略' : 'New Strategy'}
>
<Plus className="w-4 h-4" />
@@ -696,38 +698,36 @@ export function StrategyStudioPage() {
setPromptPreview(null)
setAiTestResult(null)
}}
className={`group px-2 py-2 rounded-lg cursor-pointer transition-all ${
selectedStrategy?.id === strategy.id ? 'ring-1 ring-yellow-500/50' : 'hover:bg-white/5'
}`}
style={{
background: selectedStrategy?.id === strategy.id ? 'rgba(240, 185, 11, 0.1)' : 'transparent',
}}
className={`group px-2 py-2 rounded-lg cursor-pointer transition-all ${selectedStrategy?.id === strategy.id
? 'ring-1 ring-nofx-gold/50 bg-nofx-gold/10 shadow-[0_0_15px_rgba(240,185,11,0.1)]'
: 'hover:bg-nofx-bg-lighter/60 hover:ring-1 hover:ring-nofx-gold/20 bg-transparent'
}`}
>
<div className="flex items-center justify-between">
<span className="text-sm truncate" style={{ color: '#EAECEF' }}>{strategy.name}</span>
<span className="text-sm truncate text-nofx-text">{strategy.name}</span>
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={(e) => { e.stopPropagation(); handleExportStrategy(strategy) }}
className="p-1 rounded hover:bg-white/10"
className="p-1 rounded hover:bg-white/10 text-nofx-text-muted hover:text-white"
title={language === 'zh' ? '导出' : 'Export'}
>
<Download className="w-3 h-3" style={{ color: '#848E9C' }} />
<Download className="w-3 h-3" />
</button>
{!strategy.is_default && (
<>
<button
onClick={(e) => { e.stopPropagation(); handleDuplicateStrategy(strategy.id) }}
className="p-1 rounded hover:bg-white/10"
className="p-1 rounded hover:bg-white/10 text-nofx-text-muted hover:text-white"
title={language === 'zh' ? '复制' : 'Duplicate'}
>
<Copy className="w-3 h-3" style={{ color: '#848E9C' }} />
<Copy className="w-3 h-3" />
</button>
<button
onClick={(e) => { e.stopPropagation(); handleDeleteStrategy(strategy.id) }}
className="p-1 rounded hover:bg-red-500/20"
className="p-1 rounded hover:bg-nofx-danger/20 text-nofx-danger"
title={language === 'zh' ? '删除' : 'Delete'}
>
<Trash2 className="w-3 h-3" style={{ color: '#F6465D' }} />
<Trash2 className="w-3 h-3" />
</button>
</>
)}
@@ -735,17 +735,17 @@ export function StrategyStudioPage() {
</div>
<div className="flex items-center gap-1 mt-1 flex-wrap">
{strategy.is_active && (
<span className="px-1.5 py-0.5 text-[10px] rounded" style={{ background: 'rgba(14, 203, 129, 0.15)', color: '#0ECB81' }}>
<span className="px-1.5 py-0.5 text-[10px] rounded bg-nofx-success/15 text-nofx-success">
{t('active')}
</span>
)}
{strategy.is_default && (
<span className="px-1.5 py-0.5 text-[10px] rounded" style={{ background: 'rgba(240, 185, 11, 0.15)', color: '#F0B90B' }}>
<span className="px-1.5 py-0.5 text-[10px] rounded bg-nofx-gold/15 text-nofx-gold">
{t('default')}
</span>
)}
{strategy.is_public && (
<span className="px-1.5 py-0.5 text-[10px] rounded flex items-center gap-0.5" style={{ background: 'rgba(96, 165, 250, 0.15)', color: '#60a5fa' }}>
<span className="px-1.5 py-0.5 text-[10px] rounded flex items-center gap-0.5 bg-blue-400/15 text-blue-400">
<Globe className="w-2.5 h-2.5" />
{language === 'zh' ? '公开' : 'Public'}
</span>
@@ -758,7 +758,7 @@ export function StrategyStudioPage() {
</div>
{/* Middle Column - Config Editor */}
<div className="flex-1 min-w-0 overflow-y-auto border-r" style={{ borderColor: '#2B3139' }}>
<div className="flex-1 min-w-0 overflow-y-auto border-r border-nofx-gold/20">
{selectedStrategy && editingConfig ? (
<div className="p-4">
{/* Strategy Name & Actions */}
@@ -772,19 +772,17 @@ export function StrategyStudioPage() {
setHasChanges(true)
}}
disabled={selectedStrategy.is_default}
className="text-lg font-bold bg-transparent border-none outline-none w-full"
style={{ color: '#EAECEF' }}
className="text-lg font-bold bg-transparent border-none outline-none w-full text-nofx-text placeholder-nofx-text-muted"
/>
{hasChanges && (
<span className="text-xs" style={{ color: '#F0B90B' }}> </span>
<span className="text-xs text-nofx-gold"> </span>
)}
</div>
<div className="flex items-center gap-2 flex-shrink-0">
{!selectedStrategy.is_active && (
<button
onClick={() => handleActivateStrategy(selectedStrategy.id)}
className="flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs transition-colors"
style={{ background: 'rgba(14, 203, 129, 0.1)', border: '1px solid rgba(14, 203, 129, 0.3)', color: '#0ECB81' }}
className="flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs transition-colors bg-nofx-success/10 border border-nofx-success/30 text-nofx-success hover:bg-nofx-success/20"
>
<Check className="w-3 h-3" />
{t('activate')}
@@ -794,11 +792,8 @@ export function StrategyStudioPage() {
<button
onClick={handleSaveStrategy}
disabled={isSaving || !hasChanges}
className="flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors disabled:opacity-50"
style={{
background: hasChanges ? '#F0B90B' : '#2B3139',
color: hasChanges ? '#0B0E11' : '#848E9C',
}}
className={`flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors disabled:opacity-50
${hasChanges ? 'bg-nofx-gold text-black hover:bg-yellow-500' : 'bg-nofx-bg-lighter text-nofx-text-muted cursor-not-allowed'}`}
>
<Save className="w-3 h-3" />
{isSaving ? t('saving') : t('save')}
@@ -812,8 +807,7 @@ export function StrategyStudioPage() {
{configSections.map(({ key, icon: Icon, color, title, content }) => (
<div
key={key}
className="rounded-lg overflow-hidden"
style={{ background: '#1E2329', border: '1px solid #2B3139' }}
className="rounded-lg overflow-hidden bg-nofx-bg-lighter border border-nofx-gold/20"
>
<button
onClick={() => toggleSection(key)}
@@ -821,12 +815,12 @@ export function StrategyStudioPage() {
>
<div className="flex items-center gap-2">
<Icon className="w-4 h-4" style={{ color }} />
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{title}</span>
<span className="text-sm font-medium text-nofx-text">{title}</span>
</div>
{expandedSections[key] ? (
<ChevronDown className="w-4 h-4" style={{ color: '#848E9C' }} />
<ChevronDown className="w-4 h-4 text-nofx-text-muted" />
) : (
<ChevronRight className="w-4 h-4" style={{ color: '#848E9C' }} />
<ChevronRight className="w-4 h-4 text-nofx-text-muted" />
)}
</button>
{expandedSections[key] && (
@@ -841,8 +835,8 @@ export function StrategyStudioPage() {
) : (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<Activity className="w-12 h-12 mx-auto mb-2 opacity-30" style={{ color: '#848E9C' }} />
<p className="text-sm" style={{ color: '#848E9C' }}>
<Activity className="w-12 h-12 mx-auto mb-2 opacity-30 text-nofx-text-muted" />
<p className="text-sm text-nofx-text-muted">
{language === 'zh' ? '选择或创建策略' : 'Select or create a strategy'}
</p>
</div>
@@ -853,29 +847,19 @@ export function StrategyStudioPage() {
{/* Right Column - Prompt Preview & AI Test */}
<div className="w-[420px] flex-shrink-0 flex flex-col overflow-hidden">
{/* Tabs */}
<div className="flex-shrink-0 flex border-b" style={{ borderColor: '#2B3139' }}>
<div className="flex-shrink-0 flex border-b border-nofx-gold/20">
<button
onClick={() => setActiveRightTab('prompt')}
className={`flex-1 flex items-center justify-center gap-2 px-4 py-2.5 text-sm font-medium transition-colors ${
activeRightTab === 'prompt' ? 'border-b-2' : 'opacity-60 hover:opacity-100'
}`}
style={{
borderColor: activeRightTab === 'prompt' ? '#a855f7' : 'transparent',
color: activeRightTab === 'prompt' ? '#a855f7' : '#848E9C',
}}
className={`flex-1 flex items-center justify-center gap-2 px-4 py-2.5 text-sm font-medium transition-colors ${activeRightTab === 'prompt' ? 'border-b-2 border-purple-500 text-purple-500' : 'opacity-60 hover:opacity-100 text-nofx-text-muted'
}`}
>
<Eye className="w-4 h-4" />
{t('promptPreview')}
</button>
<button
onClick={() => setActiveRightTab('test')}
className={`flex-1 flex items-center justify-center gap-2 px-4 py-2.5 text-sm font-medium transition-colors ${
activeRightTab === 'test' ? 'border-b-2' : 'opacity-60 hover:opacity-100'
}`}
style={{
borderColor: activeRightTab === 'test' ? '#22c55e' : 'transparent',
color: activeRightTab === 'test' ? '#22c55e' : '#848E9C',
}}
className={`flex-1 flex items-center justify-center gap-2 px-4 py-2.5 text-sm font-medium transition-colors ${activeRightTab === 'test' ? 'border-b-2 border-green-500 text-green-500' : 'opacity-60 hover:opacity-100 text-nofx-text-muted'
}`}
>
<Play className="w-4 h-4" />
{t('aiTestRun')}
@@ -892,8 +876,7 @@ export function StrategyStudioPage() {
<select
value={selectedVariant}
onChange={(e) => setSelectedVariant(e.target.value)}
className="px-2 py-1.5 rounded text-xs"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
className="px-2 py-1.5 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text outline-none focus:border-nofx-gold"
>
<option value="balanced">{t('balanced')}</option>
<option value="aggressive">{t('aggressive')}</option>
@@ -902,8 +885,7 @@ export function StrategyStudioPage() {
<button
onClick={fetchPromptPreview}
disabled={isLoadingPrompt || !editingConfig}
className="flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors disabled:opacity-50"
style={{ background: '#a855f7', color: '#fff' }}
className="flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors disabled:opacity-50 bg-purple-600 hover:bg-purple-700 text-white"
>
{isLoadingPrompt ? <Loader2 className="w-3 h-3 animate-spin" /> : <RefreshCw className="w-3 h-3" />}
{promptPreview ? t('refreshPrompt') : t('loadPrompt')}
@@ -913,16 +895,16 @@ export function StrategyStudioPage() {
{promptPreview ? (
<>
{/* Config Summary */}
<div className="p-2 rounded-lg" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
<div className="p-2 rounded-lg bg-nofx-bg border border-nofx-gold/20">
<div className="flex items-center gap-1.5 mb-2">
<Code className="w-3 h-3" style={{ color: '#a855f7' }} />
<span className="text-xs font-medium" style={{ color: '#a855f7' }}>Config</span>
<Code className="w-3 h-3 text-purple-500" />
<span className="text-xs font-medium text-purple-500">Config</span>
</div>
<div className="grid grid-cols-3 gap-2 text-xs">
{Object.entries(promptPreview.config_summary || {}).map(([key, value]) => (
<div key={key}>
<div style={{ color: '#848E9C' }}>{key.replace(/_/g, ' ')}</div>
<div style={{ color: '#EAECEF' }}>{String(value)}</div>
<div className="text-nofx-text-muted">{key.replace(/_/g, ' ')}</div>
<div className="text-nofx-text">{String(value)}</div>
</div>
))}
</div>
@@ -932,23 +914,23 @@ export function StrategyStudioPage() {
<div>
<div className="flex items-center justify-between mb-1.5">
<div className="flex items-center gap-1.5">
<FileText className="w-3 h-3" style={{ color: '#a855f7' }} />
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('systemPrompt')}</span>
<FileText className="w-3 h-3 text-purple-500" />
<span className="text-xs font-medium text-nofx-text">{t('systemPrompt')}</span>
</div>
<span className="text-[10px] px-1.5 py-0.5 rounded" style={{ background: '#2B3139', color: '#848E9C' }}>
<span className="text-[10px] px-1.5 py-0.5 rounded bg-nofx-bg-lighter text-nofx-text-muted">
{promptPreview.system_prompt.length.toLocaleString()} chars
</span>
</div>
<pre
className="p-2 rounded-lg text-[11px] font-mono overflow-auto"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF', maxHeight: '400px' }}
className="p-2 rounded-lg text-[11px] font-mono overflow-auto bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
style={{ maxHeight: '400px' }}
>
{promptPreview.system_prompt}
</pre>
</div>
</>
) : (
<div className="flex flex-col items-center justify-center py-12" style={{ color: '#848E9C' }}>
<div className="flex flex-col items-center justify-center py-12 text-nofx-text-muted">
<Eye className="w-10 h-10 mb-2 opacity-30" />
<p className="text-sm">{language === 'zh' ? '点击生成 Prompt 预览' : 'Click to generate prompt preview'}</p>
</div>
@@ -960,15 +942,14 @@ export function StrategyStudioPage() {
{/* Controls */}
<div className="space-y-2">
<div className="flex items-center gap-2">
<Bot className="w-4 h-4" style={{ color: '#22c55e' }} />
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('selectModel')}</span>
<Bot className="w-4 h-4 text-green-500" />
<span className="text-xs font-medium text-nofx-text">{t('selectModel')}</span>
</div>
{aiModels.length > 0 ? (
<select
value={selectedModelId}
onChange={(e) => setSelectedModelId(e.target.value)}
className="w-full px-3 py-2 rounded-lg text-sm"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
className="w-full px-3 py-2 rounded-lg text-sm bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
>
{aiModels.map((model) => (
<option key={model.id} value={model.id}>
@@ -977,7 +958,7 @@ export function StrategyStudioPage() {
))}
</select>
) : (
<div className="px-3 py-2 rounded-lg text-sm" style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}>
<div className="px-3 py-2 rounded-lg text-sm bg-nofx-danger/10 text-nofx-danger">
{t('noModel')}
</div>
)}
@@ -986,8 +967,7 @@ export function StrategyStudioPage() {
<select
value={selectedVariant}
onChange={(e) => setSelectedVariant(e.target.value)}
className="px-2 py-1.5 rounded text-xs"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
className="px-2 py-1.5 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
>
<option value="balanced">{t('balanced')}</option>
<option value="aggressive">{t('aggressive')}</option>
@@ -996,12 +976,7 @@ export function StrategyStudioPage() {
<button
onClick={runAiTest}
disabled={isRunningAiTest || !editingConfig || !selectedModelId}
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all disabled:opacity-50"
style={{
background: 'linear-gradient(135deg, #22c55e 0%, #4ade80 100%)',
color: '#fff',
boxShadow: '0 4px 12px rgba(34, 197, 94, 0.3)',
}}
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all disabled:opacity-50 text-white shadow-lg shadow-green-500/20 bg-gradient-to-br from-green-500 to-green-600"
>
{isRunningAiTest ? (
<>
@@ -1016,22 +991,22 @@ export function StrategyStudioPage() {
)}
</button>
</div>
<p className="text-[10px]" style={{ color: '#848E9C' }}>{t('testNote')}</p>
<p className="text-[10px] text-nofx-text-muted">{t('testNote')}</p>
</div>
{/* Test Results */}
{aiTestResult ? (
<div className="space-y-3">
{aiTestResult.error ? (
<div className="p-3 rounded-lg" style={{ background: 'rgba(246, 70, 93, 0.1)', border: '1px solid rgba(246, 70, 93, 0.3)' }}>
<p className="text-sm" style={{ color: '#F6465D' }}>{aiTestResult.error}</p>
<div className="p-3 rounded-lg bg-nofx-danger/10 border border-nofx-danger/30">
<p className="text-sm text-nofx-danger">{aiTestResult.error}</p>
</div>
) : (
<>
{aiTestResult.duration_ms && (
<div className="flex items-center gap-2">
<Clock className="w-3 h-3" style={{ color: '#848E9C' }} />
<span className="text-xs" style={{ color: '#848E9C' }}>
<Clock className="w-3 h-3 text-nofx-text-muted" />
<span className="text-xs text-nofx-text-muted">
{t('duration')}: {(aiTestResult.duration_ms / 1000).toFixed(2)}s
</span>
</div>
@@ -1041,12 +1016,12 @@ export function StrategyStudioPage() {
{aiTestResult.user_prompt && (
<div>
<div className="flex items-center gap-1.5 mb-1.5">
<Terminal className="w-3 h-3" style={{ color: '#60a5fa' }} />
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('userPrompt')} (Input)</span>
<Terminal className="w-3 h-3 text-blue-400" />
<span className="text-xs font-medium text-nofx-text">{t('userPrompt')} (Input)</span>
</div>
<pre
className="p-2 rounded-lg text-[10px] font-mono overflow-auto"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF', maxHeight: '200px' }}
className="p-2 rounded-lg text-[10px] font-mono overflow-auto bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
style={{ maxHeight: '200px' }}
>
{aiTestResult.user_prompt}
</pre>
@@ -1057,12 +1032,12 @@ export function StrategyStudioPage() {
{aiTestResult.reasoning && (
<div>
<div className="flex items-center gap-1.5 mb-1.5">
<Sparkles className="w-3 h-3" style={{ color: '#F0B90B' }} />
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('reasoning')}</span>
<Sparkles className="w-3 h-3 text-nofx-gold" />
<span className="text-xs font-medium text-nofx-text">{t('reasoning')}</span>
</div>
<pre
className="p-2 rounded-lg text-[10px] font-mono overflow-auto whitespace-pre-wrap"
style={{ background: '#0B0E11', border: '1px solid rgba(240, 185, 11, 0.3)', color: '#EAECEF', maxHeight: '200px' }}
className="p-2 rounded-lg text-[10px] font-mono overflow-auto whitespace-pre-wrap bg-nofx-bg border border-nofx-gold/30 text-nofx-text"
style={{ maxHeight: '200px' }}
>
{aiTestResult.reasoning}
</pre>
@@ -1073,12 +1048,12 @@ export function StrategyStudioPage() {
{aiTestResult.decisions && aiTestResult.decisions.length > 0 && (
<div>
<div className="flex items-center gap-1.5 mb-1.5">
<Activity className="w-3 h-3" style={{ color: '#22c55e' }} />
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('decisions')}</span>
<Activity className="w-3 h-3 text-green-500" />
<span className="text-xs font-medium text-nofx-text">{t('decisions')}</span>
</div>
<pre
className="p-2 rounded-lg text-[10px] font-mono overflow-auto"
style={{ background: '#0B0E11', border: '1px solid rgba(34, 197, 94, 0.3)', color: '#EAECEF', maxHeight: '200px' }}
className="p-2 rounded-lg text-[10px] font-mono overflow-auto bg-nofx-bg border border-green-500/30 text-nofx-text"
style={{ maxHeight: '200px' }}
>
{JSON.stringify(aiTestResult.decisions, null, 2)}
</pre>
@@ -1089,12 +1064,12 @@ export function StrategyStudioPage() {
{aiTestResult.ai_response && (
<div>
<div className="flex items-center gap-1.5 mb-1.5">
<FileText className="w-3 h-3" style={{ color: '#848E9C' }} />
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('aiOutput')} (Raw)</span>
<FileText className="w-3 h-3 text-nofx-text-muted" />
<span className="text-xs font-medium text-nofx-text">{t('aiOutput')} (Raw)</span>
</div>
<pre
className="p-2 rounded-lg text-[10px] font-mono overflow-auto whitespace-pre-wrap"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF', maxHeight: '300px' }}
className="p-2 rounded-lg text-[10px] font-mono overflow-auto whitespace-pre-wrap bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
style={{ maxHeight: '300px' }}
>
{aiTestResult.ai_response}
</pre>
@@ -1104,7 +1079,7 @@ export function StrategyStudioPage() {
)}
</div>
) : (
<div className="flex flex-col items-center justify-center py-12" style={{ color: '#848E9C' }}>
<div className="flex flex-col items-center justify-center py-12 text-nofx-text-muted">
<Play className="w-10 h-10 mb-2 opacity-30" />
<p className="text-sm">{language === 'zh' ? '点击运行 AI 测试' : 'Click to run AI test'}</p>
</div>
@@ -1114,7 +1089,7 @@ export function StrategyStudioPage() {
</div>
</div>
</div>
</div>
</DeepVoidBackground>
)
}

View File

@@ -0,0 +1,857 @@
import { useEffect, useState, useRef } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { mutate } from 'swr'
import { api } from '../lib/api'
import { ChartTabs } from '../components/ChartTabs'
import { DecisionCard } from '../components/DecisionCard'
import { PositionHistory } from '../components/PositionHistory'
import { PunkAvatar, getTraderAvatar } from '../components/PunkAvatar'
import { confirmToast, notify } from '../lib/notify'
import { t, type Language } from '../i18n/translations'
import { LogOut, Loader2, Eye, EyeOff, Copy, Check } from 'lucide-react'
import { DeepVoidBackground } from '../components/DeepVoidBackground'
import type {
SystemStatus,
AccountInfo,
Position,
DecisionRecord,
Statistics,
TraderInfo,
Exchange,
} from '../types'
// --- Helper Functions ---
// 获取友好的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 kline 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?.toLowerCase() || 'binance'
}
// Helper function to check if exchange is a perp-dex type (wallet-based)
function isPerpDexExchange(exchangeType: string | undefined): boolean {
if (!exchangeType) return false
const perpDexTypes = ['hyperliquid', 'lighter', 'aster']
return perpDexTypes.includes(exchangeType.toLowerCase())
}
// Helper function to get wallet address for perp-dex exchanges
function getWalletAddress(exchange: Exchange | undefined): string | undefined {
if (!exchange) return undefined
const type = exchange.exchange_type?.toLowerCase()
switch (type) {
case 'hyperliquid':
return exchange.hyperliquidWalletAddr
case 'lighter':
return exchange.lighterWalletAddr
case 'aster':
return exchange.asterSigner
default:
return undefined
}
}
// Helper function to truncate wallet address for display
function truncateAddress(address: string, startLen = 6, endLen = 4): string {
if (address.length <= startLen + endLen + 3) return address
return `${address.slice(0, startLen)}...${address.slice(-endLen)}`
}
// --- Components ---
interface TraderDashboardPageProps {
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[]
}
export function TraderDashboardPage({
selectedTrader,
status,
account,
positions,
decisions,
decisionsLimit,
onDecisionsLimitChange,
lastUpdate,
language,
traders,
tradersError,
selectedTraderId,
onTraderSelect,
onNavigateToTraders,
exchanges,
}: TraderDashboardPageProps) {
const [closingPosition, setClosingPosition] = useState<string | null>(null)
const [selectedChartSymbol, setSelectedChartSymbol] = useState<string | undefined>(undefined)
const [chartUpdateKey, setChartUpdateKey] = useState<number>(0)
const chartSectionRef = useRef<HTMLDivElement>(null)
const [showWalletAddress, setShowWalletAddress] = useState<boolean>(false)
const [copiedAddress, setCopiedAddress] = useState<boolean>(false)
// Current positions pagination
const [positionsPageSize, setPositionsPageSize] = useState<number>(20)
const [positionsCurrentPage, setPositionsCurrentPage] = useState<number>(1)
// Calculate paginated positions
const totalPositions = positions?.length || 0
const totalPositionPages = Math.ceil(totalPositions / positionsPageSize)
const paginatedPositions = positions?.slice(
(positionsCurrentPage - 1) * positionsPageSize,
positionsCurrentPage * positionsPageSize
) || []
// Reset page when positions change
useEffect(() => {
setPositionsCurrentPage(1)
}, [selectedTraderId, positionsPageSize])
// Get current exchange info for perp-dex wallet display
const currentExchange = exchanges?.find(
(e) => e.id === selectedTrader?.exchange_id
)
const walletAddress = getWalletAddress(currentExchange)
const isPerpDex = isPerpDexExchange(currentExchange?.exchange_type)
// Copy wallet address to clipboard
const handleCopyAddress = async () => {
if (!walletAddress) return
try {
await navigator.clipboard.writeText(walletAddress)
setCopiedAddress(true)
setTimeout(() => setCopiedAddress(false), 2000)
} catch (err) {
console.error('Failed to copy address:', err)
}
}
// Handle symbol click from Decision Card
const handleSymbolClick = (symbol: string) => {
// Set the selected symbol
setSelectedChartSymbol(symbol)
// Scroll to chart section
setTimeout(() => {
chartSectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}, 100)
}
// 平仓操作
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 (
<div className="flex items-center justify-center min-h-[60vh] relative z-10">
<div className="text-center max-w-md mx-auto px-6">
<div
className="w-24 h-24 mx-auto mb-6 rounded-full flex items-center justify-center nofx-glass"
style={{
background: 'rgba(240, 185, 11, 0.1)',
borderColor: 'rgba(240, 185, 11, 0.3)',
}}
>
<svg
className="w-12 h-12 text-nofx-gold"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
</div>
<h2 className="text-2xl font-bold mb-3 text-nofx-text-main">
{language === 'zh' ? '无法连接到服务器' : 'Connection Failed'}
</h2>
<p className="text-base mb-6 text-nofx-text-muted">
{language === 'zh'
? '请确认后端服务已启动。'
: 'Please check if the backend service is running.'}
</p>
<button
onClick={() => window.location.reload()}
className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105 active:scale-95 nofx-glass border border-nofx-gold/30 text-nofx-gold hover:bg-nofx-gold/10"
>
{language === 'zh' ? '重试' : 'Retry'}
</button>
</div>
</div>
)
}
// If traders is loaded and empty, show empty state
if (traders && traders.length === 0) {
return (
<div className="flex items-center justify-center min-h-[60vh] relative z-10">
<div className="text-center max-w-md mx-auto px-6">
<div
className="w-24 h-24 mx-auto mb-6 rounded-full flex items-center justify-center nofx-glass"
style={{
background: 'rgba(240, 185, 11, 0.1)',
borderColor: 'rgba(240, 185, 11, 0.3)',
}}
>
<svg
className="w-12 h-12 text-nofx-gold"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
/>
</svg>
</div>
<h2 className="text-2xl font-bold mb-3 text-nofx-text-main">
{t('dashboardEmptyTitle', language)}
</h2>
<p className="text-base mb-6 text-nofx-text-muted">
{t('dashboardEmptyDescription', language)}
</p>
<button
onClick={onNavigateToTraders}
className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105 active:scale-95 nofx-glass border border-nofx-gold/30 text-nofx-gold hover:bg-nofx-gold/10"
>
{t('goToTradersPage', language)}
</button>
</div>
</div>
)
}
// If traders is still loading or selectedTrader is not ready, show skeleton
if (!selectedTrader) {
return (
<div className="space-y-6 relative z-10">
<div className="nofx-glass p-6 animate-pulse">
<div className="h-8 w-48 mb-3 bg-nofx-bg/50 rounded"></div>
<div className="flex gap-4">
<div className="h-4 w-32 bg-nofx-bg/50 rounded"></div>
<div className="h-4 w-24 bg-nofx-bg/50 rounded"></div>
<div className="h-4 w-28 bg-nofx-bg/50 rounded"></div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="nofx-glass p-5 animate-pulse">
<div className="h-4 w-24 mb-3 bg-nofx-bg/50 rounded"></div>
<div className="h-8 w-32 bg-nofx-bg/50 rounded"></div>
</div>
))}
</div>
<div className="nofx-glass p-6 animate-pulse">
<div className="h-6 w-40 mb-4 bg-nofx-bg/50 rounded"></div>
<div className="h-64 w-full bg-nofx-bg/50 rounded"></div>
</div>
</div>
)
}
return (
<DeepVoidBackground className="min-h-screen pb-12" disableAnimation>
<div className="w-full px-4 md:px-8 relative z-10 pt-6">
{/* Trader Header */}
<div
className="mb-6 rounded-lg p-6 animate-scale-in nofx-glass group"
style={{
background: 'linear-gradient(135deg, rgba(15, 23, 42, 0.6) 0%, rgba(15, 23, 42, 0.4) 100%)',
}}
>
<div className="flex items-start justify-between mb-4">
<h2 className="text-2xl font-bold flex items-center gap-4 text-nofx-text-main">
<div className="relative">
<PunkAvatar
seed={getTraderAvatar(
selectedTrader.trader_id,
selectedTrader.trader_name
)}
size={56}
className="rounded-xl border-2 border-nofx-gold/30 shadow-[0_0_15px_rgba(240,185,11,0.2)]"
/>
<div className="absolute -bottom-1 -right-1 w-4 h-4 bg-nofx-green rounded-full border-2 border-[#0B0E11] shadow-[0_0_8px_rgba(14,203,129,0.8)] animate-pulse" />
</div>
<div className="flex flex-col">
<span className="text-3xl tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-nofx-text-main to-nofx-text-muted">
{selectedTrader.trader_name}
</span>
<span className="text-xs font-mono text-nofx-text-muted opacity-60 flex items-center gap-2">
<div className="w-1.5 h-1.5 bg-nofx-gold rounded-full" />
ID: {selectedTrader.trader_id.slice(0, 8)}...
</span>
</div>
</h2>
<div className="flex items-center gap-4">
{/* Trader Selector */}
{traders && traders.length > 0 && (
<div className="flex items-center gap-2 nofx-glass px-1 py-1 rounded-lg border border-white/5">
<select
value={selectedTraderId}
onChange={(e) => onTraderSelect(e.target.value)}
className="bg-transparent text-sm font-medium cursor-pointer transition-colors text-nofx-text-main focus:outline-none px-2 py-1"
>
{traders.map((trader) => (
<option key={trader.trader_id} value={trader.trader_id} className="bg-[#0B0E11]">
{trader.trader_name}
</option>
))}
</select>
</div>
)}
{/* Wallet Address Display for Perp-DEX */}
{exchanges && isPerpDex && (
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg nofx-glass border border-nofx-gold/20">
{walletAddress ? (
<>
<span className="text-xs font-mono text-nofx-gold">
{showWalletAddress
? walletAddress
: truncateAddress(walletAddress)}
</span>
<button
type="button"
onClick={() => setShowWalletAddress(!showWalletAddress)}
className="p-1 rounded hover:bg-white/10 transition-colors"
title={
showWalletAddress
? language === 'zh'
? '隐藏地址'
: 'Hide address'
: language === 'zh'
? '显示完整地址'
: 'Show full address'
}
>
{showWalletAddress ? (
<EyeOff className="w-3.5 h-3.5 text-nofx-text-muted" />
) : (
<Eye className="w-3.5 h-3.5 text-nofx-text-muted" />
)}
</button>
<button
type="button"
onClick={handleCopyAddress}
className="p-1 rounded hover:bg-white/10 transition-colors"
title={language === 'zh' ? '复制地址' : 'Copy address'}
>
{copiedAddress ? (
<Check className="w-3.5 h-3.5 text-nofx-green" />
) : (
<Copy className="w-3.5 h-3.5 text-nofx-text-muted" />
)}
</button>
</>
) : (
<span className="text-xs text-nofx-text-muted">
{language === 'zh' ? '未配置地址' : 'No address configured'}
</span>
)}
</div>
)}
</div>
</div>
<div className="flex items-center gap-6 text-sm flex-wrap text-nofx-text-muted font-mono pl-2">
<span className="flex items-center gap-2">
<span className="opacity-60">AI Model:</span>
<span
className="font-bold px-2 py-0.5 rounded text-xs tracking-wide"
style={{
background: selectedTrader.ai_model.includes('qwen') ? 'rgba(192, 132, 252, 0.15)' : 'rgba(96, 165, 250, 0.15)',
color: selectedTrader.ai_model.includes('qwen') ? '#c084fc' : '#60a5fa',
border: `1px solid ${selectedTrader.ai_model.includes('qwen') ? '#c084fc' : '#60a5fa'}40`
}}
>
{getModelDisplayName(
selectedTrader.ai_model.split('_').pop() ||
selectedTrader.ai_model
)}
</span>
</span>
<span className="w-px h-3 bg-white/10" />
<span className="flex items-center gap-2">
<span className="opacity-60">Exchange:</span>
<span className="text-nofx-text-main font-semibold">
{getExchangeDisplayNameFromList(
selectedTrader.exchange_id,
exchanges
)}
</span>
</span>
<span className="w-px h-3 bg-white/10" />
<span className="flex items-center gap-2">
<span className="opacity-60">Strategy:</span>
<span className="text-nofx-gold font-semibold tracking-wide">
{selectedTrader.strategy_name || 'No Strategy'}
</span>
</span>
{status && (
<>
<span className="w-px h-3 bg-white/10" />
<span>Cycles: <span className="text-nofx-text-main">{status.call_count}</span></span>
<span className="w-px h-3 bg-white/10" />
<span>Runtime: <span className="text-nofx-text-main">{status.runtime_minutes} min</span></span>
</>
)}
</div>
</div>
{/* Debug Info */}
{account && (
<div className="mb-4 px-3 py-1.5 rounded bg-black/40 border border-white/5 text-[10px] font-mono text-nofx-text-muted flex justify-between items-center opacity-60 hover:opacity-100 transition-opacity">
<span>SYSTEM_STATUS::ONLINE</span>
<div className="flex gap-4">
<span>LAST_UPDATE::{lastUpdate}</span>
<span>EQ::{account?.total_equity?.toFixed(2)}</span>
<span>PNL::{account?.total_pnl?.toFixed(2)}</span>
</div>
</div>
)}
{/* Account Overview */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
<StatCard
title={t('totalEquity', language)}
value={`${account?.total_equity?.toFixed(2) || '0.00'}`}
unit="USDT"
change={account?.total_pnl_pct || 0}
positive={(account?.total_pnl ?? 0) > 0}
icon="💰"
/>
<StatCard
title={t('availableBalance', language)}
value={`${account?.available_balance?.toFixed(2) || '0.00'}`}
unit="USDT"
subtitle={`${account?.available_balance && account?.total_equity ? ((account.available_balance / account.total_equity) * 100).toFixed(1) : '0.0'}% ${t('free', language)}`}
icon="💳"
/>
<StatCard
title={t('totalPnL', language)}
value={`${account?.total_pnl !== undefined && account.total_pnl >= 0 ? '+' : ''}${account?.total_pnl?.toFixed(2) || '0.00'}`}
unit="USDT"
change={account?.total_pnl_pct || 0}
positive={(account?.total_pnl ?? 0) >= 0}
icon="📈"
/>
<StatCard
title={t('positions', language)}
value={`${account?.position_count || 0}`}
unit="ACTIVE"
subtitle={`${t('margin', language)}: ${account?.margin_used_pct?.toFixed(1) || '0.0'}%`}
icon="📊"
/>
</div>
{/* Main Content Area */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
{/* Left Column: Charts + Positions */}
<div className="space-y-6">
{/* Chart Tabs (Equity / K-line) */}
<div
ref={chartSectionRef}
className="chart-container animate-slide-in scroll-mt-32 backdrop-blur-sm"
style={{ animationDelay: '0.1s' }}
>
<ChartTabs
traderId={selectedTrader.trader_id}
selectedSymbol={selectedChartSymbol}
updateKey={chartUpdateKey}
exchangeId={getExchangeTypeFromList(
selectedTrader.exchange_id,
exchanges
)}
/>
</div>
{/* Current Positions */}
<div
className="nofx-glass p-6 animate-slide-in relative overflow-hidden group"
style={{ animationDelay: '0.15s' }}
>
<div className="absolute top-0 right-0 p-3 opacity-10 group-hover:opacity-20 transition-opacity">
<div className="w-24 h-24 rounded-full bg-blue-500 blur-3xl" />
</div>
<div className="flex items-center justify-between mb-5 relative z-10">
<h2 className="text-lg font-bold flex items-center gap-2 text-nofx-text-main uppercase tracking-wide">
<span className="text-blue-500"></span> {t('currentPositions', language)}
</h2>
{positions && positions.length > 0 && (
<div className="text-xs px-2 py-1 rounded bg-nofx-gold/10 text-nofx-gold border border-nofx-gold/20 font-mono shadow-[0_0_10px_rgba(240,185,11,0.1)]">
{positions.length} {t('active', language)}
</div>
)}
</div>
{positions && positions.length > 0 ? (
<div>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead className="text-left border-b border-white/5">
<tr>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-left">{t('symbol', language)}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center">{t('side', language)}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center">{language === 'zh' ? '操作' : 'Action'}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right" title={t('entryPrice', language)}>{language === 'zh' ? '入场价' : 'Entry'}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right" title={t('markPrice', language)}>{language === 'zh' ? '标记价' : 'Mark'}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right" title={t('quantity', language)}>{language === 'zh' ? '数量' : 'Qty'}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right" title={t('positionValue', language)}>{language === 'zh' ? '价值' : 'Value'}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center" title={t('leverage', language)}>{language === 'zh' ? '杠杆' : 'Lev.'}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right" title={t('unrealizedPnL', language)}>{language === 'zh' ? '未实现盈亏' : 'uPnL'}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right" title={t('liqPrice', language)}>{language === 'zh' ? '强平价' : 'Liq.'}</th>
</tr>
</thead>
<tbody>
{paginatedPositions.map((pos, i) => (
<tr
key={i}
className="border-b border-white/5 last:border-0 transition-all hover:bg-white/5 cursor-pointer group/row"
onClick={() => {
setSelectedChartSymbol(pos.symbol)
setChartUpdateKey(Date.now())
if (chartSectionRef.current) {
chartSectionRef.current.scrollIntoView({
behavior: 'smooth',
block: 'start',
})
}
}}
>
<td className="px-1 py-3 font-mono font-semibold whitespace-nowrap text-left text-nofx-text-main group-hover/row:text-white transition-colors">
{pos.symbol}
</td>
<td className="px-1 py-3 whitespace-nowrap text-center">
<span
className={`px-1.5 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider ${pos.side === 'long' ? 'bg-nofx-green/10 text-nofx-green shadow-[0_0_8px_rgba(14,203,129,0.2)]' : 'bg-nofx-red/10 text-nofx-red shadow-[0_0_8px_rgba(246,70,93,0.2)]'}`}
>
{t(pos.side === 'long' ? 'long' : 'short', language)}
</span>
</td>
<td className="px-1 py-3 whitespace-nowrap text-center">
<button
type="button"
onClick={(e) => {
e.stopPropagation()
handleClosePosition(pos.symbol, pos.side.toUpperCase())
}}
disabled={closingPosition === pos.symbol}
className="inline-flex items-center gap-1 px-2 py-1 rounded text-[10px] font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed mx-auto bg-nofx-red/10 text-nofx-red border border-nofx-red/30 hover:bg-nofx-red/20"
title={language === 'zh' ? '平仓' : 'Close Position'}
>
{closingPosition === pos.symbol ? (
<Loader2 className="w-3 h-3 animate-spin" />
) : (
<LogOut className="w-3 h-3" />
)}
{language === 'zh' ? '平仓' : 'Close'}
</button>
</td>
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-main">{pos.entry_price.toFixed(4)}</td>
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-main">{pos.mark_price.toFixed(4)}</td>
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-main">{pos.quantity.toFixed(4)}</td>
<td className="px-1 py-3 font-mono font-bold whitespace-nowrap text-right text-nofx-text-main">{(pos.quantity * pos.mark_price).toFixed(2)}</td>
<td className="px-1 py-3 font-mono whitespace-nowrap text-center text-nofx-gold">{pos.leverage}x</td>
<td className="px-1 py-3 font-mono whitespace-nowrap text-right">
<span
className={`font-bold ${pos.unrealized_pnl >= 0 ? 'text-nofx-green shadow-nofx-green' : 'text-nofx-red shadow-nofx-red'}`}
style={{ textShadow: pos.unrealized_pnl >= 0 ? '0 0 10px rgba(14,203,129,0.3)' : '0 0 10px rgba(246,70,93,0.3)' }}
>
{pos.unrealized_pnl >= 0 ? '+' : ''}
{pos.unrealized_pnl.toFixed(2)}
</span>
</td>
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-muted">{pos.liquidation_price.toFixed(4)}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Pagination footer */}
{totalPositions > 10 && (
<div className="flex flex-wrap items-center justify-between gap-3 pt-4 mt-4 text-xs border-t border-white/5 text-nofx-text-muted">
<span>
{language === 'zh'
? `显示 ${paginatedPositions.length} / ${totalPositions} 个持仓`
: `Showing ${paginatedPositions.length} of ${totalPositions} positions`}
</span>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<span>{language === 'zh' ? '每页' : 'Per page'}:</span>
<select
value={positionsPageSize}
onChange={(e) => setPositionsPageSize(Number(e.target.value))}
className="bg-black/40 border border-white/10 rounded px-2 py-1 text-xs text-nofx-text-main focus:outline-none focus:border-nofx-gold/50 transition-colors"
>
<option value={20}>20</option>
<option value={50}>50</option>
<option value={100}>100</option>
</select>
</div>
{totalPositionPages > 1 && (
<div className="flex items-center gap-1">
{['«', '', `${positionsCurrentPage} / ${totalPositionPages}`, '', '»'].map((label, idx) => {
const isText = idx === 2;
const isFirst = idx === 0;
const isPrev = idx === 1;
const isNext = idx === 3;
const isLast = idx === 4;
if (isText) return <span key={idx} className="px-3 text-nofx-text-main">{label}</span>;
let onClick = () => { };
let disabled = false;
if (isFirst) { onClick = () => setPositionsCurrentPage(1); disabled = positionsCurrentPage === 1; }
if (isPrev) { onClick = () => setPositionsCurrentPage(p => Math.max(1, p - 1)); disabled = positionsCurrentPage === 1; }
if (isNext) { onClick = () => setPositionsCurrentPage(p => Math.min(totalPositionPages, p + 1)); disabled = positionsCurrentPage === totalPositionPages; }
if (isLast) { onClick = () => setPositionsCurrentPage(totalPositionPages); disabled = positionsCurrentPage === totalPositionPages; }
return (
<button
key={idx}
onClick={onClick}
disabled={disabled}
className={`px-2 py-1 rounded transition-colors ${disabled ? 'opacity-30 cursor-not-allowed' : 'hover:bg-white/10 text-nofx-text-main bg-white/5'}`}
>
{label}
</button>
)
})}
</div>
)}
</div>
</div>
)}
</div>
) : (
<div className="text-center py-16 text-nofx-text-muted opacity-60">
<div className="text-6xl mb-4 opacity-50 grayscale">📊</div>
<div className="text-lg font-semibold mb-2">{t('noPositions', language)}</div>
<div className="text-sm">{t('noActivePositions', language)}</div>
</div>
)}
</div>
</div>
{/* Right Column: Recent Decisions */}
<div
className="nofx-glass p-6 animate-slide-in h-fit lg:sticky lg:top-24 lg:max-h-[calc(100vh-120px)] flex flex-col"
style={{ animationDelay: '0.2s' }}
>
{/* Header */}
<div className="flex items-center gap-3 mb-5 pb-4 border-b border-white/5 shrink-0">
<div
className="w-10 h-10 rounded-xl flex items-center justify-center text-xl shadow-[0_4px_14px_rgba(99,102,241,0.4)]"
style={{
background: 'linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%)',
}}
>
🧠
</div>
<div className="flex-1">
<h2 className="text-xl font-bold text-nofx-text-main">
{t('recentDecisions', language)}
</h2>
{decisions && decisions.length > 0 && (
<div className="text-xs text-nofx-text-muted">
{t('lastCycles', language, { count: decisions.length })}
</div>
)}
</div>
{/* Limit Selector */}
<select
value={decisionsLimit}
onChange={(e) => onDecisionsLimitChange(Number(e.target.value))}
className="px-3 py-1.5 rounded-lg text-sm font-medium cursor-pointer transition-all bg-black/40 text-nofx-text-main border border-white/10 hover:border-nofx-accent focus:outline-none"
>
<option value={5}>5</option>
<option value={10}>10</option>
<option value={20}>20</option>
<option value={50}>50</option>
<option value={100}>100</option>
</select>
</div>
{/* Decisions List - Scrollable */}
<div
className="space-y-4 overflow-y-auto pr-2 custom-scrollbar"
style={{ maxHeight: 'calc(100vh - 280px)' }}
>
{decisions && decisions.length > 0 ? (
decisions.map((decision, i) => (
<DecisionCard key={i} decision={decision} language={language} onSymbolClick={handleSymbolClick} />
))
) : (
<div className="py-16 text-center text-nofx-text-muted opacity-60">
<div className="text-6xl mb-4 opacity-30 grayscale">🧠</div>
<div className="text-lg font-semibold mb-2 text-nofx-text-main">
{t('noDecisionsYet', language)}
</div>
<div className="text-sm">
{t('aiDecisionsWillAppear', language)}
</div>
</div>
)}
</div>
</div>
</div>
{/* Position History Section */}
{selectedTraderId && (
<div
className="nofx-glass p-6 animate-slide-in"
style={{ animationDelay: '0.25s' }}
>
<div className="flex items-center justify-between mb-5">
<h2 className="text-xl font-bold flex items-center gap-2 text-nofx-text-main">
<span className="text-2xl">📜</span>
{t('positionHistory.title', language)}
</h2>
</div>
<PositionHistory traderId={selectedTraderId} />
</div>
)}
</div>
</DeepVoidBackground>
)
}
// Stat Card Component - Deep Void Style
function StatCard({
title,
value,
unit,
change,
positive,
subtitle,
icon,
}: {
title: string
value: string
unit?: string
change?: number
positive?: boolean
subtitle?: string
icon?: string
}) {
return (
<div className="group nofx-glass p-5 rounded-lg transition-all duration-300 hover:bg-white/5 hover:translate-y-[-2px] border border-white/5 hover:border-nofx-gold/20 relative overflow-hidden">
<div className="absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity text-4xl grayscale group-hover:grayscale-0">
{icon}
</div>
<div className="text-xs mb-2 font-mono uppercase tracking-wider text-nofx-text-muted flex items-center gap-2">
{title}
</div>
<div className="flex items-baseline gap-1 mb-1">
<div className="text-2xl font-bold font-mono text-nofx-text-main tracking-tight group-hover:text-white transition-colors">
{value}
</div>
{unit && <span className="text-xs font-mono text-nofx-text-muted opacity-60">{unit}</span>}
</div>
{change !== undefined && (
<div className="flex items-center gap-1">
<div
className={`text-sm mono font-bold flex items-center gap-1 ${positive ? 'text-nofx-green' : 'text-nofx-red'}`}
>
<span>{positive ? '▲' : '▼'}</span>
<span>{positive ? '+' : ''}{change.toFixed(2)}%</span>
</div>
</div>
)}
{subtitle && (
<div className="text-xs mt-2 mono text-nofx-text-muted opacity-80">
{subtitle}
</div>
)}
</div>
)
}