feat: add DeepVoidBackground and update UI theme across pages

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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,87 +204,89 @@ 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 */}
{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"
>
<span>{chartSymbol}</span>
<ChevronDown className={`w-3 h-3 text-gray-400 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" />
<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"
autoFocus
/>
<div className="shrink-0 relative" ref={dropdownRef}>
{marketConfig.hasDropdown ? (
<>
<button
onClick={() => setShowDropdown(!showDropdown)}
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-nofx-text-muted transition-transform ${showDropdown ? 'rotate-180' : ''}`} />
</button>
{showDropdown && (
<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 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-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.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-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'}`}
>
<span>{s.symbol}</span>
<span className="text-[9px] opacity-40">{s.name}</span>
</button>
))}
</div>
)
})}
</div>
</div>
<div className="overflow-y-auto max-h-52">
{['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>
{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'}`}
>
{s.symbol}
</button>
))}
</div>
)
})}
</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}
</button>
))}
</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}

View File

@@ -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,83 +45,78 @@ export function CompetitionPage() {
if (!competition) {
return (
<div className="space-y-6">
<div className="binance-card p-8 animate-pulse">
<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>
<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="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="h-8 w-64 bg-white/5 rounded"></div>
<div className="h-4 w-48 bg-white/5 rounded"></div>
</div>
<div className="h-12 w-32 bg-white/5 rounded"></div>
</div>
</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="h-20 w-full bg-white/5 rounded"></div>
<div className="h-20 w-full bg-white/5 rounded"></div>
</div>
</div>
<div className="skeleton h-12 w-32"></div>
</div>
</div>
<div className="binance-card p-6">
<div className="skeleton h-6 w-40 mb-4"></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>
</div>
</div>
</DeepVoidBackground>
)
}
// 如果有数据返回但没有交易员,显示空状态
if (!competition.traders || competition.traders.length === 0) {
return (
<div className="space-y-5 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)',
}}
>
<Trophy
className="w-6 h-6 md:w-7 md:h-7"
style={{ color: '#000' }}
/>
</div>
<div>
<h1
className="text-xl md:text-2xl font-bold flex items-center gap-2"
style={{ color: '#EAECEF' }}
<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 bg-black/60 border border-nofx-gold/30 shadow-[0_0_15px_rgba(240,185,11,0.2)]"
>
{t('aiCompetition', language)}
<span
className="text-xs font-normal px-2 py-1 rounded"
style={{
background: 'rgba(240, 185, 11, 0.15)',
color: '#F0B90B',
}}
<Trophy
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 text-white"
>
0 {t('traders', language)}
</span>
</h1>
<p className="text-xs" style={{ color: '#848E9C' }}>
{t('liveBattle', language)}
</p>
{t('aiCompetition', language)}
<span
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 text-zinc-400">
{t('liveBattle', language)}
</p>
</div>
</div>
</div>
</div>
{/* Empty State */}
<div className="binance-card p-8 text-center">
<Trophy
className="w-16 h-16 mx-auto mb-4 opacity-40"
style={{ color: '#848E9C' }}
/>
<h3 className="text-lg font-bold mb-2" style={{ color: '#EAECEF' }}>
{t('noTraders', language)}
</h3>
<p className="text-sm" style={{ color: '#848E9C' }}>
{t('createFirstTrader', language)}
</p>
{/* Empty State */}
<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 text-zinc-700"
/>
<h3 className="text-lg font-bold mb-2 text-white">
{t('noTraders', language)}
</h3>
<p className="text-sm text-zinc-400">
{t('createFirstTrader', language)}
</p>
</div>
</div>
</div>
</DeepVoidBackground>
)
}
@@ -133,375 +129,358 @@ export function CompetitionPage() {
const leader = sortedTraders[0]
return (
<div className="space-y-5 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)',
}}
>
<Trophy
className="w-6 h-6 md:w-7 md:h-7"
style={{ color: '#000' }}
/>
</div>
<div>
<h1
className="text-xl md:text-2xl font-bold flex items-center gap-2"
style={{ color: '#EAECEF' }}
<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 bg-black/60 border border-nofx-gold/30 shadow-[0_0_15px_rgba(240,185,11,0.2)]"
>
{t('aiCompetition', language)}
<span
className="text-xs font-normal px-2 py-1 rounded"
style={{
background: 'rgba(240, 185, 11, 0.15)',
color: '#F0B90B',
}}
<Trophy
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 text-white"
>
{competition.count} {t('traders', language)}
</span>
</h1>
<p className="text-xs" style={{ color: '#848E9C' }}>
{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' }}>
{t('leader', language)}
</div>
<div
className="text-base md:text-lg font-bold"
style={{ color: '#F0B90B' }}
>
{leader?.trader_name}
</div>
<div
className="text-sm font-semibold"
style={{
color: (leader?.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D',
}}
>
{(leader?.total_pnl ?? 0) >= 0 ? '+' : ''}
{leader?.total_pnl_pct?.toFixed(2) || '0.00'}%
</div>
</div>
</div>
{/* Left/Right Split: Performance Chart + Leaderboard */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
{/* Left: Performance Comparison Chart */}
<div
className="binance-card p-5 animate-slide-in"
style={{ animationDelay: '0.1s' }}
>
<div className="flex items-center justify-between mb-4">
<h2
className="text-lg font-bold flex items-center gap-2"
style={{ color: '#EAECEF' }}
>
{t('performanceComparison', language)}
</h2>
<div className="text-xs" style={{ color: '#848E9C' }}>
{t('realTimePnL', language)}
{t('aiCompetition', language)}
<span
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 text-zinc-400">
{t('liveBattle', language)}
</p>
</div>
</div>
<ComparisonChart traders={sortedTraders.slice(0, 10)} />
</div>
{/* Right: Leaderboard */}
<div
className="binance-card p-5 animate-slide-in"
style={{ animationDelay: '0.1s' }}
>
<div className="flex items-center justify-between mb-4">
<h2
className="text-lg font-bold flex items-center gap-2"
style={{ color: '#EAECEF' }}
>
{t('leaderboard', language)}
</h2>
<div className="text-left md:text-right w-full md:w-auto">
<div className="text-xs mb-1 text-zinc-400">
{t('leader', language)}
</div>
<div
className="text-xs px-2 py-1 rounded"
className="text-base md:text-lg font-bold text-nofx-gold"
>
{leader?.trader_name}
</div>
<div
className="text-sm font-semibold"
style={{
background: 'rgba(240, 185, 11, 0.1)',
color: '#F0B90B',
border: '1px solid rgba(240, 185, 11, 0.2)',
color: (leader?.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D',
}}
>
{t('live', language)}
{(leader?.total_pnl ?? 0) >= 0 ? '+' : ''}
{leader?.total_pnl_pct?.toFixed(2) || '0.00'}%
</div>
</div>
<div className="space-y-2">
{sortedTraders.map((trader, index) => {
const isLeader = index === 0
const traderColor = getTraderColor(
sortedTraders,
trader.trader_id
)
</div>
return (
<div
key={trader.trader_id}
onClick={() => handleTraderClick(trader.trader_id)}
className="rounded p-3 transition-all duration-300 hover:translate-y-[-1px] cursor-pointer hover:shadow-lg"
style={{
background: isLeader
? 'linear-gradient(135deg, rgba(240, 185, 11, 0.08) 0%, #0B0E11 100%)'
: '#0B0E11',
border: `1px solid ${isLeader ? 'rgba(240, 185, 11, 0.4)' : '#2B3139'}`,
boxShadow: isLeader
? '0 3px 15px rgba(240, 185, 11, 0.12), 0 0 0 1px rgba(240, 185, 11, 0.15)'
: '0 1px 4px rgba(0, 0, 0, 0.3)',
}}
>
<div className="flex items-center justify-between">
{/* Rank & Avatar & Name */}
<div className="flex items-center gap-3">
{/* Rank Badge */}
<div
className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold"
style={{
background: index === 0
? 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)'
: index === 1
? 'linear-gradient(135deg, #C0C0C0 0%, #E8E8E8 100%)'
: index === 2
? 'linear-gradient(135deg, #CD7F32 0%, #E8A64C 100%)'
: '#2B3139',
color: index < 3 ? '#000' : '#848E9C',
}}
>
{index + 1}
</div>
{/* Punk Avatar */}
<PunkAvatar
seed={getTraderAvatar(trader.trader_id, trader.trader_name)}
size={36}
className="rounded-lg"
/>
<div>
<div
className="font-bold text-sm"
style={{ color: '#EAECEF' }}
>
{trader.trader_name}
</div>
<div
className="text-xs mono font-semibold"
style={{ color: traderColor }}
>
{trader.ai_model.toUpperCase()} +{' '}
{trader.exchange.toUpperCase()}
</div>
</div>
</div>
{/* Left/Right Split: Performance Chart + Leaderboard */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Left: Performance Comparison Chart */}
<div
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-6">
<h2
className="text-lg font-bold flex items-center gap-2 text-white"
>
{t('performanceComparison', language)}
</h2>
<div className="text-xs text-zinc-400">
{t('realTimePnL', language)}
</div>
</div>
<ComparisonChart traders={sortedTraders.slice(0, 10)} />
</div>
{/* Stats */}
<div className="flex items-center gap-2 md:gap-3 flex-wrap md:flex-nowrap">
{/* Total Equity */}
<div className="text-right">
<div className="text-xs" style={{ color: '#848E9C' }}>
{t('equity', language)}
</div>
<div
className="text-xs md:text-sm font-bold mono"
style={{ color: '#EAECEF' }}
>
{trader.total_equity?.toFixed(2) || '0.00'}
</div>
</div>
{/* Right: Leaderboard */}
<div
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-6">
<h2
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 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>
</div>
<div className="space-y-2">
{sortedTraders.map((trader, index) => {
const isLeader = index === 0
const traderColor = getTraderColor(
sortedTraders,
trader.trader_id
)
{/* P&L */}
<div className="text-right min-w-[70px] md:min-w-[90px]">
<div className="text-xs" style={{ color: '#848E9C' }}>
{t('pnl', language)}
</div>
return (
<div
key={trader.trader_id}
onClick={() => handleTraderClick(trader.trader_id)}
className="rounded p-3 transition-all duration-300 hover:translate-y-[-1px] cursor-pointer hover:shadow-lg"
style={{
background: isLeader
? 'linear-gradient(135deg, rgba(240, 185, 11, 0.08) 0%, #0B0E11 100%)'
: '#0B0E11',
border: `1px solid ${isLeader ? 'rgba(240, 185, 11, 0.4)' : '#2B3139'}`,
boxShadow: isLeader
? '0 3px 15px rgba(240, 185, 11, 0.12), 0 0 0 1px rgba(240, 185, 11, 0.15)'
: '0 1px 4px rgba(0, 0, 0, 0.3)',
}}
>
<div className="flex items-center justify-between">
{/* Rank & Avatar & Name */}
<div className="flex items-center gap-3">
{/* Rank Badge */}
<div
className="text-base md:text-lg font-bold mono"
className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold"
style={{
color:
(trader.total_pnl ?? 0) >= 0
? '#0ECB81'
: '#F6465D',
background: index === 0
? 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)'
: index === 1
? 'linear-gradient(135deg, #C0C0C0 0%, #E8E8E8 100%)'
: index === 2
? 'linear-gradient(135deg, #CD7F32 0%, #E8A64C 100%)'
: '#2B3139',
color: index < 3 ? '#000' : '#848E9C',
}}
>
{(trader.total_pnl ?? 0) >= 0 ? '+' : ''}
{trader.total_pnl_pct?.toFixed(2) || '0.00'}%
{index + 1}
</div>
<div
className="text-xs mono"
style={{ color: '#848E9C' }}
>
{(trader.total_pnl ?? 0) >= 0 ? '+' : ''}
{trader.total_pnl?.toFixed(2) || '0.00'}
{/* Punk Avatar */}
<PunkAvatar
seed={getTraderAvatar(trader.trader_id, trader.trader_name)}
size={36}
className="rounded-lg"
/>
<div>
<div
className="font-bold text-sm"
style={{ color: '#EAECEF' }}
>
{trader.trader_name}
</div>
<div
className="text-xs mono font-semibold"
style={{ color: traderColor }}
>
{trader.ai_model.toUpperCase()} +{' '}
{trader.exchange.toUpperCase()}
</div>
</div>
</div>
{/* Positions */}
<div className="text-right">
<div className="text-xs" style={{ color: '#848E9C' }}>
{t('pos', language)}
{/* Stats */}
<div className="flex items-center gap-2 md:gap-3 flex-wrap md:flex-nowrap">
{/* Total Equity */}
<div className="text-right">
<div className="text-xs" style={{ color: '#848E9C' }}>
{t('equity', language)}
</div>
<div
className="text-xs md:text-sm font-bold mono"
style={{ color: '#EAECEF' }}
>
{trader.total_equity?.toFixed(2) || '0.00'}
</div>
</div>
<div
className="text-xs md:text-sm font-bold mono"
style={{ color: '#EAECEF' }}
>
{trader.position_count}
</div>
<div className="text-xs" style={{ color: '#848E9C' }}>
{trader.margin_used_pct.toFixed(1)}%
</div>
</div>
{/* Status */}
<div>
<div
className="px-2 py-1 rounded text-xs font-bold"
style={
trader.is_running
? {
{/* P&L */}
<div className="text-right min-w-[70px] md:min-w-[90px]">
<div className="text-xs" style={{ color: '#848E9C' }}>
{t('pnl', language)}
</div>
<div
className="text-base md:text-lg font-bold mono"
style={{
color:
(trader.total_pnl ?? 0) >= 0
? '#0ECB81'
: '#F6465D',
}}
>
{(trader.total_pnl ?? 0) >= 0 ? '+' : ''}
{trader.total_pnl_pct?.toFixed(2) || '0.00'}%
</div>
<div
className="text-xs mono"
style={{ color: '#848E9C' }}
>
{(trader.total_pnl ?? 0) >= 0 ? '+' : ''}
{trader.total_pnl?.toFixed(2) || '0.00'}
</div>
</div>
{/* Positions */}
<div className="text-right">
<div className="text-xs" style={{ color: '#848E9C' }}>
{t('pos', language)}
</div>
<div
className="text-xs md:text-sm font-bold mono"
style={{ color: '#EAECEF' }}
>
{trader.position_count}
</div>
<div className="text-xs" style={{ color: '#848E9C' }}>
{trader.margin_used_pct.toFixed(1)}%
</div>
</div>
{/* Status */}
<div>
<div
className="px-2 py-1 rounded text-xs font-bold"
style={
trader.is_running
? {
background: 'rgba(14, 203, 129, 0.1)',
color: '#0ECB81',
}
: {
: {
background: 'rgba(246, 70, 93, 0.1)',
color: '#F6465D',
}
}
>
{trader.is_running ? '●' : '○'}
}
>
{trader.is_running ? '●' : '○'}
</div>
</div>
</div>
</div>
</div>
</div>
)
})}
)
})}
</div>
</div>
</div>
</div>
{/* Head-to-Head Stats */}
{competition.traders.length === 2 && (
<div
className="binance-card p-5 animate-slide-in"
style={{ animationDelay: '0.3s' }}
>
<h2
className="text-lg font-bold mb-4 flex items-center gap-2"
style={{ color: '#EAECEF' }}
{/* Head-to-Head Stats */}
{competition.traders.length === 2 && (
<div
className="bg-black/40 border border-white/10 rounded-xl p-6 backdrop-blur-md animate-slide-in"
style={{ animationDelay: '0.3s' }}
>
{t('headToHead', language)}
</h2>
<div className="grid grid-cols-2 gap-4">
{sortedTraders.map((trader, index) => {
const isWinning = index === 0
const opponent = sortedTraders[1 - index]
<h2
className="text-lg font-bold mb-6 flex items-center gap-2 text-white"
>
{t('headToHead', language)}
</h2>
<div className="grid grid-cols-2 gap-4">
{sortedTraders.map((trader, index) => {
const isWinning = index === 0
const opponent = sortedTraders[1 - index]
// Check if both values are valid numbers
const hasValidData =
trader.total_pnl_pct != null &&
opponent.total_pnl_pct != null &&
!isNaN(trader.total_pnl_pct) &&
!isNaN(opponent.total_pnl_pct)
// Check if both values are valid numbers
const hasValidData =
trader.total_pnl_pct != null &&
opponent.total_pnl_pct != null &&
!isNaN(trader.total_pnl_pct) &&
!isNaN(opponent.total_pnl_pct)
const gap = hasValidData
? trader.total_pnl_pct - opponent.total_pnl_pct
: NaN
const gap = hasValidData
? trader.total_pnl_pct - opponent.total_pnl_pct
: NaN
return (
<div
key={trader.trader_id}
className="p-4 rounded transition-all duration-300 hover:scale-[1.02]"
style={
isWinning
? {
return (
<div
key={trader.trader_id}
className="p-4 rounded transition-all duration-300 hover:scale-[1.02]"
style={
isWinning
? {
background:
'linear-gradient(135deg, rgba(14, 203, 129, 0.08) 0%, rgba(14, 203, 129, 0.02) 100%)',
border: '2px solid rgba(14, 203, 129, 0.3)',
boxShadow: '0 3px 15px rgba(14, 203, 129, 0.12)',
}
: {
: {
background: '#0B0E11',
border: '1px solid #2B3139',
boxShadow: '0 1px 4px rgba(0, 0, 0, 0.3)',
}
}
>
<div className="text-center">
{/* Avatar */}
<div className="flex justify-center mb-3">
<PunkAvatar
seed={getTraderAvatar(trader.trader_id, trader.trader_name)}
size={56}
className="rounded-xl"
/>
</div>
<div
className="text-sm md:text-base font-bold mb-2"
style={{
color: getTraderColor(sortedTraders, trader.trader_id),
}}
>
{trader.trader_name}
</div>
<div
className="text-lg md:text-2xl font-bold mono mb-1"
style={{
color:
(trader.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D',
}}
>
{trader.total_pnl_pct != null &&
!isNaN(trader.total_pnl_pct)
? `${trader.total_pnl_pct >= 0 ? '+' : ''}${trader.total_pnl_pct.toFixed(2)}%`
: '—'}
</div>
{hasValidData && isWinning && gap > 0 && (
<div
className="text-xs font-semibold"
style={{ color: '#0ECB81' }}
>
{t('leadingBy', language, { gap: gap.toFixed(2) })}
}
>
<div className="text-center">
{/* Avatar */}
<div className="flex justify-center mb-3">
<PunkAvatar
seed={getTraderAvatar(trader.trader_id, trader.trader_name)}
size={56}
className="rounded-xl"
/>
</div>
)}
{hasValidData && !isWinning && gap < 0 && (
<div
className="text-xs font-semibold"
style={{ color: '#F6465D' }}
className="text-sm md:text-base font-bold mb-2"
style={{
color: getTraderColor(sortedTraders, trader.trader_id),
}}
>
{t('behindBy', language, {
gap: Math.abs(gap).toFixed(2),
})}
{trader.trader_name}
</div>
)}
{!hasValidData && (
<div
className="text-xs font-semibold"
style={{ color: '#848E9C' }}
className="text-lg md:text-2xl font-bold mono mb-1"
style={{
color:
(trader.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D',
}}
>
{trader.total_pnl_pct != null &&
!isNaN(trader.total_pnl_pct)
? `${trader.total_pnl_pct >= 0 ? '+' : ''}${trader.total_pnl_pct.toFixed(2)}%`
: '—'}
</div>
)}
{hasValidData && isWinning && gap > 0 && (
<div
className="text-xs font-semibold"
style={{ color: '#0ECB81' }}
>
{t('leadingBy', language, { gap: gap.toFixed(2) })}
</div>
)}
{hasValidData && !isWinning && gap < 0 && (
<div
className="text-xs font-semibold"
style={{ color: '#F6465D' }}
>
{t('behindBy', language, {
gap: Math.abs(gap).toFixed(2),
})}
</div>
)}
{!hasValidData && (
<div
className="text-xs font-semibold"
style={{ color: '#848E9C' }}
>
</div>
)}
</div>
</div>
</div>
)
})}
)
})}
</div>
</div>
</div>
)}
)}
{/* Trader Config View Modal */}
<TraderConfigViewModal
isOpen={isModalOpen}
onClose={closeModal}
traderData={selectedTrader}
/>
</div>
{/* Trader Config View Modal */}
<TraderConfigViewModal
isOpen={isModalOpen}
onClose={closeModal}
traderData={selectedTrader}
/>
</div>
</DeepVoidBackground>
)
}

View 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>
)
}

View File

@@ -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)')
}
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"
style={{
background: 'var(--brand-yellow)',
color: 'var(--brand-black)',
}}
>
<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,11 +428,10 @@ 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'
? 'bg-yellow-500 text-black'
: 'text-gray-400 hover:text-white'
}`}
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'
}`}
>
<span className="text-lg">🇨🇳</span>
<span className="text-sm"></span>
@@ -585,11 +441,10 @@ 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'
? 'bg-yellow-500 text-black'
: 'text-gray-400 hover:text-white'
}`}
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'
}`}
>
<span className="text-lg">🇺🇸</span>
<span className="text-sm">English</span>
@@ -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)' }}
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"
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)',
}}
>
<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)}

