mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-13 07:46:54 +08:00
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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
43
web/src/pages/PageNotFound.tsx
Normal file
43
web/src/pages/PageNotFound.tsx
Normal 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">-></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</DeepVoidBackground>
|
||||
)
|
||||
}
|
||||
@@ -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 ->
|
||||
{/* 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 ->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DeepVoidBackground>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
857
web/src/pages/TraderDashboardPage.tsx
Normal file
857
web/src/pages/TraderDashboardPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user