mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-10 06:20:58 +08:00
feat: cream terminal redesign, English-only UI, autopilot launch fixes
- Redesign dashboard into a cream-paper + vermilion IBM Plex Mono terminal (live L2 order book, cost/liq map, WS K-line, signal matrix, orchestration topology, risk radar, execution log, current positions, equity curve) - Convert all user-facing UI and backend strings/prompts from Chinese to English (multi-language retained, default English) - Add /api/statistics/full endpoint + full-stats frontend wiring - Fix Autopilot launch: reuse the existing trader instead of creating duplicates (eliminates repeat ~35s create cost and stale-trader 404s); launch sends 5m scan interval - Fix unreadable toasts: cream theme with high-contrast text + per-type accent - Silence background dashboard polls (getTraderConfig) to stop error-toast spam
This commit is contained in:
@@ -9,17 +9,17 @@ import { LanguageSwitcher } from '../common/LanguageSwitcher'
|
||||
|
||||
const labels = {
|
||||
zh: {
|
||||
welcome: '欢迎使用 NOFX',
|
||||
subtitle: '创建账号开始使用',
|
||||
email: '邮箱',
|
||||
welcome: 'Welcome to NOFX',
|
||||
subtitle: 'Create your account to get started',
|
||||
email: 'Email',
|
||||
emailPlaceholder: 'you@example.com',
|
||||
password: '密码',
|
||||
passwordPlaceholder: '至少 8 个字符',
|
||||
passwordError: '密码至少需要 8 个字符',
|
||||
submit: '开始使用',
|
||||
submitting: '创建中...',
|
||||
setupFailed: '创建失败,请重试',
|
||||
singleUser: '单用户系统 — 这是唯一的账号',
|
||||
password: 'Password',
|
||||
passwordPlaceholder: 'At least 8 characters',
|
||||
passwordError: 'Password must be at least 8 characters',
|
||||
submit: 'Get Started',
|
||||
submitting: 'Creating account...',
|
||||
setupFailed: 'Setup failed, please try again',
|
||||
singleUser: 'Single-user system — this is the only account',
|
||||
},
|
||||
en: {
|
||||
welcome: 'Welcome to NOFX',
|
||||
@@ -88,7 +88,7 @@ export function SetupPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen w-full overflow-hidden bg-[#0a0a0f]">
|
||||
<div className="relative min-h-screen w-full overflow-hidden bg-nofx-bg">
|
||||
{/* Decorative background - simulates the main app behind a modal */}
|
||||
|
||||
{/* Grid */}
|
||||
@@ -99,33 +99,33 @@ export function SetupPage() {
|
||||
{/* Glow spots */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute top-[10%] left-[15%] w-[500px] h-[500px] bg-nofx-gold/8 rounded-full blur-[150px]" />
|
||||
<div className="absolute bottom-[5%] right-[10%] w-[400px] h-[400px] bg-indigo-500/6 rounded-full blur-[140px]" />
|
||||
<div className="absolute top-[40%] right-[30%] w-[300px] h-[300px] bg-emerald-500/4 rounded-full blur-[120px]" />
|
||||
<div className="absolute bottom-[5%] right-[10%] w-[400px] h-[400px] bg-nofx-gold/6 rounded-full blur-[140px]" />
|
||||
<div className="absolute top-[40%] right-[30%] w-[300px] h-[300px] bg-nofx-success/4 rounded-full blur-[120px]" />
|
||||
</div>
|
||||
|
||||
{/* Faux UI elements in background to simulate the app */}
|
||||
<div className="absolute inset-0 pointer-events-none opacity-[0.06]">
|
||||
{/* Fake header bar */}
|
||||
<div className="h-14 border-b border-white/20 flex items-center px-6 gap-4">
|
||||
<div className="w-8 h-8 rounded-lg bg-white/40" />
|
||||
<div className="h-3 w-20 rounded bg-white/30" />
|
||||
<div className="h-3 w-16 rounded bg-white/20 ml-4" />
|
||||
<div className="h-3 w-16 rounded bg-white/20" />
|
||||
<div className="h-3 w-16 rounded bg-white/20" />
|
||||
<div className="h-14 border-b border-nofx-text/20 flex items-center px-6 gap-4">
|
||||
<div className="w-8 h-8 rounded-lg bg-nofx-text/40" />
|
||||
<div className="h-3 w-20 rounded bg-nofx-text/30" />
|
||||
<div className="h-3 w-16 rounded bg-nofx-text/20 ml-4" />
|
||||
<div className="h-3 w-16 rounded bg-nofx-text/20" />
|
||||
<div className="h-3 w-16 rounded bg-nofx-text/20" />
|
||||
</div>
|
||||
{/* Fake content cards */}
|
||||
<div className="p-6 grid grid-cols-4 gap-4 mt-2">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<div key={i} className="h-24 rounded-xl border border-white/15 bg-white/5" />
|
||||
<div key={i} className="h-24 rounded-xl border border-nofx-text/15 bg-nofx-text/5" />
|
||||
))}
|
||||
</div>
|
||||
<div className="px-6 mt-2">
|
||||
<div className="h-64 rounded-xl border border-white/15 bg-white/5" />
|
||||
<div className="h-64 rounded-xl border border-nofx-text/15 bg-nofx-text/5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Blur overlay */}
|
||||
<div className="absolute inset-0 backdrop-blur-md bg-black/60" />
|
||||
<div className="absolute inset-0 backdrop-blur-md bg-nofx-bg/60" />
|
||||
|
||||
<LanguageSwitcher />
|
||||
|
||||
@@ -138,25 +138,25 @@ export function SetupPage() {
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="relative">
|
||||
<div className="absolute -inset-4 bg-nofx-gold/20 rounded-full blur-2xl" />
|
||||
<img src="/icons/nofx.svg" alt="NOFX" className="w-14 h-14 relative z-10 drop-shadow-[0_0_15px_rgba(240,185,11,0.3)]" />
|
||||
<img src="/icons/nofx.svg" alt="NOFX" className="w-14 h-14 relative z-10" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-white mb-1.5">{l.welcome}</h1>
|
||||
<p className="text-zinc-500 text-sm">{l.subtitle}</p>
|
||||
<h1 className="text-2xl font-bold text-nofx-text mb-1.5">{l.welcome}</h1>
|
||||
<p className="text-nofx-text-muted text-sm">{l.subtitle}</p>
|
||||
</div>
|
||||
|
||||
{/* Card */}
|
||||
<div className="bg-zinc-900/80 backdrop-blur-2xl border border-white/10 rounded-2xl p-8 shadow-[0_25px_60px_-15px_rgba(0,0,0,0.5),0_0_40px_-10px_rgba(240,185,11,0.08)]">
|
||||
<div className="bg-nofx-bg-lighter backdrop-blur-2xl border border-[rgba(26,24,19,0.14)] rounded-2xl p-8 shadow-lg">
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
|
||||
{/* Email */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-zinc-400 mb-2">{l.email}</label>
|
||||
<label className="block text-xs font-medium text-nofx-text-muted mb-2">{l.email}</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-3 text-sm text-white placeholder-zinc-600 focus:outline-none focus:border-nofx-gold/60 focus:ring-1 focus:ring-nofx-gold/30 transition-all"
|
||||
className="w-full bg-nofx-bg-deeper border border-[rgba(26,24,19,0.14)] rounded-xl px-4 py-3 text-sm text-nofx-text placeholder-nofx-text-muted focus:outline-none focus:border-nofx-gold/60 focus:ring-1 focus:ring-nofx-gold/30 transition-all"
|
||||
placeholder={l.emailPlaceholder}
|
||||
required
|
||||
autoFocus
|
||||
@@ -165,20 +165,20 @@ export function SetupPage() {
|
||||
|
||||
{/* Password */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-zinc-400 mb-2">{l.password}</label>
|
||||
<label className="block text-xs font-medium text-nofx-text-muted mb-2">{l.password}</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-3 pr-11 text-sm text-white placeholder-zinc-600 focus:outline-none focus:border-nofx-gold/60 focus:ring-1 focus:ring-nofx-gold/30 transition-all"
|
||||
className="w-full bg-nofx-bg-deeper border border-[rgba(26,24,19,0.14)] rounded-xl px-4 py-3 pr-11 text-sm text-nofx-text placeholder-nofx-text-muted focus:outline-none focus:border-nofx-gold/60 focus:ring-1 focus:ring-nofx-gold/30 transition-all"
|
||||
placeholder={l.passwordPlaceholder}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3.5 top-1/2 -translate-y-1/2 text-zinc-500 hover:text-zinc-300 transition-colors"
|
||||
className="absolute right-3.5 top-1/2 -translate-y-1/2 text-nofx-text-muted hover:text-nofx-text transition-colors"
|
||||
>
|
||||
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</button>
|
||||
@@ -193,7 +193,7 @@ export function SetupPage() {
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<p className="text-xs text-red-400 bg-red-500/10 border border-red-500/20 rounded-lg px-3 py-2">
|
||||
<p className="text-xs text-nofx-danger bg-nofx-danger/10 border border-nofx-danger/20 rounded-lg px-3 py-2">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
@@ -202,14 +202,14 @@ export function SetupPage() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-nofx-gold hover:bg-yellow-400 active:scale-[0.98] text-black font-semibold py-3 rounded-xl text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed mt-2 shadow-[0_0_20px_rgba(240,185,11,0.2)]"
|
||||
className="w-full bg-nofx-gold hover:bg-nofx-gold-highlight active:scale-[0.98] text-nofx-bg font-semibold py-3 rounded-xl text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed mt-2"
|
||||
>
|
||||
{loading ? l.submitting : l.submit}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-xs text-zinc-600 mt-6">
|
||||
<p className="text-center text-xs text-nofx-text-muted mt-6">
|
||||
{l.singleUser}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -92,11 +92,11 @@ export function TwoStageKeyModal({
|
||||
setProcessing(true)
|
||||
|
||||
try {
|
||||
// 生成混淆字符串
|
||||
// Generate obfuscation string
|
||||
const obfuscation = generateObfuscation()
|
||||
setManualObfuscationValue(obfuscation)
|
||||
|
||||
// 尝试复制到剪贴板
|
||||
// Try to copy to clipboard
|
||||
if (navigator.clipboard) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(obfuscation)
|
||||
@@ -105,14 +105,14 @@ export function TwoStageKeyModal({
|
||||
...obfuscationLog,
|
||||
`Stage 1: ${new Date().toISOString()} - Auto copied obfuscation`,
|
||||
])
|
||||
toast.success('已复制混淆字符串到剪贴板')
|
||||
toast.success('Obfuscation string copied to clipboard')
|
||||
} catch {
|
||||
setClipboardStatus('failed')
|
||||
setObfuscationLog([
|
||||
...obfuscationLog,
|
||||
`Stage 1: ${new Date().toISOString()} - Auto copy failed, manual required`,
|
||||
])
|
||||
toast.error('复制失败,请手动复制混淆字符串')
|
||||
toast.error('Copy failed, please copy the obfuscation string manually')
|
||||
}
|
||||
} else {
|
||||
setClipboardStatus('failed')
|
||||
@@ -120,7 +120,7 @@ export function TwoStageKeyModal({
|
||||
...obfuscationLog,
|
||||
`Stage 1: ${new Date().toISOString()} - Clipboard API not available`,
|
||||
])
|
||||
toast('当前浏览器不支持自动复制,请手动复制')
|
||||
toast('This browser does not support automatic copy, please copy manually')
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -179,17 +179,17 @@ export function TwoStageKeyModal({
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80">
|
||||
<div className="bg-gray-900 p-8 rounded-xl max-w-lg w-full mx-4 border border-gray-700">
|
||||
<div className="bg-nofx-bg-lighter p-8 rounded-xl max-w-lg w-full mx-4 border border-[rgba(26,24,19,0.14)]">
|
||||
<div className="text-center mb-6">
|
||||
<h2 className="text-xl font-bold text-white mb-2">
|
||||
<h2 className="text-xl font-bold text-nofx-text mb-2">
|
||||
🔐 {t('twoStageKey.title', language)}
|
||||
{contextLabel && (
|
||||
<span className="text-gray-300 text-base font-normal ml-2">
|
||||
<span className="text-nofx-text-muted text-base font-normal ml-2">
|
||||
({contextLabel})
|
||||
</span>
|
||||
)}
|
||||
</h2>
|
||||
<p className="text-gray-300 text-sm">
|
||||
<p className="text-nofx-text-muted text-sm">
|
||||
{stage === 1
|
||||
? t('twoStageKey.stage1Description', language, {
|
||||
length: expectedPart1Length,
|
||||
@@ -208,7 +208,7 @@ export function TwoStageKeyModal({
|
||||
{stage === 1 && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-gray-300 text-sm mb-2">
|
||||
<label className="block text-nofx-text-muted text-sm mb-2">
|
||||
{t('twoStageKey.stage1InputLabel', language)} (
|
||||
{expectedPart1Length} {t('twoStageKey.characters', language)})
|
||||
</label>
|
||||
@@ -218,13 +218,13 @@ export function TwoStageKeyModal({
|
||||
value={part1}
|
||||
onChange={(e) => setPart1(e.target.value)}
|
||||
placeholder="0x1234..."
|
||||
className="w-full bg-gray-800 border border-gray-600 rounded-lg px-4 py-3 text-white font-mono text-sm focus:border-blue-500 focus:outline-none"
|
||||
className="w-full bg-nofx-bg-deeper border border-[rgba(26,24,19,0.14)] rounded-lg px-4 py-3 text-nofx-text font-mono text-sm focus:border-nofx-gold focus:outline-none"
|
||||
maxLength={expectedPart1Length + 2} // +2 for optional 0x prefix
|
||||
disabled={processing}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && <div className="text-red-400 text-sm">{error}</div>}
|
||||
{error && <div className="text-nofx-danger text-sm">{error}</div>}
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
@@ -233,7 +233,7 @@ export function TwoStageKeyModal({
|
||||
(part1.startsWith('0x') ? part1.slice(2) : part1).length <
|
||||
expectedPart1Length || processing
|
||||
}
|
||||
className="flex-1 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 text-white font-medium py-3 px-4 rounded-lg transition-colors"
|
||||
className="flex-1 bg-nofx-gold hover:bg-nofx-gold-highlight disabled:bg-nofx-bg-deeper disabled:text-nofx-text-muted text-nofx-bg font-medium py-3 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
{processing
|
||||
? t('twoStageKey.processing', language)
|
||||
@@ -242,7 +242,7 @@ export function TwoStageKeyModal({
|
||||
<button
|
||||
onClick={onCancel}
|
||||
disabled={processing}
|
||||
className="px-6 py-3 text-gray-300 hover:text-white border border-gray-600 rounded-lg transition-colors"
|
||||
className="px-6 py-3 text-nofx-text-muted hover:text-nofx-text border border-[rgba(26,24,19,0.14)] rounded-lg transition-colors"
|
||||
>
|
||||
{t('twoStageKey.cancelButton', language)}
|
||||
</button>
|
||||
@@ -252,9 +252,9 @@ export function TwoStageKeyModal({
|
||||
|
||||
{/* Transition Message */}
|
||||
{stage === 2 && clipboardStatus !== 'idle' && (
|
||||
<div className="mb-4 p-4 rounded-lg bg-blue-900/50 border border-blue-600">
|
||||
<div className="mb-4 p-4 rounded-lg bg-nofx-gold/10 border border-nofx-gold/40">
|
||||
{clipboardStatus === 'copied' && (
|
||||
<div className="text-blue-300">
|
||||
<div className="text-nofx-text">
|
||||
<div className="font-medium">
|
||||
{t('twoStageKey.obfuscationCopied', language)}
|
||||
</div>
|
||||
@@ -264,11 +264,11 @@ export function TwoStageKeyModal({
|
||||
</div>
|
||||
)}
|
||||
{clipboardStatus === 'failed' && manualObfuscationValue && (
|
||||
<div className="text-yellow-300">
|
||||
<div className="text-nofx-gold">
|
||||
<div className="font-medium">
|
||||
{t('twoStageKey.obfuscationManual', language)}
|
||||
</div>
|
||||
<div className="text-xs mt-2 p-2 bg-gray-800 rounded font-mono break-all border">
|
||||
<div className="text-xs mt-2 p-2 bg-nofx-bg-deeper rounded font-mono break-all border border-[rgba(26,24,19,0.14)]">
|
||||
{manualObfuscationValue}
|
||||
</div>
|
||||
<div className="text-sm mt-1">
|
||||
@@ -283,7 +283,7 @@ export function TwoStageKeyModal({
|
||||
{stage === 2 && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-gray-300 text-sm mb-2">
|
||||
<label className="block text-nofx-text-muted text-sm mb-2">
|
||||
{t('twoStageKey.stage2InputLabel', language)} (
|
||||
{expectedPart2Length} {t('twoStageKey.characters', language)})
|
||||
</label>
|
||||
@@ -293,12 +293,12 @@ export function TwoStageKeyModal({
|
||||
value={part2}
|
||||
onChange={(e) => setPart2(e.target.value)}
|
||||
placeholder="...5678"
|
||||
className="w-full bg-gray-800 border border-gray-600 rounded-lg px-4 py-3 text-white font-mono text-sm focus:border-blue-500 focus:outline-none"
|
||||
className="w-full bg-nofx-bg-deeper border border-[rgba(26,24,19,0.14)] rounded-lg px-4 py-3 text-nofx-text font-mono text-sm focus:border-nofx-gold focus:outline-none"
|
||||
maxLength={expectedPart2Length + 2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && <div className="text-red-400 text-sm">{error}</div>}
|
||||
{error && <div className="text-nofx-danger text-sm">{error}</div>}
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
@@ -307,13 +307,13 @@ export function TwoStageKeyModal({
|
||||
(part2.startsWith('0x') ? part2.slice(2) : part2).length <
|
||||
expectedPart2Length
|
||||
}
|
||||
className="flex-1 bg-green-600 hover:bg-green-700 disabled:bg-gray-600 text-white font-medium py-3 px-4 rounded-lg transition-colors"
|
||||
className="flex-1 bg-nofx-success hover:bg-nofx-success/90 disabled:bg-nofx-bg-deeper disabled:text-nofx-text-muted text-nofx-bg font-medium py-3 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
🔒 {t('twoStageKey.encryptButton', language)}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="px-6 py-3 text-gray-300 hover:text-white border border-gray-600 rounded-lg transition-colors"
|
||||
className="px-6 py-3 text-nofx-text-muted hover:text-nofx-text border border-[rgba(26,24,19,0.14)] rounded-lg transition-colors"
|
||||
>
|
||||
{t('twoStageKey.backButton', language)}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user