View File

@@ -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>
)
}

View File

@@ -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,111 +52,114 @@ 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"
onClick={onClose}
className="fixed inset-0 z-50"
>
{/* 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"
onClick={(e) => e.stopPropagation()}
<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}
>
{/* 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 gap-2">
<Terminal size={12} className="text-nofx-gold" />
<span className="text-[10px] text-zinc-500 uppercase tracking-wider">auth_protocol.exe</span>
<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-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-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-nofx-text-muted uppercase tracking-wider">auth_protocol.exe</span>
</div>
<button
onClick={onClose}
className="text-nofx-text-muted hover:text-nofx-danger transition-colors"
>
<X size={14} />
</button>
</div>
<button
onClick={onClose}
className="text-zinc-600 hover:text-red-500 transition-colors"
>
<X size={14} />
</button>
</div>
{/* Main Content */}
<div className="p-8 relative">
{/* Background Grid */}
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808008_1px,transparent_1px),linear-gradient(to_bottom,#80808008_1px,transparent_1px)] bg-[size:14px_14px] pointer-events-none"></div>
{/* Main Content */}
<div className="p-8 relative">
{/* Background Grid */}
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808008_1px,transparent_1px),linear-gradient(to_bottom,#80808008_1px,transparent_1px)] bg-[size:14px_14px] pointer-events-none"></div>
<div className="relative z-10">
{/* Flashing Access Denied */}
<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)]">
<AlertTriangle size={18} className="animate-pulse" />
<span className="font-bold tracking-widest text-sm uppercase">{language === 'zh' ? '访问被拒绝' : 'ACCESS DENIED'}</span>
<div className="relative z-10">
{/* Flashing Access Denied */}
<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-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>
</div>
</div>
</div>
{/* Terminal Text */}
<div className="space-y-4 mb-8">
<div className="text-center">
<h2 className="text-xl font-bold text-white uppercase tracking-wider mb-2">{t.title}</h2>
<p className="text-nofx-gold text-xs uppercase tracking-widest border-b border-nofx-gold/20 pb-4 inline-block">{t.subtitle}</p>
{/* Terminal Text */}
<div className="space-y-4 mb-8">
<div className="text-center">
<h2 className="text-xl font-bold text-white uppercase tracking-wider mb-2">{t.title}</h2>
<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-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>
</div>
<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-nofx-text-muted uppercase tracking-wide">
<span className="text-nofx-gold"></span> {benefit}
</div>
))}
</div>
</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">
<span className="text-green-500 mr-2">$</span>
{t.description}
</p>
{/* Action Buttons */}
<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-neon hover:shadow-[0_0_25px_rgba(240,185,11,0.4)] group"
>
<LogIn size={14} />
<span>{t.login}</span>
<span className="opacity-0 group-hover:opacity-100 transition-opacity -ml-2 group-hover:ml-0">-&gt;</span>
</a>
<a
href="/register"
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>
</a>
</div>
<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">
<span className="text-nofx-gold"></span> {benefit}
</div>
))}
<div className="mt-4 text-center">
<button
onClick={onClose}
className="text-[10px] text-nofx-text-muted hover:text-nofx-danger uppercase tracking-widest hover:underline decoration-red-500/30"
>
[ {t.later} ]
</button>
</div>
</div>
{/* Action Buttons */}
<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"
>
<LogIn size={14} />
<span>{t.login}</span>
<span className="opacity-0 group-hover:opacity-100 transition-opacity -ml-2 group-hover:ml-0">-&gt;</span>
</a>
<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"
>
<UserPlus size={14} />
<span>{t.register}</span>
</a>
</div>
<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"
>
[ {t.later} ]
</button>
</div>
</div>
</div>
{/* Corner Accents */}
<div className="absolute top-0 right-0 w-2 h-2 border-t border-r border-nofx-gold"></div>
<div className="absolute bottom-0 left-0 w-2 h-2 border-b border-l border-nofx-gold"></div>
{/* Corner Accents */}
<div className="absolute top-0 right-0 w-2 h-2 border-t border-r border-nofx-gold"></div>
<div className="absolute bottom-0 left-0 w-2 h-2 border-b border-l border-nofx-gold"></div>
</motion.div>
</motion.div>
</DeepVoidBackground>
</motion.div>
)}
</AnimatePresence>

