mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-06-06 05:51:19 +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:
1021
web/src/App.tsx
1021
web/src/App.tsx
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@ import { useAuth } from '../contexts/AuthContext'
|
||||
import { getExchangeIcon } from './ExchangeIcons'
|
||||
import { getModelIcon } from './ModelIcons'
|
||||
import { TraderConfigModal } from './TraderConfigModal'
|
||||
import { DeepVoidBackground } from './DeepVoidBackground'
|
||||
import { ExchangeConfigModal } from './traders/ExchangeConfigModal'
|
||||
import { PunkAvatar, getTraderAvatar } from './PunkAvatar'
|
||||
import {
|
||||
@@ -802,333 +803,225 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 md:space-y-6 animate-fade-in">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-3 md:gap-0">
|
||||
<div className="flex items-center gap-3 md:gap-4">
|
||||
<div
|
||||
className="w-10 h-10 md:w-12 md:h-12 rounded-xl flex items-center justify-center"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
|
||||
boxShadow: '0 4px 14px rgba(240, 185, 11, 0.4)',
|
||||
}}
|
||||
>
|
||||
<Bot className="w-5 h-5 md:w-6 md:h-6" style={{ color: '#000' }} />
|
||||
<DeepVoidBackground className="py-8" disableAnimation>
|
||||
<div className="w-full px-4 md:px-8 space-y-8 animate-fade-in">
|
||||
{/* Header - Terminal Style */}
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 border-b border-white/10 pb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative group">
|
||||
<div className="absolute -inset-1 bg-nofx-gold/20 rounded-xl blur opacity-0 group-hover:opacity-100 transition duration-500"></div>
|
||||
<div className="w-12 h-12 md:w-14 md:h-14 rounded-xl flex items-center justify-center bg-black border border-nofx-gold/30 text-nofx-gold relative z-10 shadow-[0_0_15px_rgba(240,185,11,0.1)]">
|
||||
<Bot className="w-6 h-6 md:w-7 md:h-7" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h1
|
||||
className="text-xl md:text-2xl font-bold flex items-center gap-2"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
<h1 className="text-2xl md:text-3xl font-bold font-mono tracking-tight text-white flex items-center gap-3 uppercase">
|
||||
{t('aiTraders', language)}
|
||||
<span
|
||||
className="text-xs font-normal px-2 py-1 rounded"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.15)',
|
||||
color: '#F0B90B',
|
||||
}}
|
||||
>
|
||||
{traders?.length || 0} {t('active', language)}
|
||||
<span className="text-xs font-mono font-normal px-2 py-0.5 rounded bg-nofx-gold/10 text-nofx-gold border border-nofx-gold/20 tracking-wider">
|
||||
{traders?.length || 0} ACTIVE_NODES
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{t('manageAITraders', language)}
|
||||
<p className="text-xs font-mono text-zinc-500 uppercase tracking-widest mt-1 ml-1 flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-green-500 animate-pulse"></span>
|
||||
SYSTEM_READY
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 md:gap-3 w-full md:w-auto overflow-hidden flex-wrap md:flex-nowrap">
|
||||
<div className="flex gap-2 w-full md:w-auto overflow-x-auto pb-1 md:pb-0 hide-scrollbar">
|
||||
<button
|
||||
onClick={handleAddModel}
|
||||
className="px-3 md:px-4 py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105 flex items-center gap-1 md:gap-2 whitespace-nowrap"
|
||||
style={{
|
||||
background: '#2B3139',
|
||||
color: '#EAECEF',
|
||||
border: '1px solid #474D57',
|
||||
}}
|
||||
className="px-4 py-2 rounded text-xs font-mono uppercase tracking-wider transition-all border border-zinc-700 bg-black/20 text-zinc-400 hover:text-white hover:border-zinc-500 whitespace-nowrap backdrop-blur-sm"
|
||||
>
|
||||
<Plus className="w-3 h-3 md:w-4 md:h-4" />
|
||||
{t('aiModels', language)}
|
||||
<div className="flex items-center gap-2">
|
||||
<Plus className="w-3 h-3" />
|
||||
<span>MODELS_CONFIG</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleAddExchange}
|
||||
className="px-3 md:px-4 py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105 flex items-center gap-1 md:gap-2 whitespace-nowrap"
|
||||
style={{
|
||||
background: '#2B3139',
|
||||
color: '#EAECEF',
|
||||
border: '1px solid #474D57',
|
||||
}}
|
||||
className="px-4 py-2 rounded text-xs font-mono uppercase tracking-wider transition-all border border-zinc-700 bg-black/20 text-zinc-400 hover:text-white hover:border-zinc-500 whitespace-nowrap backdrop-blur-sm"
|
||||
>
|
||||
<Plus className="w-3 h-3 md:w-4 md:h-4" />
|
||||
{t('exchanges', language)}
|
||||
<div className="flex items-center gap-2">
|
||||
<Plus className="w-3 h-3" />
|
||||
<span>EXCHANGE_KEYS</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
disabled={
|
||||
configuredModels.length === 0 || configuredExchanges.length === 0
|
||||
}
|
||||
className="px-3 md:px-4 py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1 md:gap-2 whitespace-nowrap"
|
||||
style={{
|
||||
background:
|
||||
configuredModels.length > 0 && configuredExchanges.length > 0
|
||||
? '#F0B90B'
|
||||
: '#2B3139',
|
||||
color:
|
||||
configuredModels.length > 0 && configuredExchanges.length > 0
|
||||
? '#000'
|
||||
: '#848E9C',
|
||||
}}
|
||||
disabled={configuredModels.length === 0 || configuredExchanges.length === 0}
|
||||
className="group relative px-6 py-2 rounded text-xs font-bold font-mono uppercase tracking-wider transition-all disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap overflow-hidden bg-nofx-gold text-black hover:bg-yellow-400 shadow-[0_0_20px_rgba(240,185,11,0.2)] hover:shadow-[0_0_30px_rgba(240,185,11,0.4)]"
|
||||
>
|
||||
<span className="relative z-10 flex items-center gap-2">
|
||||
<Plus className="w-4 h-4" />
|
||||
{t('createTrader', language)}
|
||||
</span>
|
||||
<div className="absolute inset-0 bg-white/20 translate-y-full group-hover:translate-y-0 transition-transform duration-300"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Configuration Status */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6">
|
||||
{/* AI Models */}
|
||||
<div className="binance-card p-3 md:p-4">
|
||||
<h3
|
||||
className="text-base md:text-lg font-semibold mb-3 flex items-center gap-2"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
<Brain
|
||||
className="w-4 h-4 md:w-5 md:h-5"
|
||||
style={{ color: '#60a5fa' }}
|
||||
/>
|
||||
{/* Configuration Status Grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* AI Models Card */}
|
||||
<div className="nofx-glass rounded-lg border border-white/5 overflow-hidden">
|
||||
<div className="px-4 py-3 border-b border-white/5 bg-black/20 flex items-center gap-2 backdrop-blur-sm">
|
||||
<Brain className="w-4 h-4 text-nofx-gold" />
|
||||
<h3 className="text-sm font-mono tracking-widest text-zinc-300 uppercase">
|
||||
{t('aiModels', language)}
|
||||
</h3>
|
||||
<div className="space-y-2 md:space-y-3">
|
||||
</div>
|
||||
|
||||
<div className="p-4 space-y-3">
|
||||
{configuredModels.map((model) => {
|
||||
const inUse = isModelInUse(model.id)
|
||||
const usageInfo = getModelUsageInfo(model.id)
|
||||
return (
|
||||
<div
|
||||
key={model.id}
|
||||
className={`flex items-center justify-between p-2 md:p-3 rounded transition-all ${
|
||||
inUse
|
||||
? 'cursor-not-allowed'
|
||||
: 'cursor-pointer hover:bg-gray-700'
|
||||
}`}
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||
className={`group relative flex items-center justify-between p-3 rounded-md transition-all border border-transparent ${inUse ? 'opacity-80' : 'hover:bg-white/5 hover:border-white/10 cursor-pointer'
|
||||
} bg-black/20`}
|
||||
onClick={() => handleModelClick(model.id)}
|
||||
>
|
||||
<div className="flex items-center gap-2 md:gap-3">
|
||||
<div className="w-7 h-7 md:w-8 md:h-8 flex items-center justify-center flex-shrink-0">
|
||||
{getModelIcon(model.provider || model.id, {
|
||||
width: 28,
|
||||
height: 28,
|
||||
}) || (
|
||||
<div
|
||||
className="w-7 h-7 md:w-8 md:h-8 rounded-full flex items-center justify-center text-xs md:text-sm font-bold"
|
||||
style={{
|
||||
background:
|
||||
model.id === 'deepseek' ? '#60a5fa' : '#c084fc',
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
{getShortName(model.name)[0]}
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-indigo-500/20 rounded-full blur-sm group-hover:bg-indigo-500/30 transition-all"></div>
|
||||
<div className="w-10 h-10 rounded-full flex items-center justify-center bg-black border border-white/10 relative z-10">
|
||||
{getModelIcon(model.provider || model.id, { width: 20, height: 20 }) || (
|
||||
<span className="text-xs font-bold text-indigo-400">{getShortName(model.name)[0]}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0">
|
||||
<div
|
||||
className="font-semibold text-sm md:text-base truncate"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
<div className="font-mono text-sm text-zinc-200 group-hover:text-nofx-gold transition-colors">
|
||||
{getShortName(model.name)}
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: '#F0B90B' }}>
|
||||
<div className="text-[10px] text-zinc-500 font-mono flex items-center gap-2">
|
||||
{model.customModelName || AI_PROVIDER_CONFIG[model.provider]?.defaultModel || ''}
|
||||
</div>
|
||||
<div className="text-xs flex items-center gap-1.5" style={{ color: '#848E9C' }}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-right">
|
||||
{usageInfo.totalCount > 0 ? (
|
||||
<span
|
||||
className="px-1.5 py-0.5 rounded text-[10px] font-medium"
|
||||
style={
|
||||
usageInfo.runningCount > 0
|
||||
? { background: 'rgba(14, 203, 129, 0.15)', color: '#0ECB81' }
|
||||
: { background: 'rgba(240, 185, 11, 0.15)', color: '#F0B90B' }
|
||||
}
|
||||
>
|
||||
{usageInfo.runningCount > 0
|
||||
? `${usageInfo.runningCount}/${usageInfo.totalCount} ${language === 'zh' ? '运行中' : 'Running'}`
|
||||
: `${usageInfo.totalCount} ${language === 'zh' ? '个交易员' : usageInfo.totalCount === 1 ? 'Trader' : 'Traders'}`
|
||||
}
|
||||
<span className={`text-[10px] font-mono px-2 py-1 rounded border ${usageInfo.runningCount > 0
|
||||
? 'bg-green-500/10 border-green-500/30 text-green-400'
|
||||
: 'bg-yellow-500/10 border-yellow-500/30 text-yellow-400'
|
||||
}`}>
|
||||
{usageInfo.runningCount}/{usageInfo.totalCount} ACTIVE
|
||||
</span>
|
||||
) : (
|
||||
<span style={{ color: '#848E9C' }}>
|
||||
{model.enabled
|
||||
? (language === 'zh' ? '空闲' : 'Idle')
|
||||
: (language === 'zh' ? '已配置' : 'Configured')}
|
||||
<span className="text-[10px] font-mono text-zinc-600 uppercase tracking-wider">
|
||||
{language === 'zh' ? '就绪' : 'STANDBY'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`w-2.5 h-2.5 md:w-3 md:h-3 rounded-full flex-shrink-0 ${model.enabled ? 'bg-green-400' : 'bg-gray-500'}`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{configuredModels.length === 0 && (
|
||||
<div
|
||||
className="text-center py-6 md:py-8"
|
||||
style={{ color: '#848E9C' }}
|
||||
>
|
||||
<Brain className="w-10 h-10 md:w-12 md:h-12 mx-auto mb-2 opacity-50" />
|
||||
<div className="text-xs md:text-sm">
|
||||
{t('noModelsConfigured', language)}
|
||||
</div>
|
||||
<div className="text-center py-10 border border-dashed border-zinc-800 rounded-lg bg-black/20">
|
||||
<Brain className="w-8 h-8 mx-auto mb-3 text-zinc-700" />
|
||||
<div className="text-xs font-mono text-zinc-500 uppercase tracking-widest">{t('noModelsConfigured', language)}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Exchanges */}
|
||||
<div className="binance-card p-3 md:p-4">
|
||||
<h3
|
||||
className="text-base md:text-lg font-semibold mb-3 flex items-center gap-2"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
<Landmark
|
||||
className="w-4 h-4 md:w-5 md:h-5"
|
||||
style={{ color: '#F0B90B' }}
|
||||
/>
|
||||
{/* Exchanges Card */}
|
||||
<div className="nofx-glass rounded-lg border border-white/5 overflow-hidden">
|
||||
<div className="px-4 py-3 border-b border-white/5 bg-black/20 flex items-center gap-2 backdrop-blur-sm">
|
||||
<Landmark className="w-4 h-4 text-nofx-gold" />
|
||||
<h3 className="text-sm font-mono tracking-widest text-zinc-300 uppercase">
|
||||
{t('exchanges', language)}
|
||||
</h3>
|
||||
<div className="space-y-2 md:space-y-3">
|
||||
</div>
|
||||
|
||||
<div className="p-4 space-y-3">
|
||||
{configuredExchanges.map((exchange) => {
|
||||
const inUse = isExchangeInUse(exchange.id)
|
||||
const usageInfo = getExchangeUsageInfo(exchange.id)
|
||||
return (
|
||||
<div
|
||||
key={exchange.id}
|
||||
className={`flex items-center justify-between p-2 md:p-3 rounded transition-all ${
|
||||
inUse
|
||||
? 'cursor-not-allowed'
|
||||
: 'cursor-pointer hover:bg-gray-700'
|
||||
}`}
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||
className={`group relative flex items-center justify-between p-3 rounded-md transition-all border border-transparent ${inUse ? 'opacity-80' : 'hover:bg-white/5 hover:border-white/10 cursor-pointer'
|
||||
} bg-black/20`}
|
||||
onClick={() => handleExchangeClick(exchange.id)}
|
||||
>
|
||||
{/* Left: Icon + Name + Type */}
|
||||
<div className="flex items-center gap-2 md:gap-3 min-w-0">
|
||||
<div className="w-7 h-7 md:w-8 md:h-8 flex items-center justify-center flex-shrink-0">
|
||||
{getExchangeIcon(exchange.exchange_type || exchange.id, { width: 28, height: 28 })}
|
||||
<div className="flex items-center gap-4 min-w-0">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-yellow-500/20 rounded-full blur-sm group-hover:bg-yellow-500/30 transition-all"></div>
|
||||
<div className="w-10 h-10 rounded-full flex items-center justify-center bg-black border border-white/10 relative z-10">
|
||||
{getExchangeIcon(exchange.exchange_type || exchange.id, { width: 20, height: 20 })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0">
|
||||
<div
|
||||
className="font-semibold text-sm md:text-base truncate"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
<div className="font-mono text-sm text-zinc-200 group-hover:text-nofx-gold transition-colors truncate">
|
||||
{exchange.exchange_type?.toUpperCase() || getShortName(exchange.name)}
|
||||
<span className="text-xs font-normal ml-1.5" style={{ color: '#F0B90B' }}>
|
||||
- {exchange.account_name || 'Default'}
|
||||
<span className="text-[10px] text-zinc-500 ml-2 border border-zinc-800 px-1 rounded">
|
||||
{exchange.account_name || 'DEFAULT'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs flex items-center gap-1.5" style={{ color: '#848E9C' }}>
|
||||
<span>{exchange.type?.toUpperCase() || 'CEX'}</span>
|
||||
<span>•</span>
|
||||
{usageInfo.totalCount > 0 ? (
|
||||
<span
|
||||
className="px-1.5 py-0.5 rounded text-[10px] font-medium"
|
||||
style={
|
||||
usageInfo.runningCount > 0
|
||||
? { background: 'rgba(14, 203, 129, 0.15)', color: '#0ECB81' }
|
||||
: { background: 'rgba(240, 185, 11, 0.15)', color: '#F0B90B' }
|
||||
}
|
||||
>
|
||||
{usageInfo.runningCount > 0
|
||||
? `${usageInfo.runningCount}/${usageInfo.totalCount} ${language === 'zh' ? '运行中' : 'Running'}`
|
||||
: `${usageInfo.totalCount} ${language === 'zh' ? '个交易员' : usageInfo.totalCount === 1 ? 'Trader' : 'Traders'}`
|
||||
}
|
||||
</span>
|
||||
) : (
|
||||
<span style={{ color: '#848E9C' }}>
|
||||
{exchange.enabled
|
||||
? (language === 'zh' ? '空闲' : 'Idle')
|
||||
: (language === 'zh' ? '已配置' : 'Configured')}
|
||||
</span>
|
||||
)}
|
||||
<div className="text-[10px] text-zinc-500 font-mono flex items-center gap-2">
|
||||
{exchange.type?.toUpperCase() || 'CEX'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Right: Wallet Address + Status Dot */}
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
{/* Wallet address for DEX exchanges */}
|
||||
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
{/* Wallet Address Display Logic */}
|
||||
{(() => {
|
||||
const walletAddr = exchange.hyperliquidWalletAddr || exchange.asterUser || exchange.lighterWalletAddr
|
||||
if (exchange.type !== 'dex' || !walletAddr) return null
|
||||
|
||||
const isVisible = visibleExchangeAddresses.has(exchange.id)
|
||||
const isCopied = copiedId === `exchange-${exchange.id}`
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-1 px-2 py-1 rounded"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.08)',
|
||||
border: '1px solid rgba(240, 185, 11, 0.2)',
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<span className="text-xs font-mono" style={{ color: '#F0B90B' }}>
|
||||
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
|
||||
<span className="text-[10px] font-mono text-zinc-400 bg-black/40 px-1.5 py-0.5 rounded border border-zinc-800">
|
||||
{isVisible ? walletAddr : truncateAddress(walletAddr)}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
toggleExchangeAddressVisibility(exchange.id)
|
||||
}}
|
||||
className="p-0.5 rounded hover:bg-gray-700 transition-colors"
|
||||
title={isVisible ? (language === 'zh' ? '隐藏' : 'Hide') : (language === 'zh' ? '显示' : 'Show')}
|
||||
onClick={(e) => { e.stopPropagation(); toggleExchangeAddressVisibility(exchange.id) }}
|
||||
className="text-zinc-600 hover:text-zinc-300"
|
||||
>
|
||||
{isVisible ? (
|
||||
<EyeOff className="w-3 h-3" style={{ color: '#848E9C' }} />
|
||||
) : (
|
||||
<Eye className="w-3 h-3" style={{ color: '#848E9C' }} />
|
||||
)}
|
||||
{isVisible ? <EyeOff size={10} /> : <Eye size={10} />}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleCopyAddress(`exchange-${exchange.id}`, walletAddr)
|
||||
}}
|
||||
className="p-0.5 rounded hover:bg-gray-700 transition-colors"
|
||||
title={language === 'zh' ? '复制' : 'Copy'}
|
||||
onClick={(e) => { e.stopPropagation(); handleCopyAddress(`exchange-${exchange.id}`, walletAddr) }}
|
||||
className="text-zinc-600 hover:text-nofx-gold"
|
||||
>
|
||||
{isCopied ? (
|
||||
<Check className="w-3 h-3" style={{ color: '#0ECB81' }} />
|
||||
) : (
|
||||
<Copy className="w-3 h-3" style={{ color: '#848E9C' }} />
|
||||
)}
|
||||
{isCopied ? <Check size={10} className="text-green-500" /> : <Copy size={10} />}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
<div
|
||||
className={`w-2.5 h-2.5 md:w-3 md:h-3 rounded-full flex-shrink-0 ${exchange.enabled ? 'bg-green-400' : 'bg-gray-500'}`}
|
||||
/>
|
||||
|
||||
{usageInfo.totalCount > 0 ? (
|
||||
<span className={`text-[10px] font-mono px-2 py-1 rounded border ${usageInfo.runningCount > 0
|
||||
? 'bg-green-500/10 border-green-500/30 text-green-400'
|
||||
: 'bg-yellow-500/10 border-yellow-500/30 text-yellow-400'
|
||||
}`}>
|
||||
{usageInfo.runningCount}/{usageInfo.totalCount} ACTIVE
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-[10px] font-mono text-zinc-600 uppercase tracking-wider">
|
||||
{language === 'zh' ? '就绪' : 'STANDBY'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{configuredExchanges.length === 0 && (
|
||||
<div
|
||||
className="text-center py-6 md:py-8"
|
||||
style={{ color: '#848E9C' }}
|
||||
>
|
||||
<Landmark className="w-10 h-10 md:w-12 md:h-12 mx-auto mb-2 opacity-50" />
|
||||
<div className="text-xs md:text-sm">
|
||||
{t('noExchangesConfigured', language)}
|
||||
</div>
|
||||
<div className="text-center py-10 border border-dashed border-zinc-800 rounded-lg bg-black/20">
|
||||
<Landmark className="w-8 h-8 mx-auto mb-3 text-zinc-700" />
|
||||
<div className="text-xs font-mono text-zinc-500 uppercase tracking-widest">{t('noExchangesConfigured', language)}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -1279,8 +1172,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
{t('status', language)}
|
||||
</div> */}
|
||||
<div
|
||||
className={`px-2 md:px-3 py-1 rounded text-xs font-bold ${
|
||||
trader.is_running
|
||||
className={`px-2 md:px-3 py-1 rounded text-xs font-bold ${trader.is_running
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-red-100 text-red-800'
|
||||
}`}
|
||||
@@ -1488,6 +1380,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</DeepVoidBackground>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
ArrowDownRight,
|
||||
CandlestickChart as CandlestickIcon,
|
||||
} from 'lucide-react'
|
||||
import { DeepVoidBackground } from './DeepVoidBackground'
|
||||
import {
|
||||
ResponsiveContainer,
|
||||
AreaChart,
|
||||
@@ -1068,7 +1069,8 @@ export function BacktestPage() {
|
||||
|
||||
// Render
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<DeepVoidBackground className="py-8" disableAnimation>
|
||||
<div className="w-full px-4 md:px-8 space-y-6">
|
||||
{/* Toast */}
|
||||
<AnimatePresence>
|
||||
{toast && (
|
||||
@@ -1989,5 +1991,6 @@ export function BacktestPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DeepVoidBackground>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -145,43 +145,41 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
|
||||
console.log('[ChartTabs] rendering, activeTab:', activeTab)
|
||||
|
||||
return (
|
||||
<div className="binance-card" style={{ background: '#0D1117', borderRadius: '8px', overflow: 'hidden' }}>
|
||||
<div className="nofx-glass rounded-lg border border-white/5 relative z-10 w-full h-[600px] flex flex-col">
|
||||
{/* Clean Professional Toolbar */}
|
||||
<div
|
||||
className="flex items-center justify-between px-3 py-1.5"
|
||||
style={{ borderBottom: '1px solid rgba(43, 49, 57, 0.6)', background: '#161B22' }}
|
||||
className="relative z-20 flex flex-wrap md:flex-nowrap items-center justify-between gap-y-2 px-3 py-2 shrink-0 backdrop-blur-md bg-[#0B0E11]/80 rounded-t-lg"
|
||||
style={{ borderBottom: '1px solid rgba(255, 255, 255, 0.05)' }}
|
||||
>
|
||||
{/* Left: Tab Switcher */}
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => setActiveTab('equity')}
|
||||
className={`flex items-center gap-1.5 px-2.5 py-1 rounded text-[11px] font-medium transition-all ${
|
||||
activeTab === 'equity'
|
||||
? 'bg-blue-500/15 text-blue-400'
|
||||
: 'text-gray-500 hover:text-gray-300'
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-[11px] font-medium transition-all ${activeTab === 'equity'
|
||||
? 'bg-nofx-gold/10 text-nofx-gold border border-nofx-gold/20 shadow-[0_0_10px_rgba(240,185,11,0.1)]'
|
||||
: 'text-nofx-text-muted hover:text-nofx-text-main hover:bg-white/5'
|
||||
}`}
|
||||
>
|
||||
<BarChart3 className="w-3 h-3" />
|
||||
<BarChart3 className="w-3.5 h-3.5" />
|
||||
<span>{t('accountEquityCurve', language)}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('kline')}
|
||||
className={`flex items-center gap-1.5 px-2.5 py-1 rounded text-[11px] font-medium transition-all ${
|
||||
activeTab === 'kline'
|
||||
? 'bg-blue-500/15 text-blue-400'
|
||||
: 'text-gray-500 hover:text-gray-300'
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-[11px] font-medium transition-all ${activeTab === 'kline'
|
||||
? 'bg-nofx-gold/10 text-nofx-gold border border-nofx-gold/20 shadow-[0_0_10px_rgba(240,185,11,0.1)]'
|
||||
: 'text-nofx-text-muted hover:text-nofx-text-main hover:bg-white/5'
|
||||
}`}
|
||||
>
|
||||
<CandlestickChart className="w-3 h-3" />
|
||||
<CandlestickChart className="w-3.5 h-3.5" />
|
||||
<span>{t('marketChart', language)}</span>
|
||||
</button>
|
||||
|
||||
{/* Market Type Pills - Only when kline active */}
|
||||
{activeTab === 'kline' && (
|
||||
<>
|
||||
<div className="w-px h-3 bg-[#30363D] mx-1" />
|
||||
<div className="flex items-center gap-0.5">
|
||||
<div className="w-px h-4 bg-white/10 mx-2" />
|
||||
<div className="flex items-center gap-1">
|
||||
{(Object.keys(MARKET_CONFIG) as MarketType[]).map((type) => {
|
||||
const config = MARKET_CONFIG[type]
|
||||
const isActive = marketType === type
|
||||
@@ -189,13 +187,13 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => handleMarketTypeChange(type)}
|
||||
className={`px-2 py-0.5 text-[10px] font-medium rounded transition-all ${
|
||||
isActive
|
||||
? 'bg-[#21262D] text-white'
|
||||
: 'text-gray-500 hover:text-gray-400'
|
||||
className={`px-2.5 py-1 text-[10px] font-medium rounded transition-all border ${isActive
|
||||
? 'bg-white/10 text-white border-white/20'
|
||||
: 'text-nofx-text-muted border-transparent hover:text-nofx-text-main hover:bg-white/5'
|
||||
}`}
|
||||
>
|
||||
{config.icon} {language === 'zh' ? config.label.zh : config.label.en}
|
||||
<span className="mr-1 opacity-70">{config.icon}</span>
|
||||
{language === 'zh' ? config.label.zh : config.label.en}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
@@ -206,47 +204,49 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
|
||||
|
||||
{/* Right: Symbol + Interval */}
|
||||
{activeTab === 'kline' && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 md:gap-3 w-full md:w-auto min-w-0">
|
||||
{/* Symbol Dropdown */}
|
||||
<div className="shrink-0 relative" ref={dropdownRef}>
|
||||
{marketConfig.hasDropdown ? (
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<>
|
||||
<button
|
||||
onClick={() => setShowDropdown(!showDropdown)}
|
||||
className="flex items-center gap-1 px-2 py-1 bg-[#21262D] rounded text-[11px] font-bold text-white hover:bg-[#30363D] transition-all"
|
||||
className="flex items-center gap-1.5 px-2.5 py-1 bg-black/40 border border-white/10 rounded text-[11px] font-bold text-nofx-text-main hover:border-nofx-gold/30 hover:text-nofx-gold transition-all"
|
||||
>
|
||||
<span>{chartSymbol}</span>
|
||||
<ChevronDown className={`w-3 h-3 text-gray-400 transition-transform ${showDropdown ? 'rotate-180' : ''}`} />
|
||||
<ChevronDown className={`w-3 h-3 text-nofx-text-muted transition-transform ${showDropdown ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
{showDropdown && (
|
||||
<div className="absolute top-full right-0 mt-1 w-56 bg-[#161B22] border border-[#30363D] rounded-lg shadow-2xl z-50 max-h-72 overflow-hidden">
|
||||
<div className="p-2 border-b border-[#30363D]">
|
||||
<div className="flex items-center gap-2 px-2 py-1 bg-[#0D1117] rounded border border-[#30363D]">
|
||||
<Search className="w-3 h-3 text-gray-500" />
|
||||
<div className="absolute top-full right-0 mt-2 w-64 bg-[#0B0E11] border border-white/10 rounded-lg shadow-[0_10px_40px_-10px_rgba(0,0,0,0.5)] z-50 overflow-hidden nofx-glass ring-1 ring-white/5">
|
||||
<div className="p-2 border-b border-white/5">
|
||||
<div className="flex items-center gap-2 px-2 py-1.5 bg-black/40 rounded border border-white/10 focus-within:border-nofx-gold/50 transition-colors">
|
||||
<Search className="w-3.5 h-3.5 text-nofx-text-muted" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchFilter}
|
||||
onChange={(e) => setSearchFilter(e.target.value)}
|
||||
placeholder="Search..."
|
||||
className="flex-1 bg-transparent text-[11px] text-white placeholder-gray-600 focus:outline-none"
|
||||
placeholder="Search symbol..."
|
||||
className="flex-1 bg-transparent text-[11px] text-white placeholder-gray-600 focus:outline-none font-mono"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-y-auto max-h-52">
|
||||
<div className="overflow-y-auto max-h-60 custom-scrollbar">
|
||||
{['crypto', 'stock', 'forex', 'commodity', 'index'].map(category => {
|
||||
const categorySymbols = filteredSymbols.filter(s => s.category === category)
|
||||
if (categorySymbols.length === 0) return null
|
||||
const labels: Record<string, string> = { crypto: 'Crypto', stock: 'Stocks', forex: 'Forex', commodity: 'Commodities', index: 'Index' }
|
||||
return (
|
||||
<div key={category}>
|
||||
<div className="px-3 py-1 text-[9px] font-medium text-gray-500 bg-[#0D1117] uppercase tracking-wider">{labels[category]}</div>
|
||||
<div className="px-3 py-1.5 text-[9px] font-bold text-nofx-text-muted/60 bg-white/5 uppercase tracking-wider">{labels[category]}</div>
|
||||
{categorySymbols.map(s => (
|
||||
<button
|
||||
key={s.symbol}
|
||||
onClick={() => { setChartSymbol(s.symbol); setShowDropdown(false); setSearchFilter('') }}
|
||||
className={`w-full px-3 py-1.5 text-left text-[11px] hover:bg-[#21262D] transition-all ${chartSymbol === s.symbol ? 'bg-blue-500/20 text-blue-400' : 'text-gray-300'}`}
|
||||
className={`w-full px-3 py-2 text-left text-[11px] font-mono hover:bg-white/5 transition-all flex items-center justify-between ${chartSymbol === s.symbol ? 'bg-nofx-gold/10 text-nofx-gold' : 'text-nofx-text-muted'}`}
|
||||
>
|
||||
{s.symbol}
|
||||
<span>{s.symbol}</span>
|
||||
<span className="text-[9px] opacity-40">{s.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -255,21 +255,21 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<span className="px-2 py-1 bg-[#21262D] rounded text-[11px] font-bold text-white">{chartSymbol}</span>
|
||||
<span className="px-2.5 py-1 bg-black/40 border border-white/10 rounded text-[11px] font-bold text-nofx-text-main font-mono">{chartSymbol}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Interval Selector */}
|
||||
<div className="flex items-center bg-[#21262D] rounded overflow-hidden">
|
||||
{/* Interval Selector - Allow scrolling if needed */}
|
||||
<div className="flex items-center bg-black/40 rounded border border-white/10 overflow-x-auto no-scrollbar max-w-[200px] md:max-w-none">
|
||||
{INTERVALS.map((int) => (
|
||||
<button
|
||||
key={int.value}
|
||||
onClick={() => setInterval(int.value)}
|
||||
className={`px-2 py-1 text-[10px] font-medium transition-all ${
|
||||
interval === int.value
|
||||
? 'bg-blue-500/30 text-blue-400'
|
||||
: 'text-gray-500 hover:text-gray-300 hover:bg-[#30363D]'
|
||||
className={`px-2 py-1 text-[10px] font-medium transition-all ${interval === int.value
|
||||
? 'bg-nofx-gold/20 text-nofx-gold'
|
||||
: 'text-nofx-text-muted hover:text-white hover:bg-white/5'
|
||||
}`}
|
||||
>
|
||||
{int.label}
|
||||
@@ -277,16 +277,16 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Quick Input */}
|
||||
<form onSubmit={handleSymbolSubmit} className="flex items-center">
|
||||
{/* Quick Input - Hidden on mobile, dropdown search is enough */}
|
||||
<form onSubmit={handleSymbolSubmit} className="hidden md:flex items-center shrink-0">
|
||||
<input
|
||||
type="text"
|
||||
value={symbolInput}
|
||||
onChange={(e) => setSymbolInput(e.target.value)}
|
||||
placeholder="Symbol..."
|
||||
className="w-20 px-2 py-1 bg-[#0D1117] border border-[#30363D] rounded-l text-[10px] text-white placeholder-gray-600 focus:outline-none focus:border-blue-500/50"
|
||||
placeholder="Sym"
|
||||
className="w-16 px-2 py-1 bg-black/40 border border-white/10 rounded-l text-[10px] text-white placeholder-gray-600 focus:outline-none focus:border-nofx-gold/50 font-mono transition-colors"
|
||||
/>
|
||||
<button type="submit" className="px-2 py-1 bg-[#21262D] border border-[#30363D] border-l-0 rounded-r text-[10px] text-gray-400 hover:text-white hover:bg-[#30363D] transition-all">
|
||||
<button type="submit" className="px-2 py-1 bg-white/5 border border-white/10 border-l-0 rounded-r text-[10px] text-nofx-text-muted hover:text-white hover:bg-white/10 transition-all">
|
||||
Go
|
||||
</button>
|
||||
</form>
|
||||
@@ -295,32 +295,33 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="relative overflow-hidden min-h-[400px]">
|
||||
<div className="relative flex-1 bg-[#0B0E11]/50 rounded-b-lg overflow-hidden">
|
||||
<AnimatePresence mode="wait">
|
||||
{activeTab === 'equity' ? (
|
||||
<motion.div
|
||||
key="equity"
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 20 }}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="h-full"
|
||||
className="h-full w-full absolute inset-0"
|
||||
>
|
||||
<EquityChart traderId={traderId} embedded />
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key={`kline-${chartSymbol}-${interval}-${currentExchange}`}
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="h-full"
|
||||
className="h-full w-full absolute inset-0"
|
||||
>
|
||||
<AdvancedChart
|
||||
symbol={chartSymbol}
|
||||
interval={interval}
|
||||
traderID={traderId}
|
||||
// Dynamic height to fill container
|
||||
height={550}
|
||||
exchange={currentExchange}
|
||||
onSymbolChange={setChartSymbol}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { getTraderColor } from '../utils/traderColors'
|
||||
import { useLanguage } from '../contexts/LanguageContext'
|
||||
import { t } from '../i18n/translations'
|
||||
import { PunkAvatar, getTraderAvatar } from './PunkAvatar'
|
||||
import { DeepVoidBackground } from './DeepVoidBackground'
|
||||
|
||||
export function CompetitionPage() {
|
||||
const { language } = useLanguage()
|
||||
@@ -44,63 +45,58 @@ export function CompetitionPage() {
|
||||
|
||||
if (!competition) {
|
||||
return (
|
||||
<DeepVoidBackground className="py-8" disableAnimation>
|
||||
<div className="container mx-auto max-w-7xl px-4 md:px-8">
|
||||
<div className="space-y-6">
|
||||
<div className="binance-card p-8 animate-pulse">
|
||||
<div className="animate-pulse bg-black/40 border border-white/10 rounded-xl p-8 backdrop-blur-md">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="space-y-3 flex-1">
|
||||
<div className="skeleton h-8 w-64"></div>
|
||||
<div className="skeleton h-4 w-48"></div>
|
||||
<div className="h-8 w-64 bg-white/5 rounded"></div>
|
||||
<div className="h-4 w-48 bg-white/5 rounded"></div>
|
||||
</div>
|
||||
<div className="skeleton h-12 w-32"></div>
|
||||
<div className="h-12 w-32 bg-white/5 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="binance-card p-6">
|
||||
<div className="skeleton h-6 w-40 mb-4"></div>
|
||||
<div className="bg-black/40 border border-white/10 rounded-xl p-6 backdrop-blur-md">
|
||||
<div className="h-6 w-40 mb-4 bg-white/5 rounded"></div>
|
||||
<div className="space-y-3">
|
||||
<div className="skeleton h-20 w-full rounded"></div>
|
||||
<div className="skeleton h-20 w-full rounded"></div>
|
||||
<div className="h-20 w-full bg-white/5 rounded"></div>
|
||||
<div className="h-20 w-full bg-white/5 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DeepVoidBackground>
|
||||
)
|
||||
}
|
||||
|
||||
// 如果有数据返回但没有交易员,显示空状态
|
||||
if (!competition.traders || competition.traders.length === 0) {
|
||||
return (
|
||||
<div className="space-y-5 animate-fade-in">
|
||||
<DeepVoidBackground className="py-8" disableAnimation>
|
||||
<div className="container mx-auto max-w-7xl px-4 md:px-8 space-y-8 animate-fade-in">
|
||||
{/* Competition Header - 精简版 */}
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-3 md:gap-0">
|
||||
<div className="flex items-center gap-3 md:gap-4">
|
||||
<div
|
||||
className="w-10 h-10 md:w-12 md:h-12 rounded-xl flex items-center justify-center"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
|
||||
boxShadow: '0 4px 14px rgba(240, 185, 11, 0.4)',
|
||||
}}
|
||||
className="w-10 h-10 md:w-12 md:h-12 rounded-xl flex items-center justify-center bg-black/60 border border-nofx-gold/30 shadow-[0_0_15px_rgba(240,185,11,0.2)]"
|
||||
>
|
||||
<Trophy
|
||||
className="w-6 h-6 md:w-7 md:h-7"
|
||||
style={{ color: '#000' }}
|
||||
className="w-6 h-6 md:w-7 md:h-7 text-nofx-gold"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h1
|
||||
className="text-xl md:text-2xl font-bold flex items-center gap-2"
|
||||
style={{ color: '#EAECEF' }}
|
||||
className="text-xl md:text-2xl font-bold flex items-center gap-2 text-white"
|
||||
>
|
||||
{t('aiCompetition', language)}
|
||||
<span
|
||||
className="text-xs font-normal px-2 py-1 rounded"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.15)',
|
||||
color: '#F0B90B',
|
||||
}}
|
||||
className="text-xs font-normal px-2 py-1 rounded bg-nofx-gold/10 text-nofx-gold border border-nofx-gold/20"
|
||||
>
|
||||
0 {t('traders', language)}
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-xs" style={{ color: '#848E9C' }}>
|
||||
<p className="text-xs text-zinc-400">
|
||||
{t('liveBattle', language)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -108,19 +104,19 @@ export function CompetitionPage() {
|
||||
</div>
|
||||
|
||||
{/* Empty State */}
|
||||
<div className="binance-card p-8 text-center">
|
||||
<div className="bg-black/40 border border-white/10 rounded-xl p-16 text-center backdrop-blur-md">
|
||||
<Trophy
|
||||
className="w-16 h-16 mx-auto mb-4 opacity-40"
|
||||
style={{ color: '#848E9C' }}
|
||||
className="w-16 h-16 mx-auto mb-4 text-zinc-700"
|
||||
/>
|
||||
<h3 className="text-lg font-bold mb-2" style={{ color: '#EAECEF' }}>
|
||||
<h3 className="text-lg font-bold mb-2 text-white">
|
||||
{t('noTraders', language)}
|
||||
</h3>
|
||||
<p className="text-sm" style={{ color: '#848E9C' }}>
|
||||
<p className="text-sm text-zinc-400">
|
||||
{t('createFirstTrader', language)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</DeepVoidBackground>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -133,50 +129,40 @@ export function CompetitionPage() {
|
||||
const leader = sortedTraders[0]
|
||||
|
||||
return (
|
||||
<div className="space-y-5 animate-fade-in">
|
||||
<DeepVoidBackground className="py-8" disableAnimation>
|
||||
<div className="w-full px-4 md:px-8 space-y-8 animate-fade-in">
|
||||
{/* Competition Header - 精简版 */}
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-3 md:gap-0">
|
||||
<div className="flex items-center gap-3 md:gap-4">
|
||||
<div
|
||||
className="w-10 h-10 md:w-12 md:h-12 rounded-xl flex items-center justify-center"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
|
||||
boxShadow: '0 4px 14px rgba(240, 185, 11, 0.4)',
|
||||
}}
|
||||
className="w-10 h-10 md:w-12 md:h-12 rounded-xl flex items-center justify-center bg-black/60 border border-nofx-gold/30 shadow-[0_0_15px_rgba(240,185,11,0.2)]"
|
||||
>
|
||||
<Trophy
|
||||
className="w-6 h-6 md:w-7 md:h-7"
|
||||
style={{ color: '#000' }}
|
||||
className="w-6 h-6 md:w-7 md:h-7 text-nofx-gold"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h1
|
||||
className="text-xl md:text-2xl font-bold flex items-center gap-2"
|
||||
style={{ color: '#EAECEF' }}
|
||||
className="text-xl md:text-2xl font-bold flex items-center gap-2 text-white"
|
||||
>
|
||||
{t('aiCompetition', language)}
|
||||
<span
|
||||
className="text-xs font-normal px-2 py-1 rounded"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.15)',
|
||||
color: '#F0B90B',
|
||||
}}
|
||||
className="text-xs font-normal px-2 py-1 rounded bg-nofx-gold/10 text-nofx-gold border border-nofx-gold/20"
|
||||
>
|
||||
{competition.count} {t('traders', language)}
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-xs" style={{ color: '#848E9C' }}>
|
||||
<p className="text-xs text-zinc-400">
|
||||
{t('liveBattle', language)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-left md:text-right w-full md:w-auto">
|
||||
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>
|
||||
<div className="text-xs mb-1 text-zinc-400">
|
||||
{t('leader', language)}
|
||||
</div>
|
||||
<div
|
||||
className="text-base md:text-lg font-bold"
|
||||
style={{ color: '#F0B90B' }}
|
||||
className="text-base md:text-lg font-bold text-nofx-gold"
|
||||
>
|
||||
{leader?.trader_name}
|
||||
</div>
|
||||
@@ -193,20 +179,19 @@ export function CompetitionPage() {
|
||||
</div>
|
||||
|
||||
{/* Left/Right Split: Performance Chart + Leaderboard */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Left: Performance Comparison Chart */}
|
||||
<div
|
||||
className="binance-card p-5 animate-slide-in"
|
||||
className="bg-black/40 border border-white/10 rounded-xl p-6 backdrop-blur-md animate-slide-in hover:border-white/20 transition-colors"
|
||||
style={{ animationDelay: '0.1s' }}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2
|
||||
className="text-lg font-bold flex items-center gap-2"
|
||||
style={{ color: '#EAECEF' }}
|
||||
className="text-lg font-bold flex items-center gap-2 text-white"
|
||||
>
|
||||
{t('performanceComparison', language)}
|
||||
</h2>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
<div className="text-xs text-zinc-400">
|
||||
{t('realTimePnL', language)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -215,23 +200,17 @@ export function CompetitionPage() {
|
||||
|
||||
{/* Right: Leaderboard */}
|
||||
<div
|
||||
className="binance-card p-5 animate-slide-in"
|
||||
className="bg-black/40 border border-white/10 rounded-xl p-6 backdrop-blur-md animate-slide-in hover:border-white/20 transition-colors"
|
||||
style={{ animationDelay: '0.1s' }}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2
|
||||
className="text-lg font-bold flex items-center gap-2"
|
||||
style={{ color: '#EAECEF' }}
|
||||
className="text-lg font-bold flex items-center gap-2 text-white"
|
||||
>
|
||||
{t('leaderboard', language)}
|
||||
</h2>
|
||||
<div
|
||||
className="text-xs px-2 py-1 rounded"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.1)',
|
||||
color: '#F0B90B',
|
||||
border: '1px solid rgba(240, 185, 11, 0.2)',
|
||||
}}
|
||||
className="text-xs px-2 py-1 rounded bg-nofx-gold/10 text-nofx-gold border border-nofx-gold/20 shadow-[0_0_8px_rgba(240,185,11,0.1)]"
|
||||
>
|
||||
{t('live', language)}
|
||||
</div>
|
||||
@@ -389,12 +368,11 @@ export function CompetitionPage() {
|
||||
{/* Head-to-Head Stats */}
|
||||
{competition.traders.length === 2 && (
|
||||
<div
|
||||
className="binance-card p-5 animate-slide-in"
|
||||
className="bg-black/40 border border-white/10 rounded-xl p-6 backdrop-blur-md animate-slide-in"
|
||||
style={{ animationDelay: '0.3s' }}
|
||||
>
|
||||
<h2
|
||||
className="text-lg font-bold mb-4 flex items-center gap-2"
|
||||
style={{ color: '#EAECEF' }}
|
||||
className="text-lg font-bold mb-6 flex items-center gap-2 text-white"
|
||||
>
|
||||
{t('headToHead', language)}
|
||||
</h2>
|
||||
@@ -503,5 +481,6 @@ export function CompetitionPage() {
|
||||
traderData={selectedTrader}
|
||||
/>
|
||||
</div>
|
||||
</DeepVoidBackground>
|
||||
)
|
||||
}
|
||||
|
||||
41
web/src/components/DeepVoidBackground.tsx
Normal file
41
web/src/components/DeepVoidBackground.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
interface DeepVoidBackgroundProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
disableAnimation?: boolean
|
||||
}
|
||||
|
||||
export function DeepVoidBackground({ children, className = '', disableAnimation = false, ...props }: DeepVoidBackgroundProps) {
|
||||
return (
|
||||
<div className={`relative w-full min-h-screen bg-nofx-bg text-nofx-text overflow-hidden flex flex-col ${className}`} {...props}>
|
||||
{/* BACKGROUND LAYERS */}
|
||||
|
||||
{/* 1. Grain/Noise Texture */}
|
||||
<div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-20 mix-blend-soft-light pointer-events-none fixed z-0"></div>
|
||||
|
||||
{/* 2. Grid System */}
|
||||
<div className="absolute inset-0 pointer-events-none fixed z-0">
|
||||
<div className="absolute inset-x-0 bottom-0 h-[50vh] bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:40px_40px] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,#000_70%,transparent_100%)] opacity-50" style={{ transform: 'perspective(500px) rotateX(60deg) translateY(100px) scale(2)' }}></div>
|
||||
<div className="absolute inset-0 bg-grid-pattern opacity-[0.03]"></div>
|
||||
</div>
|
||||
|
||||
{/* 3. Ambient Glow Spots */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none fixed z-0">
|
||||
<div className="absolute top-[-10%] left-[-10%] w-[40vw] h-[40vw] bg-nofx-gold/10 rounded-full blur-[120px] mix-blend-screen animate-pulse-slow"></div>
|
||||
<div className="absolute bottom-[-10%] right-[-10%] w-[40vw] h-[40vw] bg-nofx-accent/5 rounded-full blur-[120px] mix-blend-screen animate-pulse-slow" style={{ animationDelay: '2s' }}></div>
|
||||
</div>
|
||||
|
||||
{/* 4. CRT/Scanline Overlay */}
|
||||
<div className="absolute inset-0 pointer-events-none fixed z-[9999] opacity-40">
|
||||
<div className="absolute inset-0 bg-[linear-gradient(rgba(18,16,16,0)_50%,rgba(0,0,0,0.25)_50%),linear-gradient(90deg,rgba(255,0,0,0.06),rgba(0,255,0,0.02),rgba(0,0,255,0.06))] bg-[length:100%_4px,3px_100%] pointer-events-none"></div>
|
||||
</div>
|
||||
|
||||
{/* Content Layer */}
|
||||
<div className="relative z-10 flex-1 flex flex-col h-full w-full">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -85,10 +85,7 @@ export default function HeaderBar({
|
||||
className="flex items-center gap-2 hover:opacity-80 transition-opacity cursor-pointer"
|
||||
>
|
||||
<img src="/icons/nofx.svg" alt="NOFX Logo" className="w-7 h-7" />
|
||||
<span
|
||||
className="text-lg font-bold"
|
||||
style={{ color: 'var(--brand-yellow)' }}
|
||||
>
|
||||
<span className="text-lg font-bold text-nofx-gold">
|
||||
NOFX
|
||||
</span>
|
||||
</div>
|
||||
@@ -128,28 +125,12 @@ export default function HeaderBar({
|
||||
<button
|
||||
key={tab.page}
|
||||
onClick={() => handleNavClick(tab)}
|
||||
className="text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
|
||||
style={{
|
||||
color: currentPage === tab.page ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '8px',
|
||||
position: 'relative',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (currentPage !== tab.page) {
|
||||
e.currentTarget.style.color = 'var(--brand-yellow)'
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (currentPage !== tab.page) {
|
||||
e.currentTarget.style.color = 'var(--brand-light-gray)'
|
||||
}
|
||||
}}
|
||||
className={`text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500 px-3 py-2 rounded-lg
|
||||
${currentPage === tab.page ? 'text-nofx-gold' : 'text-nofx-text-muted hover:text-nofx-gold'}`}
|
||||
>
|
||||
{currentPage === tab.page && (
|
||||
<span
|
||||
className="absolute inset-0 rounded-lg"
|
||||
style={{ background: 'rgba(240, 185, 11, 0.15)', zIndex: -1 }}
|
||||
className="absolute inset-0 rounded-lg bg-nofx-gold/15 -z-10"
|
||||
/>
|
||||
)}
|
||||
{tab.label}
|
||||
@@ -167,16 +148,7 @@ export default function HeaderBar({
|
||||
href={OFFICIAL_LINKS.github}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-2 rounded-lg transition-all hover:scale-110"
|
||||
style={{ color: '#848E9C' }}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.color = '#EAECEF'
|
||||
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.05)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.color = '#848E9C'
|
||||
e.currentTarget.style.background = 'transparent'
|
||||
}}
|
||||
className="p-2 rounded-lg transition-all hover:scale-110 text-nofx-text-muted hover:text-white hover:bg-white/5"
|
||||
title="GitHub"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor">
|
||||
@@ -188,16 +160,7 @@ export default function HeaderBar({
|
||||
href={OFFICIAL_LINKS.twitter}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-2 rounded-lg transition-all hover:scale-110"
|
||||
style={{ color: '#848E9C' }}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.color = '#1DA1F2'
|
||||
e.currentTarget.style.background = 'rgba(29, 161, 242, 0.1)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.color = '#848E9C'
|
||||
e.currentTarget.style.background = 'transparent'
|
||||
}}
|
||||
className="p-2 rounded-lg transition-all hover:scale-110 text-nofx-text-muted hover:text-[#1DA1F2] hover:bg-[#1DA1F2]/10"
|
||||
title="Twitter"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||
@@ -209,16 +172,7 @@ export default function HeaderBar({
|
||||
href={OFFICIAL_LINKS.telegram}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-2 rounded-lg transition-all hover:scale-110"
|
||||
style={{ color: '#848E9C' }}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.color = '#0088cc'
|
||||
e.currentTarget.style.background = 'rgba(0, 136, 204, 0.1)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.color = '#848E9C'
|
||||
e.currentTarget.style.background = 'transparent'
|
||||
}}
|
||||
className="p-2 rounded-lg transition-all hover:scale-110 text-nofx-text-muted hover:text-[#0088cc] hover:bg-[#0088cc]/10"
|
||||
title="Telegram"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||
@@ -237,62 +191,24 @@ export default function HeaderBar({
|
||||
<div className="relative" ref={userDropdownRef}>
|
||||
<button
|
||||
onClick={() => setUserDropdownOpen(!userDropdownOpen)}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded transition-colors"
|
||||
style={{
|
||||
background: 'var(--panel-bg)',
|
||||
border: '1px solid var(--panel-border)',
|
||||
}}
|
||||
onMouseEnter={(e) =>
|
||||
(e.currentTarget.style.background =
|
||||
'rgba(255, 255, 255, 0.05)')
|
||||
}
|
||||
onMouseLeave={(e) =>
|
||||
(e.currentTarget.style.background = 'var(--panel-bg)')
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold"
|
||||
style={{
|
||||
background: 'var(--brand-yellow)',
|
||||
color: 'var(--brand-black)',
|
||||
}}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded transition-colors bg-nofx-bg-lighter border border-nofx-gold/20 hover:bg-white/5"
|
||||
>
|
||||
<div className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold bg-nofx-gold text-black">
|
||||
{user.email[0].toUpperCase()}
|
||||
</div>
|
||||
<span
|
||||
className="text-sm"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
<span className="text-sm text-nofx-text-muted">
|
||||
{user.email}
|
||||
</span>
|
||||
<ChevronDown
|
||||
className="w-4 h-4"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
/>
|
||||
<ChevronDown className="w-4 h-4 text-nofx-text-muted" />
|
||||
</button>
|
||||
|
||||
{userDropdownOpen && (
|
||||
<div
|
||||
className="absolute right-0 top-full mt-2 w-48 rounded-lg shadow-lg overflow-hidden z-50"
|
||||
style={{
|
||||
background: 'var(--brand-dark-gray)',
|
||||
border: '1px solid var(--panel-border)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="px-3 py-2 border-b"
|
||||
style={{ borderColor: 'var(--panel-border)' }}
|
||||
>
|
||||
<div
|
||||
className="text-xs"
|
||||
style={{ color: 'var(--text-secondary)' }}
|
||||
>
|
||||
<div className="absolute right-0 top-full mt-2 w-48 rounded-lg shadow-lg overflow-hidden z-50 bg-nofx-bg-lighter border border-nofx-gold/20">
|
||||
<div className="px-3 py-2 border-b border-nofx-gold/20">
|
||||
<div className="text-xs text-nofx-text-muted">
|
||||
{t('loggedInAs', language)}
|
||||
</div>
|
||||
<div
|
||||
className="text-sm font-medium"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
<div className="text-sm font-medium text-nofx-text-muted">
|
||||
{user.email}
|
||||
</div>
|
||||
</div>
|
||||
@@ -302,11 +218,7 @@ export default function HeaderBar({
|
||||
onLogout()
|
||||
setUserDropdownOpen(false)
|
||||
}}
|
||||
className="w-full px-3 py-2 text-sm font-semibold transition-colors hover:opacity-80 text-center"
|
||||
style={{
|
||||
background: 'var(--binance-red-bg)',
|
||||
color: 'var(--binance-red)',
|
||||
}}
|
||||
className="w-full px-3 py-2 text-sm font-semibold transition-colors hover:opacity-80 text-center bg-nofx-danger/20 text-nofx-danger"
|
||||
>
|
||||
{t('exitLogin', language)}
|
||||
</button>
|
||||
@@ -322,19 +234,14 @@ export default function HeaderBar({
|
||||
<div className="flex items-center gap-3">
|
||||
<a
|
||||
href="/login"
|
||||
className="px-3 py-2 text-sm font-medium transition-colors rounded"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
className="px-3 py-2 text-sm font-medium transition-colors rounded text-nofx-text-muted hover:text-white"
|
||||
>
|
||||
{t('signIn', language)}
|
||||
</a>
|
||||
{registrationEnabled && (
|
||||
<a
|
||||
href="/register"
|
||||
className="px-4 py-2 rounded font-semibold text-sm transition-colors hover:opacity-90"
|
||||
style={{
|
||||
background: 'var(--brand-yellow)',
|
||||
color: 'var(--brand-black)',
|
||||
}}
|
||||
className="px-4 py-2 rounded font-semibold text-sm transition-colors hover:opacity-90 bg-nofx-gold text-black"
|
||||
>
|
||||
{t('signUp', language)}
|
||||
</a>
|
||||
@@ -347,15 +254,7 @@ export default function HeaderBar({
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setLanguageDropdownOpen(!languageDropdownOpen)}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded transition-colors"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
onMouseEnter={(e) =>
|
||||
(e.currentTarget.style.background =
|
||||
'rgba(255, 255, 255, 0.05)')
|
||||
}
|
||||
onMouseLeave={(e) =>
|
||||
(e.currentTarget.style.background = 'transparent')
|
||||
}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded transition-colors text-nofx-text-muted hover:bg-white/5"
|
||||
>
|
||||
<span className="text-lg">
|
||||
{language === 'zh' ? '🇨🇳' : '🇺🇸'}
|
||||
@@ -364,28 +263,14 @@ export default function HeaderBar({
|
||||
</button>
|
||||
|
||||
{languageDropdownOpen && (
|
||||
<div
|
||||
className="absolute right-0 top-full mt-2 w-32 rounded-lg shadow-lg overflow-hidden z-50"
|
||||
style={{
|
||||
background: 'var(--brand-dark-gray)',
|
||||
border: '1px solid var(--panel-border)',
|
||||
}}
|
||||
>
|
||||
<div className="absolute right-0 top-full mt-2 w-32 rounded-lg shadow-lg overflow-hidden z-50 bg-nofx-bg-lighter border border-nofx-gold/20">
|
||||
<button
|
||||
onClick={() => {
|
||||
onLanguageChange?.('zh')
|
||||
setLanguageDropdownOpen(false)
|
||||
}}
|
||||
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors ${
|
||||
language === 'zh' ? '' : 'hover:opacity-80'
|
||||
}`}
|
||||
style={{
|
||||
color: 'var(--brand-light-gray)',
|
||||
background:
|
||||
language === 'zh'
|
||||
? 'rgba(240, 185, 11, 0.1)'
|
||||
: 'transparent',
|
||||
}}
|
||||
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors text-nofx-text-muted hover:text-white
|
||||
${language === 'zh' ? 'bg-nofx-gold/10' : 'hover:bg-white/5'}`}
|
||||
>
|
||||
<span className="text-base">🇨🇳</span>
|
||||
<span className="text-sm">中文</span>
|
||||
@@ -395,16 +280,8 @@ export default function HeaderBar({
|
||||
onLanguageChange?.('en')
|
||||
setLanguageDropdownOpen(false)
|
||||
}}
|
||||
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors ${
|
||||
language === 'en' ? '' : 'hover:opacity-80'
|
||||
}`}
|
||||
style={{
|
||||
color: 'var(--brand-light-gray)',
|
||||
background:
|
||||
language === 'en'
|
||||
? 'rgba(240, 185, 11, 0.1)'
|
||||
: 'transparent',
|
||||
}}
|
||||
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors text-nofx-text-muted hover:text-white
|
||||
${language === 'en' ? 'bg-nofx-gold/10' : 'hover:bg-white/5'}`}
|
||||
>
|
||||
<span className="text-base">🇺🇸</span>
|
||||
<span className="text-sm">English</span>
|
||||
@@ -418,8 +295,7 @@ export default function HeaderBar({
|
||||
{/* Mobile Menu Button */}
|
||||
<motion.button
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
className="md:hidden"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
className="md:hidden text-nofx-text-muted hover:text-white"
|
||||
whileTap={{ scale: 0.9 }}
|
||||
>
|
||||
{mobileMenuOpen ? (
|
||||
@@ -439,11 +315,7 @@ export default function HeaderBar({
|
||||
: { height: 0, opacity: 0 }
|
||||
}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="md:hidden overflow-hidden"
|
||||
style={{
|
||||
background: 'var(--brand-dark-gray)',
|
||||
borderTop: '1px solid rgba(240, 185, 11, 0.1)',
|
||||
}}
|
||||
className="md:hidden overflow-hidden bg-nofx-bg-lighter border-t border-nofx-gold/10"
|
||||
>
|
||||
<div className="px-4 py-4 space-y-2">
|
||||
{/* Mobile Navigation Tabs - Show all tabs */}
|
||||
@@ -476,25 +348,17 @@ export default function HeaderBar({
|
||||
<button
|
||||
key={tab.page}
|
||||
onClick={() => handleMobileNavClick(tab)}
|
||||
className="block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
|
||||
style={{
|
||||
color: currentPage === tab.page ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
|
||||
padding: '12px 16px',
|
||||
borderRadius: '8px',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
className={`block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500 w-full text-left px-4 py-3 rounded-lg
|
||||
${currentPage === tab.page ? 'text-nofx-gold' : 'text-nofx-text-muted hover:text-white hover:bg-white/5'}`}
|
||||
>
|
||||
{currentPage === tab.page && (
|
||||
<span
|
||||
className="absolute inset-0 rounded-lg"
|
||||
style={{ background: 'rgba(240, 185, 11, 0.15)', zIndex: -1 }}
|
||||
className="absolute inset-0 rounded-lg bg-nofx-gold/15 -z-10"
|
||||
/>
|
||||
)}
|
||||
{tab.label}
|
||||
{tab.requiresAuth && !isLoggedIn && (
|
||||
<span className="ml-2 text-[10px] px-1.5 py-0.5 rounded" style={{ background: 'rgba(240, 185, 11, 0.2)', color: '#F0B90B' }}>
|
||||
<span className="ml-2 text-[10px] px-1.5 py-0.5 rounded bg-nofx-gold/20 text-nofx-gold">
|
||||
{language === 'zh' ? '需登录' : 'Login'}
|
||||
</span>
|
||||
)}
|
||||
@@ -511,21 +375,19 @@ export default function HeaderBar({
|
||||
<a
|
||||
key={item.key}
|
||||
href={`#${item.key === 'features' ? 'features' : 'how-it-works'}`}
|
||||
className="block text-sm py-2"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
className="block text-sm py-2 text-nofx-text-muted hover:text-white"
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
|
||||
{/* Social Links - Mobile */}
|
||||
<div className="py-3 flex items-center gap-3" style={{ borderTop: '1px solid #2B3139' }}>
|
||||
<div className="py-3 flex items-center gap-3 border-t border-nofx-gold/20">
|
||||
<a
|
||||
href={OFFICIAL_LINKS.github}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-2 rounded-lg"
|
||||
style={{ color: '#848E9C', background: 'rgba(255, 255, 255, 0.05)' }}
|
||||
className="p-2 rounded-lg text-nofx-text-muted bg-white/5 hover:text-white"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
|
||||
@@ -535,8 +397,7 @@ export default function HeaderBar({
|
||||
href={OFFICIAL_LINKS.twitter}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-2 rounded-lg"
|
||||
style={{ color: '#848E9C', background: 'rgba(255, 255, 255, 0.05)' }}
|
||||
className="p-2 rounded-lg text-nofx-text-muted bg-white/5 hover:text-[#1DA1F2]"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
@@ -546,8 +407,7 @@ export default function HeaderBar({
|
||||
href={OFFICIAL_LINKS.telegram}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-2 rounded-lg"
|
||||
style={{ color: '#848E9C', background: 'rgba(255, 255, 255, 0.05)' }}
|
||||
className="p-2 rounded-lg text-nofx-text-muted bg-white/5 hover:text-[#0088cc]"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
|
||||
@@ -558,10 +418,7 @@ export default function HeaderBar({
|
||||
{/* Language Toggle */}
|
||||
<div className="py-2">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span
|
||||
className="text-xs"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
<span className="text-xs text-nofx-text-muted">
|
||||
{t('language', language)}:
|
||||
</span>
|
||||
</div>
|
||||
@@ -571,8 +428,7 @@ export default function HeaderBar({
|
||||
onLanguageChange?.('zh')
|
||||
setMobileMenuOpen(false)
|
||||
}}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2 rounded transition-colors ${
|
||||
language === 'zh'
|
||||
className={`w-full flex items-center gap-3 px-3 py-2 rounded transition-colors ${language === 'zh'
|
||||
? 'bg-yellow-500 text-black'
|
||||
: 'text-gray-400 hover:text-white'
|
||||
}`}
|
||||
@@ -585,8 +441,7 @@ export default function HeaderBar({
|
||||
onLanguageChange?.('en')
|
||||
setMobileMenuOpen(false)
|
||||
}}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2 rounded transition-colors ${
|
||||
language === 'en'
|
||||
className={`w-full flex items-center gap-3 px-3 py-2 rounded transition-colors ${language === 'en'
|
||||
? 'bg-yellow-500 text-black'
|
||||
: 'text-gray-400 hover:text-white'
|
||||
}`}
|
||||
@@ -600,33 +455,17 @@ export default function HeaderBar({
|
||||
{/* User info and logout for mobile when logged in */}
|
||||
{isLoggedIn && user && (
|
||||
<div
|
||||
className="mt-4 pt-4"
|
||||
style={{ borderTop: '1px solid var(--panel-border)' }}
|
||||
>
|
||||
<div
|
||||
className="flex items-center gap-2 px-3 py-2 mb-2 rounded"
|
||||
style={{ background: 'var(--panel-bg)' }}
|
||||
>
|
||||
<div
|
||||
className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold"
|
||||
style={{
|
||||
background: 'var(--brand-yellow)',
|
||||
color: 'var(--brand-black)',
|
||||
}}
|
||||
className="mt-4 pt-4 border-t border-nofx-gold/20"
|
||||
>
|
||||
<div className="flex items-center gap-2 px-3 py-2 mb-2 rounded bg-nofx-bg-lighter">
|
||||
<div className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold bg-nofx-gold text-black">
|
||||
{user.email[0].toUpperCase()}
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className="text-xs"
|
||||
style={{ color: 'var(--text-secondary)' }}
|
||||
>
|
||||
<div className="text-xs text-nofx-text-muted">
|
||||
{t('loggedInAs', language)}
|
||||
</div>
|
||||
<div
|
||||
className="text-sm"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
<div className="text-sm text-nofx-text-muted">
|
||||
{user.email}
|
||||
</div>
|
||||
</div>
|
||||
@@ -637,11 +476,7 @@ export default function HeaderBar({
|
||||
onLogout()
|
||||
setMobileMenuOpen(false)
|
||||
}}
|
||||
className="w-full px-4 py-2 rounded text-sm font-semibold transition-colors text-center"
|
||||
style={{
|
||||
background: 'var(--binance-red-bg)',
|
||||
color: 'var(--binance-red)',
|
||||
}}
|
||||
className="w-full px-4 py-2 rounded text-sm font-semibold transition-colors text-center bg-nofx-danger/20 text-nofx-danger"
|
||||
>
|
||||
{t('exitLogin', language)}
|
||||
</button>
|
||||
@@ -656,11 +491,7 @@ export default function HeaderBar({
|
||||
<div className="space-y-2 mt-2">
|
||||
<a
|
||||
href="/login"
|
||||
className="block w-full px-4 py-2 rounded text-sm font-medium text-center transition-colors"
|
||||
style={{
|
||||
color: 'var(--brand-light-gray)',
|
||||
border: '1px solid var(--brand-light-gray)',
|
||||
}}
|
||||
className="block w-full px-4 py-2 rounded text-sm font-medium text-center transition-colors text-nofx-text-muted border border-nofx-text-muted hover:text-white hover:border-white"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
{t('signIn', language)}
|
||||
@@ -668,11 +499,7 @@ export default function HeaderBar({
|
||||
{registrationEnabled && (
|
||||
<a
|
||||
href="/register"
|
||||
className="block w-full px-4 py-2 rounded font-semibold text-sm text-center transition-colors"
|
||||
style={{
|
||||
background: 'var(--brand-yellow)',
|
||||
color: 'var(--brand-black)',
|
||||
}}
|
||||
className="block w-full px-4 py-2 rounded font-semibold text-sm text-center transition-colors bg-nofx-gold text-black hover:opacity-90"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
{t('signUp', language)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useAuth } from '../contexts/AuthContext'
|
||||
import { useLanguage } from '../contexts/LanguageContext'
|
||||
import { t } from '../i18n/translations'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
import { DeepVoidBackground } from './DeepVoidBackground'
|
||||
// import { Input } from './ui/input' // Removed unused import
|
||||
import { toast } from 'sonner'
|
||||
import { useSystemConfig } from '../hooks/useSystemConfig'
|
||||
@@ -102,13 +103,7 @@ export function LoginPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-zinc-300 font-mono relative overflow-hidden flex items-center justify-center py-12">
|
||||
{/* Background Effects */}
|
||||
<div className="absolute 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="absolute inset-0 bg-gradient-to-t from-black via-transparent to-transparent pointer-events-none"></div>
|
||||
|
||||
{/* Scanline Effect */}
|
||||
<div className="absolute 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 flex items-center justify-center py-12 font-mono" disableAnimation>
|
||||
|
||||
<div className="w-full max-w-md relative z-10 px-6">
|
||||
{/* Navigation - Top Bar (Mobile/Desktop Friendly) */}
|
||||
@@ -361,6 +356,6 @@ export function LoginPage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DeepVoidBackground>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { LogIn, UserPlus, X, AlertTriangle, Terminal } from 'lucide-react'
|
||||
import { DeepVoidBackground } from './DeepVoidBackground'
|
||||
import { useLanguage } from '../contexts/LanguageContext'
|
||||
|
||||
interface LoginRequiredOverlayProps {
|
||||
@@ -51,29 +52,31 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/90 backdrop-blur-sm"
|
||||
className="fixed inset-0 z-50"
|
||||
>
|
||||
<DeepVoidBackground
|
||||
className="w-full h-full bg-nofx-bg/95 backdrop-blur-md flex items-center justify-center p-4 text-nofx-text"
|
||||
disableAnimation
|
||||
onClick={onClose}
|
||||
>
|
||||
{/* Scanline Effect */}
|
||||
<div className="absolute 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>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95, y: 10 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: 10 }}
|
||||
transition={{ type: 'spring', damping: 20, stiffness: 300 }}
|
||||
className="relative max-w-md w-full overflow-hidden bg-black border border-nofx-gold/30 shadow-[0_0_50px_rgba(240,185,11,0.1)] rounded-sm group font-mono"
|
||||
className="relative max-w-md w-full overflow-hidden bg-nofx-bg border border-nofx-gold/30 shadow-neon rounded-sm group font-mono"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Terminal Window Header */}
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-zinc-900 border-b border-zinc-800">
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-nofx-bg-lighter border-b border-nofx-gold/20">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal size={12} className="text-nofx-gold" />
|
||||
<span className="text-[10px] text-zinc-500 uppercase tracking-wider">auth_protocol.exe</span>
|
||||
<span className="text-[10px] text-nofx-text-muted uppercase tracking-wider">auth_protocol.exe</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-zinc-600 hover:text-red-500 transition-colors"
|
||||
className="text-nofx-text-muted hover:text-nofx-danger transition-colors"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
@@ -89,7 +92,7 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
|
||||
<div className="flex justify-center mb-6">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-red-500/20 blur-xl animate-pulse"></div>
|
||||
<div className="bg-black border border-red-500/50 text-red-500 px-4 py-2 flex items-center gap-3 shadow-[0_0_15px_rgba(239,68,68,0.2)]">
|
||||
<div className="bg-nofx-bg border border-red-500/50 text-red-500 px-4 py-2 flex items-center gap-3 shadow-[0_0_15px_rgba(239,68,68,0.2)]">
|
||||
<AlertTriangle size={18} className="animate-pulse" />
|
||||
<span className="font-bold tracking-widest text-sm uppercase">{language === 'zh' ? '访问被拒绝' : 'ACCESS DENIED'}</span>
|
||||
</div>
|
||||
@@ -103,8 +106,8 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
|
||||
<p className="text-nofx-gold text-xs uppercase tracking-widest border-b border-nofx-gold/20 pb-4 inline-block">{t.subtitle}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-zinc-900/50 border-l-2 border-zinc-700 p-3 my-4">
|
||||
<p className="text-xs text-zinc-400 leading-relaxed font-mono">
|
||||
<div className="bg-nofx-bg-lighter border-l-2 border-nofx-gold/20 p-3 my-4">
|
||||
<p className="text-xs text-nofx-text-muted leading-relaxed font-mono">
|
||||
<span className="text-green-500 mr-2">$</span>
|
||||
{t.description}
|
||||
</p>
|
||||
@@ -112,7 +115,7 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{t.benefits.map((benefit, i) => (
|
||||
<div key={i} className="flex items-center gap-2 text-[10px] text-zinc-500 uppercase tracking-wide">
|
||||
<div key={i} className="flex items-center gap-2 text-[10px] text-nofx-text-muted uppercase tracking-wide">
|
||||
<span className="text-nofx-gold">✓</span> {benefit}
|
||||
</div>
|
||||
))}
|
||||
@@ -123,7 +126,7 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
|
||||
<div className="space-y-3">
|
||||
<a
|
||||
href="/login"
|
||||
className="flex items-center justify-center gap-2 w-full py-3 bg-nofx-gold text-black font-bold text-xs uppercase tracking-widest hover:bg-yellow-400 transition-all shadow-[0_0_15px_rgba(240,185,11,0.2)] hover:shadow-[0_0_25px_rgba(240,185,11,0.4)] group"
|
||||
className="flex items-center justify-center gap-2 w-full py-3 bg-nofx-gold text-black font-bold text-xs uppercase tracking-widest hover:bg-yellow-400 transition-all shadow-neon hover:shadow-[0_0_25px_rgba(240,185,11,0.4)] group"
|
||||
>
|
||||
<LogIn size={14} />
|
||||
<span>{t.login}</span>
|
||||
@@ -132,7 +135,7 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
|
||||
|
||||
<a
|
||||
href="/register"
|
||||
className="flex items-center justify-center gap-2 w-full py-3 bg-transparent border border-zinc-700 text-zinc-400 hover:text-white hover:border-zinc-500 font-bold text-xs uppercase tracking-widest transition-all hover:bg-zinc-900"
|
||||
className="flex items-center justify-center gap-2 w-full py-3 bg-transparent border border-nofx-gold/20 text-nofx-text-muted hover:text-white hover:border-nofx-gold font-bold text-xs uppercase tracking-widest transition-all hover:bg-nofx-gold/10"
|
||||
>
|
||||
<UserPlus size={14} />
|
||||
<span>{t.register}</span>
|
||||
@@ -142,7 +145,7 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
|
||||
<div className="mt-4 text-center">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-[10px] text-zinc-600 hover:text-red-500 uppercase tracking-widest hover:underline decoration-red-500/30"
|
||||
className="text-[10px] text-nofx-text-muted hover:text-nofx-danger uppercase tracking-widest hover:underline decoration-red-500/30"
|
||||
>
|
||||
[ {t.later} ]
|
||||
</button>
|
||||
@@ -156,6 +159,7 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
|
||||
<div className="absolute bottom-0 left-0 w-2 h-2 border-b border-l border-nofx-gold"></div>
|
||||
|
||||
</motion.div>
|
||||
</DeepVoidBackground>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { getSystemConfig } from '../lib/config'
|
||||
import { toast } from 'sonner'
|
||||
import { copyWithToast } from '../lib/clipboard'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
import { DeepVoidBackground } from './DeepVoidBackground'
|
||||
// import { Input } from './ui/input' // Removed unused import
|
||||
import PasswordChecklist from 'react-password-checklist'
|
||||
import { RegistrationDisabled } from './RegistrationDisabled'
|
||||
@@ -148,13 +149,7 @@ export function RegisterPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-zinc-300 font-mono relative overflow-hidden flex items-center justify-center py-12">
|
||||
{/* Background Effects */}
|
||||
<div className="absolute 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="absolute inset-0 bg-gradient-to-t from-black via-transparent to-transparent pointer-events-none"></div>
|
||||
|
||||
{/* Scanline Effect */}
|
||||
<div className="absolute 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 flex items-center justify-center py-12 font-mono" disableAnimation>
|
||||
|
||||
<div className="w-full max-w-lg relative z-10 px-6">
|
||||
{/* Navigation - Top Bar (Mobile/Desktop Friendly) */}
|
||||
@@ -469,6 +464,6 @@ export function RegisterPage() {
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</DeepVoidBackground>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export function WhitelistFullPage({ onBack }: WhitelistFullPageProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-white font-mono relative overflow-hidden flex items-center justify-center px-4">
|
||||
<div className="min-h-screen bg-nofx-bg-deeper text-white font-mono relative overflow-hidden flex items-center justify-center px-4">
|
||||
{/* 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>
|
||||
|
||||
@@ -56,14 +56,11 @@ export function FAQContent({
|
||||
return (
|
||||
<div className="space-y-12">
|
||||
{categories.map((category) => (
|
||||
<div key={category.id}>
|
||||
<div key={category.id} className="nofx-glass p-8 rounded-xl border border-white/5">
|
||||
{/* Category Header */}
|
||||
<div
|
||||
className="flex items-center gap-3 mb-6 pb-3"
|
||||
style={{ borderBottom: '2px solid #2B3139' }}
|
||||
>
|
||||
<category.icon className="w-7 h-7" style={{ color: '#F0B90B' }} />
|
||||
<h2 className="text-2xl font-bold" style={{ color: '#EAECEF' }}>
|
||||
<div className="flex items-center gap-3 mb-6 pb-3 border-b border-white/10">
|
||||
<category.icon className="w-7 h-7 text-nofx-gold" />
|
||||
<h2 className="text-2xl font-bold text-nofx-text-main">
|
||||
{t(category.titleKey, language)}
|
||||
</h2>
|
||||
</div>
|
||||
@@ -79,21 +76,12 @@ export function FAQContent({
|
||||
className="scroll-mt-24"
|
||||
>
|
||||
{/* Question */}
|
||||
<h3
|
||||
className="text-xl font-semibold mb-3"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
<h3 className="text-xl font-semibold mb-3 text-nofx-text-main">
|
||||
{t(item.questionKey, language)}
|
||||
</h3>
|
||||
|
||||
{/* Answer */}
|
||||
<div
|
||||
className="prose prose-invert max-w-none"
|
||||
style={{
|
||||
color: '#B7BDC6',
|
||||
lineHeight: '1.7',
|
||||
}}
|
||||
>
|
||||
<div className="prose prose-invert max-w-none text-nofx-text-muted leading-relaxed">
|
||||
{item.id === 'github-projects-tasks' ? (
|
||||
<div className="space-y-3">
|
||||
<div className="text-base">
|
||||
@@ -295,7 +283,7 @@ export function FAQContent({
|
||||
href="https://github.com/NoFxAiOS/nofx/blob/dev/CONTRIBUTING.md"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: '#F0B90B' }}
|
||||
className="text-nofx-gold hover:underline"
|
||||
>
|
||||
CONTRIBUTING.md
|
||||
</a>
|
||||
@@ -304,7 +292,7 @@ export function FAQContent({
|
||||
href="https://github.com/NoFxAiOS/nofx/blob/dev/.github/PR_TITLE_GUIDE.md"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: '#F0B90B' }}
|
||||
className="text-nofx-gold hover:underline"
|
||||
>
|
||||
PR_TITLE_GUIDE.md
|
||||
</a>
|
||||
@@ -383,16 +371,10 @@ export function FAQContent({
|
||||
)}
|
||||
</ol>
|
||||
|
||||
<div
|
||||
className="rounded p-3 mt-3"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.08)',
|
||||
border: '1px solid rgba(240, 185, 11, 0.25)',
|
||||
}}
|
||||
>
|
||||
<div className="rounded p-3 mt-3 bg-nofx-gold/10 border border-nofx-gold/25">
|
||||
{language === 'zh' ? (
|
||||
<div className="text-sm">
|
||||
<strong style={{ color: '#F0B90B' }}>提示:</strong>{' '}
|
||||
<strong className="text-nofx-gold">Note:</strong>{' '}
|
||||
我们为高质量贡献提供激励(Bounty/奖金、荣誉徽章与鸣谢、优先
|
||||
Review/合并与内测资格 等)。 详情可关注带
|
||||
<a
|
||||
@@ -448,7 +430,7 @@ export function FAQContent({
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="mt-6 h-px" style={{ background: '#2B3139' }} />
|
||||
<div className="mt-6 h-px bg-white/5" />
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useMemo } from 'react'
|
||||
import { HelpCircle } from 'lucide-react'
|
||||
import { Container } from '../Container'
|
||||
import { DeepVoidBackground } from '../DeepVoidBackground'
|
||||
import { t, type Language } from '../../i18n/translations'
|
||||
import { FAQSearchBar } from './FAQSearchBar'
|
||||
import { FAQSidebar } from './FAQSidebar'
|
||||
@@ -58,24 +58,19 @@ export function FAQLayout({ language }: FAQLayoutProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="py-6 pt-24">
|
||||
<DeepVoidBackground className="py-6 pt-24" disableAnimation>
|
||||
<div className="w-full px-4 md:px-8">
|
||||
{/* Page Header */}
|
||||
<div className="text-center mb-12">
|
||||
<div className="flex items-center justify-center gap-3 mb-4">
|
||||
<div
|
||||
className="w-16 h-16 rounded-full flex items-center justify-center"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
|
||||
boxShadow: '0 8px 24px rgba(240, 185, 11, 0.4)',
|
||||
}}
|
||||
>
|
||||
<HelpCircle className="w-8 h-8" style={{ color: '#0B0E11' }} />
|
||||
<div className="w-16 h-16 rounded-full flex items-center justify-center bg-gradient-to-br from-nofx-gold to-[#FCD535] shadow-[0_8px_24px_rgba(240,185,11,0.4)]">
|
||||
<HelpCircle className="w-8 h-8 text-[#0B0E11]" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold mb-4" style={{ color: '#EAECEF' }}>
|
||||
<h1 className="text-4xl font-bold mb-4 text-nofx-text-main">
|
||||
{t('faqTitle', language)}
|
||||
</h1>
|
||||
<p className="text-lg mb-8" style={{ color: '#848E9C' }}>
|
||||
<p className="text-lg mb-8 text-nofx-text-muted">
|
||||
{t('faqSubtitle', language)}
|
||||
</p>
|
||||
|
||||
@@ -177,6 +172,7 @@ export function FAQLayout({ language }: FAQLayoutProps) {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</DeepVoidBackground>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,36 +12,21 @@ export function FAQSearchBar({
|
||||
placeholder = 'Search FAQ...',
|
||||
}: FAQSearchBarProps) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="relative group">
|
||||
<Search
|
||||
className="absolute left-4 top-1/2 transform -translate-y-1/2 w-5 h-5"
|
||||
style={{ color: '#848E9C' }}
|
||||
className="absolute left-4 top-1/2 transform -translate-y-1/2 w-5 h-5 text-nofx-text-muted group-focus-within:text-nofx-gold transition-colors"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="w-full pl-12 pr-12 py-3 rounded-lg text-base transition-all focus:outline-none focus:ring-2"
|
||||
style={{
|
||||
background: '#1E2329',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = '#F0B90B'
|
||||
e.target.style.boxShadow = '0 0 0 3px rgba(240, 185, 11, 0.1)'
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = '#2B3139'
|
||||
e.target.style.boxShadow = 'none'
|
||||
}}
|
||||
className="w-full pl-12 pr-12 py-3 rounded-lg text-base transition-all focus:outline-none bg-black/40 border border-white/10 text-nofx-text-main placeholder-nofx-text-muted/50 focus:border-nofx-gold/50 focus:ring-1 focus:ring-nofx-gold/20 hover:border-nofx-gold/30 font-mono"
|
||||
/>
|
||||
{searchTerm && (
|
||||
<button
|
||||
onClick={() => onSearchChange('')}
|
||||
className="absolute right-4 top-1/2 transform -translate-y-1/2 hover:opacity-70 transition-opacity"
|
||||
style={{ color: '#848E9C' }}
|
||||
className="absolute right-4 top-1/2 transform -translate-y-1/2 text-nofx-text-muted hover:text-white transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
@@ -24,14 +24,11 @@ export function FAQSidebar({
|
||||
>
|
||||
<div className="space-y-6">
|
||||
{categories.map((category) => (
|
||||
<div key={category.id}>
|
||||
<div key={category.id} className="nofx-glass p-4 rounded-xl border border-white/5">
|
||||
{/* Category Title */}
|
||||
<div className="flex items-center gap-2 mb-3 px-3">
|
||||
<category.icon className="w-5 h-5" style={{ color: '#F0B90B' }} />
|
||||
<h3
|
||||
className="text-sm font-bold uppercase tracking-wide"
|
||||
style={{ color: '#F0B90B' }}
|
||||
>
|
||||
<category.icon className="w-5 h-5 text-nofx-gold" />
|
||||
<h3 className="text-sm font-bold uppercase tracking-wide text-nofx-gold">
|
||||
{t(category.titleKey, language)}
|
||||
</h3>
|
||||
</div>
|
||||
@@ -44,30 +41,10 @@ export function FAQSidebar({
|
||||
<li key={item.id}>
|
||||
<button
|
||||
onClick={() => onItemClick(category.id, item.id)}
|
||||
className="w-full text-left px-3 py-2 rounded-lg text-sm transition-all"
|
||||
style={{
|
||||
background: isActive
|
||||
? 'rgba(240, 185, 11, 0.1)'
|
||||
: 'transparent',
|
||||
color: isActive ? '#F0B90B' : '#848E9C',
|
||||
borderLeft: isActive
|
||||
? '3px solid #F0B90B'
|
||||
: '3px solid transparent',
|
||||
paddingLeft: isActive ? '9px' : '12px',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isActive) {
|
||||
e.currentTarget.style.background =
|
||||
'rgba(240, 185, 11, 0.05)'
|
||||
e.currentTarget.style.color = '#EAECEF'
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isActive) {
|
||||
e.currentTarget.style.background = 'transparent'
|
||||
e.currentTarget.style.color = '#848E9C'
|
||||
}
|
||||
}}
|
||||
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-all border-l-[3px] ${isActive
|
||||
? 'bg-nofx-gold/10 text-nofx-gold border-nofx-gold pl-[9px]'
|
||||
: 'bg-transparent text-nofx-text-muted border-transparent pl-3 hover:bg-nofx-gold/5 hover:text-nofx-text-main'
|
||||
}`}
|
||||
>
|
||||
{t(item.questionKey, language)}
|
||||
</button>
|
||||
|
||||
@@ -143,12 +143,7 @@ export function CoinSourceEditor({
|
||||
// NofxOS badge component
|
||||
const NofxOSBadge = () => (
|
||||
<span
|
||||
className="text-[9px] px-1.5 py-0.5 rounded font-medium"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, rgba(99, 102, 241, 0.2), rgba(168, 85, 247, 0.2))',
|
||||
color: '#a855f7',
|
||||
border: '1px solid rgba(139, 92, 246, 0.3)'
|
||||
}}
|
||||
className="text-[9px] px-1.5 py-0.5 rounded font-medium bg-purple-500/20 text-purple-400 border border-purple-500/30"
|
||||
>
|
||||
NofxOS
|
||||
</span>
|
||||
@@ -158,7 +153,7 @@ export function CoinSourceEditor({
|
||||
<div className="space-y-6">
|
||||
{/* Source Type Selector */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-3" style={{ color: '#EAECEF' }}>
|
||||
<label className="block text-sm font-medium mb-3 text-nofx-text">
|
||||
{t('sourceType')}
|
||||
</label>
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
@@ -170,24 +165,16 @@ export function CoinSourceEditor({
|
||||
onChange({ ...config, source_type: value as CoinSourceConfig['source_type'] })
|
||||
}
|
||||
disabled={disabled}
|
||||
className={`p-4 rounded-lg border transition-all ${
|
||||
config.source_type === value
|
||||
? 'ring-2 ring-yellow-500'
|
||||
: 'hover:bg-white/5'
|
||||
}`}
|
||||
style={{
|
||||
background:
|
||||
config.source_type === value
|
||||
? 'rgba(240, 185, 11, 0.1)'
|
||||
: '#0B0E11',
|
||||
borderColor: '#2B3139',
|
||||
}}
|
||||
className={`p-4 rounded-lg border transition-all ${config.source_type === value
|
||||
? 'ring-2 ring-nofx-gold bg-nofx-gold/10'
|
||||
: 'hover:bg-white/5 bg-nofx-bg'
|
||||
} border-nofx-gold/20`}
|
||||
>
|
||||
<Icon className="w-6 h-6 mx-auto mb-2" style={{ color }} />
|
||||
<div className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
||||
<div className="text-sm font-medium text-nofx-text">
|
||||
{t(value)}
|
||||
</div>
|
||||
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
|
||||
<div className="text-xs mt-1 text-nofx-text-muted">
|
||||
{t(`${value}Desc`)}
|
||||
</div>
|
||||
</button>
|
||||
@@ -198,15 +185,14 @@ export function CoinSourceEditor({
|
||||
{/* Static Coins */}
|
||||
{(config.source_type === 'static' || config.source_type === 'mixed') && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-3" style={{ color: '#EAECEF' }}>
|
||||
<label className="block text-sm font-medium mb-3 text-nofx-text">
|
||||
{t('staticCoins')}
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2 mb-3">
|
||||
{(config.static_coins || []).map((coin) => (
|
||||
<span
|
||||
key={coin}
|
||||
className="flex items-center gap-1 px-3 py-1.5 rounded-full text-sm"
|
||||
style={{ background: '#2B3139', color: '#EAECEF' }}
|
||||
className="flex items-center gap-1 px-3 py-1.5 rounded-full text-sm bg-nofx-bg-lighter text-nofx-text"
|
||||
>
|
||||
{coin}
|
||||
{!disabled && (
|
||||
@@ -228,17 +214,11 @@ export function CoinSourceEditor({
|
||||
onChange={(e) => setNewCoin(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleAddCoin()}
|
||||
placeholder="BTC, ETH, SOL..."
|
||||
className="flex-1 px-4 py-2 rounded-lg"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
className="flex-1 px-4 py-2 rounded-lg bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||
/>
|
||||
<button
|
||||
onClick={handleAddCoin}
|
||||
className="px-4 py-2 rounded-lg flex items-center gap-2 transition-colors"
|
||||
style={{ background: '#F0B90B', color: '#0B0E11' }}
|
||||
className="px-4 py-2 rounded-lg flex items-center gap-2 transition-colors bg-nofx-gold text-black hover:bg-yellow-500"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
{t('addCoin')}
|
||||
@@ -251,20 +231,19 @@ export function CoinSourceEditor({
|
||||
{/* Excluded Coins */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Ban className="w-4 h-4" style={{ color: '#F6465D' }} />
|
||||
<label className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
||||
<Ban className="w-4 h-4 text-nofx-danger" />
|
||||
<label className="text-sm font-medium text-nofx-text">
|
||||
{t('excludedCoins')}
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-xs mb-3" style={{ color: '#848E9C' }}>
|
||||
<p className="text-xs mb-3 text-nofx-text-muted">
|
||||
{t('excludedCoinsDesc')}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 mb-3">
|
||||
{(config.excluded_coins || []).map((coin) => (
|
||||
<span
|
||||
key={coin}
|
||||
className="flex items-center gap-1 px-3 py-1.5 rounded-full text-sm"
|
||||
style={{ background: 'rgba(246, 70, 93, 0.15)', color: '#F6465D' }}
|
||||
className="flex items-center gap-1 px-3 py-1.5 rounded-full text-sm bg-nofx-danger/15 text-nofx-danger"
|
||||
>
|
||||
{coin}
|
||||
{!disabled && (
|
||||
@@ -278,7 +257,7 @@ export function CoinSourceEditor({
|
||||
</span>
|
||||
))}
|
||||
{(config.excluded_coins || []).length === 0 && (
|
||||
<span className="text-xs italic" style={{ color: '#5E6673' }}>
|
||||
<span className="text-xs italic text-nofx-text-muted">
|
||||
{language === 'zh' ? '无' : 'None'}
|
||||
</span>
|
||||
)}
|
||||
@@ -291,17 +270,11 @@ export function CoinSourceEditor({
|
||||
onChange={(e) => setNewExcludedCoin(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleAddExcludedCoin()}
|
||||
placeholder="BTC, ETH, DOGE..."
|
||||
className="flex-1 px-4 py-2 rounded-lg text-sm"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
className="flex-1 px-4 py-2 rounded-lg text-sm bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||
/>
|
||||
<button
|
||||
onClick={handleAddExcludedCoin}
|
||||
className="px-4 py-2 rounded-lg flex items-center gap-2 transition-colors text-sm"
|
||||
style={{ background: '#F6465D', color: '#EAECEF' }}
|
||||
className="px-4 py-2 rounded-lg flex items-center gap-2 transition-colors text-sm bg-nofx-danger text-white hover:bg-red-600"
|
||||
>
|
||||
<Ban className="w-4 h-4" />
|
||||
{t('addExcludedCoin')}
|
||||
@@ -313,16 +286,12 @@ export function CoinSourceEditor({
|
||||
{/* AI500 Options */}
|
||||
{(config.source_type === 'ai500' || config.source_type === 'mixed') && (
|
||||
<div
|
||||
className="p-4 rounded-lg"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.05)',
|
||||
border: '1px solid rgba(240, 185, 11, 0.2)',
|
||||
}}
|
||||
className="p-4 rounded-lg bg-nofx-gold/5 border border-nofx-gold/20"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
||||
<Zap className="w-4 h-4 text-nofx-gold" />
|
||||
<span className="text-sm font-medium text-nofx-text">
|
||||
AI500 {t('dataSourceConfig')}
|
||||
</span>
|
||||
<NofxOSBadge />
|
||||
@@ -338,14 +307,14 @@ export function CoinSourceEditor({
|
||||
!disabled && onChange({ ...config, use_ai500: e.target.checked })
|
||||
}
|
||||
disabled={disabled}
|
||||
className="w-5 h-5 rounded accent-yellow-500"
|
||||
className="w-5 h-5 rounded accent-nofx-gold"
|
||||
/>
|
||||
<span style={{ color: '#EAECEF' }}>{t('useAI500')}</span>
|
||||
<span className="text-nofx-text">{t('useAI500')}</span>
|
||||
</label>
|
||||
|
||||
{config.use_ai500 && (
|
||||
<div className="flex items-center gap-3 pl-8">
|
||||
<span className="text-sm" style={{ color: '#848E9C' }}>
|
||||
<span className="text-sm text-nofx-text-muted">
|
||||
{t('ai500Limit')}:
|
||||
</span>
|
||||
<select
|
||||
@@ -355,12 +324,7 @@ export function CoinSourceEditor({
|
||||
onChange({ ...config, ai500_limit: parseInt(e.target.value) || 10 })
|
||||
}
|
||||
disabled={disabled}
|
||||
className="px-3 py-1.5 rounded"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
className="px-3 py-1.5 rounded bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||
>
|
||||
{[5, 10, 15, 20, 30, 50].map(n => (
|
||||
<option key={n} value={n}>{n}</option>
|
||||
@@ -369,7 +333,7 @@ export function CoinSourceEditor({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-xs pl-8" style={{ color: '#5E6673' }}>
|
||||
<p className="text-xs pl-8 text-nofx-text-muted">
|
||||
{t('nofxosNote')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -379,16 +343,12 @@ export function CoinSourceEditor({
|
||||
{/* OI Top Options */}
|
||||
{(config.source_type === 'oi_top' || config.source_type === 'mixed') && (
|
||||
<div
|
||||
className="p-4 rounded-lg"
|
||||
style={{
|
||||
background: 'rgba(14, 203, 129, 0.05)',
|
||||
border: '1px solid rgba(14, 203, 129, 0.2)',
|
||||
}}
|
||||
className="p-4 rounded-lg bg-nofx-success/5 border border-nofx-success/20"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingUp className="w-4 h-4" style={{ color: '#0ECB81' }} />
|
||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
||||
<TrendingUp className="w-4 h-4 text-nofx-success" />
|
||||
<span className="text-sm font-medium text-nofx-text">
|
||||
OI Top {t('dataSourceConfig')}
|
||||
</span>
|
||||
<NofxOSBadge />
|
||||
@@ -404,14 +364,14 @@ export function CoinSourceEditor({
|
||||
!disabled && onChange({ ...config, use_oi_top: e.target.checked })
|
||||
}
|
||||
disabled={disabled}
|
||||
className="w-5 h-5 rounded accent-green-500"
|
||||
className="w-5 h-5 rounded accent-nofx-success"
|
||||
/>
|
||||
<span style={{ color: '#EAECEF' }}>{t('useOITop')}</span>
|
||||
<span className="text-nofx-text">{t('useOITop')}</span>
|
||||
</label>
|
||||
|
||||
{config.use_oi_top && (
|
||||
<div className="flex items-center gap-3 pl-8">
|
||||
<span className="text-sm" style={{ color: '#848E9C' }}>
|
||||
<span className="text-sm text-nofx-text-muted">
|
||||
{t('oiTopLimit')}:
|
||||
</span>
|
||||
<select
|
||||
@@ -421,12 +381,7 @@ export function CoinSourceEditor({
|
||||
onChange({ ...config, oi_top_limit: parseInt(e.target.value) || 20 })
|
||||
}
|
||||
disabled={disabled}
|
||||
className="px-3 py-1.5 rounded"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
className="px-3 py-1.5 rounded bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||
>
|
||||
{[5, 10, 15, 20, 30, 50].map(n => (
|
||||
<option key={n} value={n}>{n}</option>
|
||||
@@ -435,7 +390,7 @@ export function CoinSourceEditor({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-xs pl-8" style={{ color: '#5E6673' }}>
|
||||
<p className="text-xs pl-8 text-nofx-text-muted">
|
||||
{t('nofxosNote')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -439,11 +439,15 @@ button:disabled {
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
/* Premium Input Styles */
|
||||
/* Premium Input Styles */
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background-color: var(--panel-bg);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--panel-border);
|
||||
}
|
||||
|
||||
input:focus,
|
||||
@@ -452,6 +456,35 @@ textarea:focus {
|
||||
border-color: var(--binance-yellow);
|
||||
box-shadow: 0 0 0 2px rgba(252, 213, 53, 0.15);
|
||||
outline: none;
|
||||
background-color: var(--panel-bg-hover);
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
/* Specific Premium Input Utility */
|
||||
.premium-input {
|
||||
background: rgba(11, 14, 17, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-primary);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.premium-input:hover {
|
||||
border-color: rgba(240, 185, 11, 0.3);
|
||||
background: rgba(11, 14, 17, 0.8);
|
||||
}
|
||||
|
||||
.premium-input:focus {
|
||||
border-color: var(--binance-yellow);
|
||||
box-shadow: 0 0 0 1px rgba(240, 185, 11, 0.2), 0 0 15px rgba(240, 185, 11, 0.1);
|
||||
background: rgba(11, 14, 17, 0.9);
|
||||
}
|
||||
|
||||
/* Binance Card - Premium Polish */
|
||||
|
||||
@@ -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,13 +226,10 @@ 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">
|
||||
@@ -511,5 +509,6 @@ export function StrategyMarketPage() {
|
||||
|
||||
</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'
|
||||
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'
|
||||
}`}
|
||||
style={{
|
||||
background: selectedStrategy?.id === strategy.id ? 'rgba(240, 185, 11, 0.1)' : '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'
|
||||
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'
|
||||
}`}
|
||||
style={{
|
||||
borderColor: activeRightTab === 'prompt' ? '#a855f7' : 'transparent',
|
||||
color: activeRightTab === 'prompt' ? '#a855f7' : '#848E9C',
|
||||
}}
|
||||
>
|
||||
<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'
|
||||
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'
|
||||
}`}
|
||||
style={{
|
||||
borderColor: activeRightTab === 'test' ? '#22c55e' : 'transparent',
|
||||
color: activeRightTab === 'test' ? '#22c55e' : '#848E9C',
|
||||
}}
|
||||
>
|
||||
<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