View File

@@ -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>
)
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,125 +58,121 @@ export function FAQLayout({ language }: FAQLayoutProps) {
}
return (
<Container className="py-6 pt-24">
{/* 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' }} />
<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 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 text-nofx-text-main">
{t('faqTitle', language)}
</h1>
<p className="text-lg mb-8 text-nofx-text-muted">
{t('faqSubtitle', language)}
</p>
{/* Search Bar */}
<div className="max-w-2xl mx-auto">
<FAQSearchBar
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
placeholder={
language === 'zh' ? '搜索常见问题...' : 'Search FAQ...'
}
/>
</div>
</div>
<h1 className="text-4xl font-bold mb-4" style={{ color: '#EAECEF' }}>
{t('faqTitle', language)}
</h1>
<p className="text-lg mb-8" style={{ color: '#848E9C' }}>
{t('faqSubtitle', language)}
</p>
{/* Search Bar */}
<div className="max-w-2xl mx-auto">
<FAQSearchBar
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
placeholder={
language === 'zh' ? '搜索常见问题...' : 'Search FAQ...'
}
/>
</div>
</div>
{/* Main Content */}
<div className="flex gap-8">
{/* Sidebar - Hidden on mobile, visible on desktop */}
<aside className="hidden lg:block w-64 flex-shrink-0">
<FAQSidebar
categories={filteredCategories}
activeItemId={activeItemId}
language={language}
onItemClick={handleItemClick}
/>
</aside>
{/* Content Area */}
<main className="flex-1 min-w-0">
{filteredCategories.length > 0 ? (
<FAQContent
{/* Main Content */}
<div className="flex gap-8">
{/* Sidebar - Hidden on mobile, visible on desktop */}
<aside className="hidden lg:block w-64 flex-shrink-0">
<FAQSidebar
categories={filteredCategories}
activeItemId={activeItemId}
language={language}
onActiveItemChange={setActiveItemId}
onItemClick={handleItemClick}
/>
) : (
<div className="text-center py-12">
<p className="text-lg" style={{ color: '#848E9C' }}>
{language === 'zh'
? '没有找到匹配的问题'
: 'No matching questions found'}
</p>
<button
onClick={() => setSearchTerm('')}
className="mt-4 px-6 py-2 rounded-lg font-semibold transition-all hover:opacity-90"
style={{
background:
'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
color: '#0B0E11',
}}
>
{language === 'zh' ? '清除搜索' : 'Clear Search'}
</button>
</div>
)}
</main>
</div>
</aside>
{/* Contact Section */}
<div
className="mt-16 p-8 rounded-lg text-center"
style={{
background:
'linear-gradient(135deg, rgba(240, 185, 11, 0.1) 0%, rgba(252, 213, 53, 0.05) 100%)',
border: '1px solid rgba(240, 185, 11, 0.2)',
}}
>
<h3 className="text-xl font-bold mb-3" style={{ color: '#EAECEF' }}>
{t('faqStillHaveQuestions', language)}
</h3>
<p className="mb-6" style={{ color: '#848E9C' }}>
{t('faqContactUs', language)}
</p>
<div className="flex items-center justify-center gap-4">
<a
href="https://github.com/NoFxAiOS/nofx"
target="_blank"
rel="noopener noreferrer"
className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105"
style={{
background: '#1E2329',
color: '#EAECEF',
border: '1px solid #2B3139',
}}
>
GitHub
</a>
<a
href="https://t.me/nofx_dev_community"
target="_blank"
rel="noopener noreferrer"
className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105"
style={{
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
color: '#0B0E11',
}}
>
{t('community', language)}
</a>
{/* Content Area */}
<main className="flex-1 min-w-0">
{filteredCategories.length > 0 ? (
<FAQContent
categories={filteredCategories}
language={language}
onActiveItemChange={setActiveItemId}
/>
) : (
<div className="text-center py-12">
<p className="text-lg" style={{ color: '#848E9C' }}>
{language === 'zh'
? '没有找到匹配的问题'
: 'No matching questions found'}
</p>
<button
onClick={() => setSearchTerm('')}
className="mt-4 px-6 py-2 rounded-lg font-semibold transition-all hover:opacity-90"
style={{
background:
'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
color: '#0B0E11',
}}
>
{language === 'zh' ? '清除搜索' : 'Clear Search'}
</button>
</div>
)}
</main>
</div>
{/* Contact Section */}
<div
className="mt-16 p-8 rounded-lg text-center"
style={{
background:
'linear-gradient(135deg, rgba(240, 185, 11, 0.1) 0%, rgba(252, 213, 53, 0.05) 100%)',
border: '1px solid rgba(240, 185, 11, 0.2)',
}}
>
<h3 className="text-xl font-bold mb-3" style={{ color: '#EAECEF' }}>
{t('faqStillHaveQuestions', language)}
</h3>
<p className="mb-6" style={{ color: '#848E9C' }}>
{t('faqContactUs', language)}
</p>
<div className="flex items-center justify-center gap-4">
<a
href="https://github.com/NoFxAiOS/nofx"
target="_blank"
rel="noopener noreferrer"
className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105"
style={{
background: '#1E2329',
color: '#EAECEF',
border: '1px solid #2B3139',
}}
>
GitHub
</a>
<a
href="https://t.me/nofx_dev_community"
target="_blank"
rel="noopener noreferrer"
className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105"
style={{
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
color: '#0B0E11',
}}
>
{t('community', language)}
</a>
</div>
</div>
</div>
</Container>
</DeepVoidBackground>
)
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 */

View File

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

View File

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

View File

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

View File

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

View File

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