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:
tinkle-community
2026-06-30 16:03:52 +08:00
parent eba28bcf0e
commit 110bf52908
149 changed files with 6835 additions and 3611 deletions

View File

@@ -70,14 +70,14 @@ export function LoginPage() {
<main className="flex-1 grid lg:grid-cols-2">
{/* ───────── LEFT: brand panel (desktop only) ───────── */}
<section className="hidden lg:flex flex-col justify-between p-12 xl:p-16 relative overflow-hidden">
{/* Ambient gold halo */}
{/* Ambient vermilion halo */}
<div className="absolute -left-32 top-1/3 w-[28rem] h-[28rem] bg-nofx-gold/[0.06] rounded-full blur-3xl pointer-events-none" />
<div className="absolute -right-16 bottom-0 w-72 h-72 bg-nofx-accent/[0.04] rounded-full blur-3xl pointer-events-none" />
{/* Brand mark */}
<div className="flex items-center gap-3 relative">
<img src="/icons/nofx.svg" alt="NOFX" className="w-9 h-9" />
<div className="font-mono font-bold text-xl tracking-tight text-white">
<div className="font-mono font-bold text-xl tracking-tight text-nofx-text">
NOFX<span className="text-nofx-gold">.</span>
</div>
</div>
@@ -90,33 +90,33 @@ export function LoginPage() {
Terminal Online
</span>
</div>
<h2 className="text-4xl xl:text-5xl font-bold tracking-tight text-white leading-[1.05]">
<h2 className="text-4xl xl:text-5xl font-bold tracking-tight text-nofx-text leading-[1.05]">
{language === 'zh' ? (
<>
AI <br />
<span className="bg-gradient-to-r from-nofx-gold to-yellow-300 bg-clip-text text-transparent">
AI-Powered<br />
<span className="text-nofx-gold">
Multi-Market Trading Terminal
</span>
</>
) : language === 'id' ? (
<>
Terminal Trading<br />
<span className="bg-gradient-to-r from-nofx-gold to-yellow-300 bg-clip-text text-transparent">
<span className="text-nofx-gold">
Multi-Pasar AI
</span>
</>
) : (
<>
AI-Powered<br />
<span className="bg-gradient-to-r from-nofx-gold to-yellow-300 bg-clip-text text-transparent">
<span className="text-nofx-gold">
Trading Terminal
</span>
</>
)}
</h2>
<p className="mt-5 text-zinc-400 text-base leading-relaxed max-w-md">
<p className="mt-5 text-nofx-text-muted text-base leading-relaxed max-w-md">
{language === 'zh'
? '一键接入 HyperliquidOKXAster 等 10+ 交易所与 7 LLM 模型, 用自然语言部署 24/7 自动化策略.'
? 'Plug into 10+ exchanges including Hyperliquid, OKX, Aster, and 7 LLM models. Deploy 24/7 automated strategies with natural language.'
: language === 'id'
? 'Hubungkan ke 10+ bursa termasuk Hyperliquid, OKX, Aster dan 7 model LLM. Terapkan strategi otomatis 24/7 dengan bahasa alami.'
: 'Plug into 10+ exchanges including Hyperliquid, OKX, Aster, and 7 LLM models. Deploy 24/7 automated strategies with natural language.'}
@@ -129,7 +129,7 @@ export function LoginPage() {
value="10+"
label={
language === 'zh'
? '交易所'
? 'Exchanges'
: language === 'id'
? 'Bursa'
: 'Exchanges'
@@ -139,7 +139,7 @@ export function LoginPage() {
value="7"
label={
language === 'zh'
? 'AI 模型'
? 'AI Models'
: language === 'id'
? 'Model AI'
: 'AI Models'
@@ -149,7 +149,7 @@ export function LoginPage() {
value="24/7"
label={
language === 'zh'
? '全天候'
? 'Always On'
: language === 'id'
? 'Sepanjang Waktu'
: 'Always On'
@@ -164,19 +164,19 @@ export function LoginPage() {
{/* Mobile brand */}
<div className="lg:hidden flex flex-col items-center gap-3 mb-10">
<img src="/icons/nofx.svg" alt="NOFX" className="w-12 h-12" />
<div className="font-mono font-bold text-lg tracking-tight text-white">
<div className="font-mono font-bold text-lg tracking-tight text-nofx-text">
NOFX<span className="text-nofx-gold">.</span>
</div>
</div>
{/* Form header */}
<div className="mb-7">
<h1 className="text-[26px] sm:text-3xl font-bold tracking-tight text-white">
<h1 className="text-[26px] sm:text-3xl font-bold tracking-tight text-nofx-text">
{t('signIn', language)}
</h1>
<p className="mt-1.5 text-sm text-zinc-500">
<p className="mt-1.5 text-sm text-nofx-text-muted">
{language === 'zh'
? '使用您的邮箱继续'
? 'Continue with your email'
: language === 'id'
? 'Lanjutkan dengan email Anda'
: 'Continue with your email'}
@@ -187,14 +187,14 @@ export function LoginPage() {
<form onSubmit={handleLogin} className="space-y-4">
{/* Email */}
<div>
<label className="block text-[10.5px] font-medium uppercase tracking-[0.14em] text-zinc-500 mb-2">
<label className="block text-[10.5px] font-medium uppercase tracking-[0.14em] text-nofx-text-muted mb-2">
{t('email', language)}
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full bg-zinc-900/60 border border-white/[0.08] rounded-lg px-4 py-[11px] text-[14px] text-white placeholder-zinc-600 focus:outline-none focus:border-nofx-gold/50 focus:bg-zinc-900 focus:ring-2 focus:ring-nofx-gold/20 transition-all"
className="w-full bg-nofx-bg-lighter border border-[rgba(26,24,19,0.14)] rounded-lg px-4 py-[11px] text-[14px] text-nofx-text placeholder-nofx-text-muted focus:outline-none focus:border-nofx-gold/50 focus:bg-nofx-bg-lighter focus:ring-2 focus:ring-nofx-gold/20 transition-all"
placeholder="you@example.com"
required
autoFocus
@@ -205,13 +205,13 @@ export function LoginPage() {
{/* Password */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-[10.5px] font-medium uppercase tracking-[0.14em] text-zinc-500">
<label className="text-[10.5px] font-medium uppercase tracking-[0.14em] text-nofx-text-muted">
{t('password', language)}
</label>
<button
type="button"
onClick={() => navigate('/reset-password')}
className="text-xs text-zinc-500 hover:text-nofx-gold transition-colors"
className="text-xs text-nofx-text-muted hover:text-nofx-gold transition-colors"
>
{t('forgotPassword', language)}
</button>
@@ -221,7 +221,7 @@ export function LoginPage() {
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full bg-zinc-900/60 border border-white/[0.08] rounded-lg px-4 py-[11px] pr-11 text-[14px] text-white placeholder-zinc-600 focus:outline-none focus:border-nofx-gold/50 focus:bg-zinc-900 focus:ring-2 focus:ring-nofx-gold/20 transition-all"
className="w-full bg-nofx-bg-lighter border border-[rgba(26,24,19,0.14)] rounded-lg px-4 py-[11px] pr-11 text-[14px] text-nofx-text placeholder-nofx-text-muted focus:outline-none focus:border-nofx-gold/50 focus:bg-nofx-bg-lighter focus:ring-2 focus:ring-nofx-gold/20 transition-all"
placeholder="••••••••"
required
autoComplete="current-password"
@@ -229,7 +229,7 @@ export function LoginPage() {
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-500 hover:text-zinc-300 transition-colors"
className="absolute right-3 top-1/2 -translate-y-1/2 text-nofx-text-muted hover:text-nofx-text transition-colors"
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
@@ -239,8 +239,8 @@ export function LoginPage() {
{/* Error banner */}
{error && (
<div className="flex items-start gap-2 rounded-lg border border-red-500/25 bg-red-500/[0.08] px-3 py-2.5 text-xs text-red-300">
<span className="text-red-400 font-bold mt-px">!</span>
<div className="flex items-start gap-2 rounded-lg border border-nofx-danger/25 bg-nofx-danger/[0.08] px-3 py-2.5 text-xs text-nofx-danger">
<span className="text-nofx-danger font-bold mt-px">!</span>
<span className="leading-relaxed">{error}</span>
</div>
)}
@@ -249,7 +249,7 @@ export function LoginPage() {
<button
type="submit"
disabled={loading}
className="group mt-2 flex w-full items-center justify-center gap-2 rounded-lg bg-nofx-gold py-[11px] text-sm font-semibold text-black shadow-lg shadow-nofx-gold/10 transition-all hover:bg-yellow-400 active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-60"
className="group mt-2 flex w-full items-center justify-center gap-2 rounded-lg bg-nofx-gold py-[11px] text-sm font-semibold text-nofx-bg transition-all hover:bg-nofx-gold-highlight active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-60"
>
{loading ? (
<>
@@ -269,12 +269,12 @@ export function LoginPage() {
</form>
{/* Footer */}
<div className="mt-8 pt-5 border-t border-white/[0.06] flex items-center justify-between text-[11px]">
<span className="font-mono text-zinc-600">v1.0</span>
<div className="mt-8 pt-5 border-t border-[rgba(26,24,19,0.14)] flex items-center justify-between text-[11px]">
<span className="font-mono text-nofx-text-muted">v1.0</span>
<button
type="button"
onClick={handleResetAccount}
className="text-zinc-600 transition-colors hover:text-red-400"
className="text-nofx-text-muted transition-colors hover:text-nofx-danger"
>
{t('forgotAccount', language)}
</button>
@@ -289,10 +289,10 @@ export function LoginPage() {
function Stat({ value, label }: { value: string; label: string }) {
return (
<div>
<div className="font-mono text-2xl xl:text-3xl font-bold text-white">
<div className="font-mono text-2xl xl:text-3xl font-bold text-nofx-text">
{value}
</div>
<div className="mt-1 text-[10.5px] uppercase tracking-[0.14em] text-zinc-500">
<div className="mt-1 text-[10.5px] uppercase tracking-[0.14em] text-nofx-text-muted">
{label}
</div>
</div>

View File

@@ -46,11 +46,11 @@ export function LoginRequiredOverlay({
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"
className="relative max-w-md w-full overflow-hidden bg-nofx-bg-lighter border border-nofx-gold/30 shadow-lg 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 justify-between px-3 py-2 bg-nofx-bg-deeper 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">
@@ -74,8 +74,7 @@ export function LoginRequiredOverlay({
{/* 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)]">
<div className="bg-nofx-bg-lighter border border-nofx-danger/50 text-nofx-danger px-4 py-2 flex items-center gap-3">
<AlertTriangle size={18} className="animate-pulse" />
<span className="font-bold tracking-widest text-sm uppercase">
{tr('accessDenied')}
@@ -87,7 +86,7 @@ export function LoginRequiredOverlay({
{/* 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">
<h2 className="text-xl font-bold text-nofx-text uppercase tracking-wider mb-2">
{tr('title')}
</h2>
<p className="text-nofx-gold text-xs uppercase tracking-widest border-b border-nofx-gold/20 pb-4 inline-block">
@@ -95,9 +94,9 @@ export function LoginRequiredOverlay({
</p>
</div>
<div className="bg-nofx-bg-lighter border-l-2 border-nofx-gold/20 p-3 my-4">
<div className="bg-nofx-bg-deeper 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>
<span className="text-nofx-success mr-2">$</span>
{tr('description')}
</p>
</div>
@@ -118,7 +117,7 @@ export function LoginRequiredOverlay({
<div className="space-y-3">
<Link
to="/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"
className="flex items-center justify-center gap-2 w-full py-3 bg-nofx-gold text-nofx-bg font-bold text-xs uppercase tracking-widest hover:bg-nofx-gold-highlight transition-all group"
>
<LogIn size={14} />
<span>{tr('loginButton')}</span>
@@ -129,7 +128,7 @@ export function LoginRequiredOverlay({
<Link
to="/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"
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-nofx-text hover:border-nofx-gold font-bold text-xs uppercase tracking-widest transition-all hover:bg-nofx-gold/10"
>
<UserPlus size={14} />
<span>{tr('registerButton')}</span>
@@ -139,7 +138,7 @@ export function LoginRequiredOverlay({
<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"
className="text-[10px] text-nofx-text-muted hover:text-nofx-danger uppercase tracking-widest hover:underline decoration-nofx-danger/30"
>
[ {tr('abort')} ]
</button>

View File

@@ -21,25 +21,25 @@ export function OnboardingModeSelector({
}> = [
{
id: 'beginner',
title: isZh ? '新手模式' : 'Beginner Mode',
badge: isZh ? '推荐' : 'Recommended',
title: isZh ? 'Beginner Mode' : 'Beginner Mode',
badge: isZh ? 'Recommended' : 'Recommended',
description: isZh
? '自动生成 Base 钱包,默认接入 Claw402 + GLM,最快完成首次启动。'
? 'Generate a Base wallet automatically and start with Claw402 + GLM by default.'
: 'Generate a Base wallet automatically and start with Claw402 + GLM by default.',
},
{
id: 'advanced',
title: isZh ? '老手模式' : 'Advanced Mode',
title: isZh ? 'Advanced Mode' : 'Advanced Mode',
description: isZh
? '保持现在的完整配置流程,你自己决定模型、钱包和交易所。'
? 'Keep the full manual flow and configure models, wallets, and exchanges yourself.'
: 'Keep the full manual flow and configure models, wallets, and exchanges yourself.',
},
]
return (
<div className="space-y-2">
<div className="text-xs font-medium text-zinc-400">
{isZh ? '使用模式' : 'Experience'}
<div className="text-xs font-medium text-nofx-text-muted">
{isZh ? 'Experience' : 'Experience'}
</div>
<div className="grid grid-cols-1 gap-2">
{options.map((option) => {
@@ -51,19 +51,19 @@ export function OnboardingModeSelector({
onClick={() => onChange(option.id)}
className={`w-full rounded-xl border px-4 py-3 text-left transition-all ${
selected
? 'border-nofx-gold/60 bg-nofx-gold/10 shadow-[0_0_0_1px_rgba(240,185,11,0.15)]'
: 'border-zinc-800 bg-zinc-950/60 hover:border-zinc-700'
? 'border-nofx-gold/60 bg-nofx-gold/10'
: 'border-[rgba(26,24,19,0.14)] bg-nofx-bg-lighter hover:border-nofx-gold/40'
}`}
>
<div className="flex items-center gap-2 text-sm font-semibold text-white">
<div className="flex items-center gap-2 text-sm font-semibold text-nofx-text">
<span>{option.title}</span>
{option.badge ? (
<span className="rounded-full bg-nofx-gold px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-black">
<span className="rounded-full bg-nofx-gold px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-nofx-bg">
{option.badge}
</span>
) : null}
</div>
<p className="mt-1 text-xs leading-5 text-zinc-400">
<p className="mt-1 text-xs leading-5 text-nofx-text-muted">
{option.description}
</p>
</button>

View File

@@ -1,25 +1,25 @@
import { describe, it, expect } from 'vitest'
/**
* PR #XXX 测试: 修复密码校验不一致的问题
* PR #XXX test: fix inconsistent password validation
*
* 问题:RegisterPage 中存在两处密码校验逻辑:
* 1. PasswordChecklist 组件提供的可视化校验
* 2. 自定义的 isStrongPassword 函数
* 这导致校验规则可能不一致
* Problem: RegisterPage had two password validation paths:
* 1. Visual validation provided by the PasswordChecklist component
* 2. A custom isStrongPassword function
* This could cause the validation rules to diverge.
*
* 修复:移除重复的 isStrongPassword 函数,统一使用 PasswordChecklist 的校验结果
* Fix: remove the duplicate isStrongPassword function and rely solely on the PasswordChecklist result.
*
* 本测试专注于验证密码校验逻辑的一致性,确保:
* 1. 移除了重复的 isStrongPassword 函数
* 2. 使用统一的 PasswordChecklist 校验
* 3. 特殊字符规则在正常显示和错误提示中保持一致
* This test focuses on verifying the consistency of the password validation logic, ensuring:
* 1. The duplicate isStrongPassword function is removed
* 2. A single PasswordChecklist validation is used
* 3. The special character rule stays consistent between normal display and error messages
*/
describe('RegisterPage - Password Validation Consistency (Logic Tests)', () => {
/**
* 测试密码校验规则逻辑
* 这些测试验证密码校验的核心逻辑,与 PasswordChecklist 组件的规则一致
* Test the password validation rule logic
* These tests verify the core validation logic, consistent with the PasswordChecklist component rules
*/
describe('password validation rules', () => {
it('should validate minimum 8 characters', () => {
@@ -57,7 +57,7 @@ describe('RegisterPage - Password Validation Consistency (Logic Tests)', () => {
})
it('should require special character from allowed set', () => {
// 根据 RegisterPage.tsx 中的设置,特殊字符正则为 /[@#$%!&*?]/
// Per the RegisterPage.tsx config, the special character regex is /[@#$%!&*?]/
const hasSpecialChar = (pwd: string) => /[@#$%!&*?]/.test(pwd)
expect(hasSpecialChar('NoSpecial123')).toBe(false)
@@ -70,7 +70,7 @@ describe('RegisterPage - Password Validation Consistency (Logic Tests)', () => {
expect(hasSpecialChar('HasStar123*')).toBe(true)
expect(hasSpecialChar('HasQuestion123?')).toBe(true)
// 不在允许列表中的特殊字符应该不通过
// Special characters not in the allowed list should fail
expect(hasSpecialChar('HasCaret123^')).toBe(false)
expect(hasSpecialChar('HasTilde123~')).toBe(false)
})
@@ -86,8 +86,8 @@ describe('RegisterPage - Password Validation Consistency (Logic Tests)', () => {
})
/**
* 测试完整的密码强度校验
* 模拟 PasswordChecklist 的完整校验逻辑
* Test the complete password strength validation
* Simulates the full PasswordChecklist validation logic
*/
describe('complete password strength validation', () => {
const validatePassword = (
@@ -193,22 +193,22 @@ describe('RegisterPage - Password Validation Consistency (Logic Tests)', () => {
})
/**
* 测试特殊字符一致性
* 确保在 RegisterPage 的正常显示(第 229-251 行)和错误提示(第 300-323 行)中
* 使用相同的特殊字符正则 /[@#$%!&*?]/
* Test special character consistency
* Ensures the normal display (lines 229-251) and error messages (lines 300-323) in RegisterPage
* use the same special character regex /[@#$%!&*?]/
*/
describe('special character consistency', () => {
it('should use consistent special character regex across all validations', () => {
// RegisterPage 中两处 PasswordChecklist 都应该使用相同的 specialCharsRegex
// Both PasswordChecklist instances in RegisterPage should use the same specialCharsRegex
const specialCharsRegex = /[@#$%!&*?]/
// 测试允许的特殊字符
// Test the allowed special characters
const validSpecialChars = ['@', '#', '$', '%', '!', '&', '*', '?']
validSpecialChars.forEach((char) => {
expect(specialCharsRegex.test(char)).toBe(true)
})
// 测试不允许的特殊字符
// Test the disallowed special characters
const invalidSpecialChars = ['^', '~', '`', '(', ')', '-', '_', '=', '+']
invalidSpecialChars.forEach((char) => {
expect(specialCharsRegex.test(char)).toBe(false)
@@ -254,7 +254,7 @@ describe('RegisterPage - Password Validation Consistency (Logic Tests)', () => {
})
/**
* 测试边界情况
* Test edge cases
*/
describe('edge cases', () => {
const validatePassword = (pwd: string, confirmPwd: string): boolean => {
@@ -311,16 +311,16 @@ describe('RegisterPage - Password Validation Consistency (Logic Tests)', () => {
})
/**
* 测试重构后的一致性
* 确保移除 isStrongPassword 函数后,所有校验都通过 PasswordChecklist
* Test consistency after refactoring
* Ensures that after removing the isStrongPassword function, all validation goes through PasswordChecklist
*/
describe('refactoring consistency verification', () => {
it('should have removed duplicate isStrongPassword function', () => {
// 这个测试验证重构的意图:
// 在重构之前,存在一个 isStrongPassword 函数
// 重构后应该移除该函数,只使用 PasswordChecklist 的校验
// This test verifies the intent of the refactor:
// Before the refactor there was an isStrongPassword function
// After the refactor it should be removed, using only PasswordChecklist validation
// 我们通过模拟 PasswordChecklist 的逻辑来验证一致性
// We verify consistency by simulating the PasswordChecklist logic
const passwordChecklistValidation = (pwd: string, confirm: string) => {
return {
minLength: pwd.length >= 8,
@@ -332,7 +332,7 @@ describe('RegisterPage - Password Validation Consistency (Logic Tests)', () => {
}
}
// 测试几个密码
// Test a few passwords
const testCases = [
{ pwd: 'Weak', confirm: 'Weak', shouldPass: false },
{ pwd: 'StrongPass123!', confirm: 'StrongPass123!', shouldPass: true },
@@ -351,7 +351,7 @@ describe('RegisterPage - Password Validation Consistency (Logic Tests)', () => {
})
it('should use consistent validation logic across the component', () => {
// 验证校验逻辑的一致性
// Verify the consistency of the validation logic
const validation1 = {
minLength: 8,
requireCapital: true,
@@ -361,7 +361,7 @@ describe('RegisterPage - Password Validation Consistency (Logic Tests)', () => {
specialCharsRegex: /[@#$%!&*?]/,
}
// 在 RegisterPage 的正常显示和错误提示中应该使用相同的配置
// The normal display and error messages in RegisterPage should use the same config
const validation2 = {
minLength: 8,
requireCapital: true,

View File

@@ -57,7 +57,7 @@ export function RegisterPage() {
}
if (betaMode && !betaCode.trim()) {
setError('内测期间,注册需要提供内测码')
setError('A beta code is required to register during the closed beta')
return
}
@@ -123,9 +123,9 @@ export function RegisterPage() {
<div className="flex justify-between items-center mb-8">
<button
onClick={() => navigate('/')}
className="flex items-center gap-2 text-zinc-500 hover:text-white transition-colors group px-3 py-1.5 rounded border border-transparent hover:border-zinc-700 bg-black/20 backdrop-blur-sm"
className="flex items-center gap-2 text-nofx-text-muted hover:text-nofx-text transition-colors group px-3 py-1.5 rounded border border-transparent hover:border-[rgba(26,24,19,0.14)] bg-nofx-bg-deeper backdrop-blur-sm"
>
<div className="w-2 h-2 rounded-full bg-red-500 group-hover:animate-pulse"></div>
<div className="w-2 h-2 rounded-full bg-nofx-danger group-hover:animate-pulse"></div>
<span className="text-xs font-mono uppercase tracking-widest">
&lt; ABORT_REGISTRATION
</span>
@@ -135,7 +135,6 @@ export function RegisterPage() {
<div className="mb-8 text-center">
<div className="flex justify-center mb-6">
<div className="relative">
<div className="absolute -inset-2 bg-nofx-gold/20 rounded-full blur-xl animate-pulse"></div>
<img
src="/icons/nofx.svg"
alt="NoFx Logo"
@@ -143,56 +142,54 @@ export function RegisterPage() {
/>
</div>
</div>
<h1 className="text-3xl font-bold tracking-tighter text-white uppercase mb-2">
<h1 className="text-3xl font-bold tracking-tighter text-nofx-text uppercase mb-2">
<span className="text-nofx-gold">NEW_USER</span> ONBOARDING
</h1>
<p className="text-zinc-500 text-xs tracking-[0.2em] uppercase">
<p className="text-nofx-text-muted text-xs tracking-[0.2em] uppercase">
Initializing Registration Sequence...
</p>
</div>
<div className="bg-zinc-900/40 backdrop-blur-md border border-zinc-800 rounded-lg overflow-hidden shadow-2xl relative group">
<div className="absolute inset-0 bg-zinc-900/50 opacity-0 group-hover:opacity-100 transition duration-700 pointer-events-none"></div>
<div className="flex items-center justify-between px-4 py-2 bg-zinc-900/80 border-b border-zinc-800">
<div className="bg-nofx-bg-lighter backdrop-blur-md border border-[rgba(26,24,19,0.14)] rounded-lg overflow-hidden shadow-lg relative group">
<div className="flex items-center justify-between px-4 py-2 bg-nofx-bg-deeper border-b border-[rgba(26,24,19,0.14)]">
<div className="flex gap-1.5">
<div
className="w-2.5 h-2.5 rounded-full bg-red-500/50 hover:bg-red-500 cursor-pointer transition-colors"
className="w-2.5 h-2.5 rounded-full bg-nofx-danger/50 hover:bg-nofx-danger cursor-pointer transition-colors"
onClick={() => navigate('/')}
title="Close / Return Home"
></div>
<div className="w-2.5 h-2.5 rounded-full bg-yellow-500/50"></div>
<div className="w-2.5 h-2.5 rounded-full bg-green-500/50"></div>
<div className="w-2.5 h-2.5 rounded-full bg-nofx-gold/50"></div>
<div className="w-2.5 h-2.5 rounded-full bg-nofx-success/50"></div>
</div>
<div className="text-[10px] text-zinc-600 font-mono flex items-center gap-1">
<span className="text-emerald-500"></span> setup_account.sh
<div className="text-[10px] text-nofx-text-muted font-mono flex items-center gap-1">
<span className="text-nofx-success"></span> setup_account.sh
</div>
</div>
<div className="p-6 md:p-8 relative">
<div className="mb-6 font-mono text-xs space-y-1 text-zinc-500 border-b border-zinc-800/50 pb-4">
<div className="mb-6 font-mono text-xs space-y-1 text-nofx-text-muted border-b border-[rgba(26,24,19,0.14)] pb-4">
<div className="flex gap-2">
<span className="text-emerald-500"></span>
<span className="text-nofx-success"></span>
<span>
System Check: <span className="text-emerald-500">READY</span>
System Check: <span className="text-nofx-success">READY</span>
</span>
</div>
<div className="flex gap-2">
<span className="text-emerald-500"></span>
<span className="text-nofx-success"></span>
<span>Mode: {betaMode ? 'CLOSED_BETA CA1' : 'PUBLIC'}</span>
</div>
</div>
<form onSubmit={handleRegister} className="space-y-5">
<div>
<label className="block text-xs uppercase tracking-wider text-zinc-500 mb-1.5 ml-1 font-bold">
<label className="block text-xs uppercase tracking-wider text-nofx-text-muted mb-1.5 ml-1 font-bold">
{t('email', language)}
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full bg-black/50 border border-zinc-700 rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-zinc-800 text-white font-mono"
className="w-full bg-nofx-bg border border-[rgba(26,24,19,0.14)] rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-nofx-text-muted text-nofx-text font-mono"
placeholder="user@nofx.os"
required
/>
@@ -200,7 +197,7 @@ export function RegisterPage() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-xs uppercase tracking-wider text-zinc-500 mb-1.5 ml-1 font-bold">
<label className="block text-xs uppercase tracking-wider text-nofx-text-muted mb-1.5 ml-1 font-bold">
{t('password', language)}
</label>
<div className="relative">
@@ -208,14 +205,14 @@ export function RegisterPage() {
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full bg-black/50 border border-zinc-700 rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-zinc-800 text-white font-mono pr-10"
className="w-full bg-nofx-bg border border-[rgba(26,24,19,0.14)] rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-nofx-text-muted text-nofx-text font-mono pr-10"
placeholder="••••••••"
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-600 hover:text-zinc-400 transition-colors"
className="absolute right-3 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>
@@ -223,7 +220,7 @@ export function RegisterPage() {
</div>
<div>
<label className="block text-xs uppercase tracking-wider text-zinc-500 mb-1.5 ml-1 font-bold">
<label className="block text-xs uppercase tracking-wider text-nofx-text-muted mb-1.5 ml-1 font-bold">
{t('confirmPassword', language)}
</label>
<div className="relative">
@@ -231,7 +228,7 @@ export function RegisterPage() {
type={showConfirmPassword ? 'text' : 'password'}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="w-full bg-black/50 border border-zinc-700 rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-zinc-800 text-white font-mono pr-10"
className="w-full bg-nofx-bg border border-[rgba(26,24,19,0.14)] rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-nofx-text-muted text-nofx-text font-mono pr-10"
placeholder="••••••••"
required
/>
@@ -240,7 +237,7 @@ export function RegisterPage() {
onClick={() =>
setShowConfirmPassword(!showConfirmPassword)
}
className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-600 hover:text-zinc-400 transition-colors"
className="absolute right-3 top-1/2 -translate-y-1/2 text-nofx-text-muted hover:text-nofx-text transition-colors"
>
{showConfirmPassword ? (
<EyeOff size={16} />
@@ -252,12 +249,12 @@ export function RegisterPage() {
</div>
</div>
<div className="bg-zinc-900/50 p-3 rounded border border-zinc-800/50">
<div className="text-[10px] uppercase tracking-wider text-zinc-500 mb-2 font-bold flex items-center gap-2">
<div className="w-1 h-1 rounded-full bg-zinc-500"></div>
<div className="bg-nofx-bg-deeper p-3 rounded border border-[rgba(26,24,19,0.14)]">
<div className="text-[10px] uppercase tracking-wider text-nofx-text-muted mb-2 font-bold flex items-center gap-2">
<div className="w-1 h-1 rounded-full bg-nofx-text-muted"></div>
Password Strength Protocol
</div>
<div className="text-xs font-mono text-zinc-400">
<div className="text-xs font-mono text-nofx-text-muted">
<PasswordChecklist
rules={[
'minLength',
@@ -298,19 +295,19 @@ export function RegisterPage() {
e.target.value.replace(/[^a-z0-9]/gi, '').toLowerCase()
)
}
className="w-full bg-black/50 border border-zinc-700 rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-zinc-800 text-white font-mono tracking-widest"
className="w-full bg-nofx-bg border border-[rgba(26,24,19,0.14)] rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-nofx-text-muted text-nofx-text font-mono tracking-widest"
placeholder="XXXXXX"
maxLength={6}
required={betaMode}
/>
<p className="text-[10px] text-zinc-600 font-mono mt-1 ml-1">
<p className="text-[10px] text-nofx-text-muted font-mono mt-1 ml-1">
* CASE SENSITIVE ALPHANUMERIC
</p>
</div>
)}
{error && (
<div className="text-xs bg-red-500/10 border border-red-500/30 text-red-500 px-3 py-2 rounded font-mono">
<div className="text-xs bg-nofx-danger/10 border border-nofx-danger/30 text-nofx-danger px-3 py-2 rounded font-mono">
[REGISTRATION_ERROR]: {error}
</div>
)}
@@ -320,7 +317,7 @@ export function RegisterPage() {
disabled={
loading || (betaMode && !betaCode.trim()) || !passwordValid
}
className="w-full bg-nofx-gold text-black font-bold py-3 px-4 rounded text-sm tracking-wide uppercase hover:bg-yellow-400 transition-all transform active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed font-mono shadow-[0_0_15px_rgba(255,215,0,0.1)] hover:shadow-[0_0_25px_rgba(255,215,0,0.25)] flex items-center justify-center gap-2 group mt-4"
className="w-full bg-nofx-gold text-nofx-bg font-bold py-3 px-4 rounded text-sm tracking-wide uppercase hover:bg-nofx-gold-highlight transition-all transform active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed font-mono flex items-center justify-center gap-2 group mt-4"
>
{loading ? (
<span className="animate-pulse">INITIALIZING...</span>
@@ -336,25 +333,25 @@ export function RegisterPage() {
</form>
</div>
<div className="bg-zinc-900/50 p-3 flex justify-between items-center text-[10px] font-mono text-zinc-600 border-t border-zinc-800">
<div className="bg-nofx-bg-deeper p-3 flex justify-between items-center text-[10px] font-mono text-nofx-text-muted border-t border-[rgba(26,24,19,0.14)]">
<div>ENCRYPTION: AES-256</div>
<div>SECURE_REGISTRY</div>
</div>
</div>
<div className="text-center mt-8 space-y-4">
<p className="text-xs font-mono text-zinc-500">
<p className="text-xs font-mono text-nofx-text-muted">
EXISTING_OPERATOR?{' '}
<button
onClick={() => navigate('/login')}
className="text-nofx-gold hover:underline hover:text-yellow-300 transition-colors ml-1 uppercase"
className="text-nofx-gold hover:underline hover:text-nofx-gold-highlight transition-colors ml-1 uppercase"
>
ACCESS TERMINAL
</button>
</p>
<button
onClick={() => navigate('/')}
className="text-[10px] text-zinc-600 hover:text-red-500 transition-colors uppercase tracking-widest hover:underline decoration-red-500/30 font-mono"
className="text-[10px] text-nofx-text-muted hover:text-nofx-danger transition-colors uppercase tracking-widest hover:underline decoration-nofx-danger/30 font-mono"
>
[ ABORT_REGISTRATION_RETURN_HOME ]
</button>

View File

@@ -89,15 +89,15 @@ describe('RegistrationDisabled Component', () => {
it('should have correct background color', () => {
const { container } = renderComponent()
const mainDiv = container.firstChild as HTMLElement
// Browser converts hex to rgb
expect(mainDiv.style.background).toMatch(/rgb\(11,\s*14,\s*17\)|#0B0E11/i)
// Browser converts hex to rgb (cream paper theme)
expect(mainDiv.style.background).toMatch(/rgb\(241,\s*236,\s*226\)|#F1ECE2/i)
})
it('should have correct text color', () => {
const { container } = renderComponent()
const mainDiv = container.firstChild as HTMLElement
// Browser converts hex to rgb
expect(mainDiv.style.color).toMatch(/rgb\(234,\s*236,\s*239\)|#EAECEF/i)
// Browser converts hex to rgb (ink text)
expect(mainDiv.style.color).toMatch(/rgb\(26,\s*24,\s*19\)|#1A1813/i)
})
it('should have centered layout', () => {

View File

@@ -13,7 +13,7 @@ export function RegistrationDisabled() {
return (
<div
className="min-h-screen flex items-center justify-center"
style={{ background: '#0B0E11', color: '#EAECEF' }}
style={{ background: '#F1ECE2', color: '#1A1813' }}
>
<div className="text-center max-w-md px-6">
<img
@@ -24,12 +24,12 @@ export function RegistrationDisabled() {
<h1 className="text-2xl font-semibold mb-3">
{t('registrationClosed', language)}
</h1>
<p className="text-sm text-gray-400">
<p className="text-sm text-nofx-text-muted">
{t('registrationClosedMessage', language)}
</p>
<button
className="mt-6 px-4 py-2 rounded text-sm font-semibold transition-colors hover:opacity-90"
style={{ background: '#F0B90B', color: '#000' }}
style={{ background: '#E0483B', color: '#F1ECE2' }}
onClick={handleBackToLogin}
>
{t('backToLogin', language)}

View File

@@ -25,7 +25,7 @@ export function ResetPasswordPage() {
}
return (
<div className="min-h-screen" style={{ background: '#0B0E11' }}>
<div className="min-h-screen" style={{ background: '#F1ECE2' }}>
<Header simple />
<div
@@ -36,8 +36,8 @@ export function ResetPasswordPage() {
{/* Back to Login */}
<button
onClick={() => navigate('/login')}
className="flex items-center gap-2 mb-6 text-sm hover:text-[#F0B90B] transition-colors"
style={{ color: '#848E9C' }}
className="flex items-center gap-2 mb-6 text-sm hover:text-[#E0483B] transition-colors"
style={{ color: '#8A8478' }}
>
<ArrowLeft className="w-4 h-4" />
{t('backToLogin', language)}
@@ -47,11 +47,11 @@ export function ResetPasswordPage() {
<div className="text-center mb-8">
<div
className="w-16 h-16 mx-auto mb-4 flex items-center justify-center rounded-full"
style={{ background: 'rgba(240, 185, 11, 0.1)' }}
style={{ background: 'rgba(224, 72, 59, 0.1)' }}
>
<KeyRound className="w-8 h-8" style={{ color: '#F0B90B' }} />
<KeyRound className="w-8 h-8" style={{ color: '#E0483B' }} />
</div>
<h1 className="text-2xl font-bold" style={{ color: '#EAECEF' }}>
<h1 className="text-2xl font-bold" style={{ color: '#1A1813' }}>
{t('resetPasswordTitle', language)}
</h1>
</div>
@@ -59,22 +59,22 @@ export function ResetPasswordPage() {
{/* CLI recovery instructions */}
<div
className="rounded-lg p-6"
style={{ background: '#1E2329', border: '1px solid #2B3139' }}
style={{ background: '#F7F4EC', border: '1px solid rgba(26,24,19,0.14)' }}
>
<p
className="text-sm leading-relaxed mb-4"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
{t('resetPasswordCliIntro', language)}
</p>
<div
className="flex items-center justify-between gap-3 rounded px-3 py-3 font-mono text-xs"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
style={{ background: '#E8E2D5', border: '1px solid rgba(26,24,19,0.14)' }}
>
<code
className="break-all"
style={{ color: '#F0B90B' }}
style={{ color: '#E0483B' }}
>
{RESET_PASSWORD_COMMAND}
</code>
@@ -82,11 +82,11 @@ export function ResetPasswordPage() {
type="button"
onClick={handleCopy}
className="shrink-0 btn-icon"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
aria-label={t('copy', language)}
>
{copied ? (
<Check className="w-4 h-4" style={{ color: '#0ECB81' }} />
<Check className="w-4 h-4" style={{ color: '#2E8B57' }} />
) : (
<Copy className="w-4 h-4" />
)}
@@ -95,7 +95,7 @@ export function ResetPasswordPage() {
<p
className="text-xs leading-relaxed mt-4"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
{t('resetPasswordCliSecurityNote', language)}
</p>

View File

@@ -138,10 +138,10 @@ export function AdvancedChart({
// Indicator configuration
const [indicators, setIndicators] = useState<IndicatorConfig[]>([
{ id: 'volume', name: 'Volume', enabled: true, color: '#3B82F6' },
{ id: 'volume', name: 'Volume', enabled: true, color: '#E0483B' },
{ id: 'ma5', name: 'MA5', enabled: false, color: '#FF6B6B', params: { period: 5 } },
{ id: 'ma10', name: 'MA10', enabled: false, color: '#4ECDC4', params: { period: 10 } },
{ id: 'ma20', name: 'MA20', enabled: false, color: '#FFD93D', params: { period: 20 } },
{ id: 'ma20', name: 'MA20', enabled: false, color: '#E0483B', params: { period: 20 } },
{ id: 'ma60', name: 'MA60', enabled: false, color: '#95E1D3', params: { period: 60 } },
{ id: 'ema12', name: 'EMA12', enabled: false, color: '#A8E6CF', params: { period: 12 } },
{ id: 'ema26', name: 'EMA26', enabled: false, color: '#FFD3B6', params: { period: 26 } },
@@ -356,18 +356,18 @@ export function AdvancedChart({
width: chartContainerRef.current.clientWidth || 800,
height: chartContainerRef.current.clientHeight || height,
layout: {
background: { color: '#0B0E11' },
textColor: '#B7BDC6',
background: { color: '#F1ECE2' },
textColor: '#1A1813',
fontSize: 12,
},
grid: {
vertLines: {
color: 'rgba(43, 49, 57, 0.2)',
color: 'rgba(26, 24, 19, 0.08)',
style: 1,
visible: true,
},
horzLines: {
color: 'rgba(43, 49, 57, 0.2)',
color: 'rgba(26, 24, 19, 0.08)',
style: 1,
visible: true,
},
@@ -375,20 +375,20 @@ export function AdvancedChart({
crosshair: {
mode: 1,
vertLine: {
color: 'rgba(240, 185, 11, 0.5)',
color: 'rgba(224, 72, 59, 0.5)',
width: 1,
style: 2,
labelBackgroundColor: '#F0B90B',
labelBackgroundColor: '#E0483B',
},
horzLine: {
color: 'rgba(240, 185, 11, 0.5)',
color: 'rgba(224, 72, 59, 0.5)',
width: 1,
style: 2,
labelBackgroundColor: '#F0B90B',
labelBackgroundColor: '#E0483B',
},
},
rightPriceScale: {
borderColor: '#2B3139',
borderColor: 'rgba(26, 24, 19, 0.14)',
scaleMargins: {
top: 0.1,
bottom: 0.25,
@@ -397,7 +397,7 @@ export function AdvancedChart({
entireTextOnly: false,
},
timeScale: {
borderColor: '#2B3139',
borderColor: 'rgba(26, 24, 19, 0.14)',
timeVisible: true,
secondsVisible: false,
borderVisible: true,
@@ -433,18 +433,18 @@ export function AdvancedChart({
// Create candlestick series
const candlestickSeries = chart.addSeries(CandlestickSeries, {
upColor: '#0ECB81',
downColor: '#F6465D',
borderUpColor: '#0ECB81',
borderDownColor: '#F6465D',
wickUpColor: '#0ECB81',
wickDownColor: '#F6465D',
upColor: '#2E8B57',
downColor: '#D6433A',
borderUpColor: '#2E8B57',
borderDownColor: '#D6433A',
wickUpColor: '#2E8B57',
wickDownColor: '#D6433A',
})
candlestickSeriesRef.current = candlestickSeries as any
// Create volume series
const volumeSeries = chart.addSeries(HistogramSeries, {
color: '#26a69a',
color: '#2E8B57',
priceFormat: {
type: 'volume',
},
@@ -579,7 +579,7 @@ export function AdvancedChart({
const volumeData = klineData.map((k: Kline) => ({
time: k.time,
value: k.volume || 0,
color: k.close >= k.open ? 'rgba(14, 203, 129, 0.5)' : 'rgba(246, 70, 93, 0.5)',
color: k.close >= k.open ? 'rgba(46, 139, 87, 0.5)' : 'rgba(214, 67, 58, 0.5)',
}))
volumeSeriesRef.current.setData(volumeData)
} else {
@@ -666,7 +666,7 @@ export function AdvancedChart({
markers.push({
time: candleTime as Time,
position: 'belowBar' as const,
color: '#0ECB81',
color: '#2E8B57',
shape: 'circle' as const,
text: counts.buys > 1 ? `B${counts.buys}` : 'B',
size: 1,
@@ -677,7 +677,7 @@ export function AdvancedChart({
markers.push({
time: candleTime as Time,
position: 'aboveBar' as const,
color: '#F6465D',
color: '#D6433A',
shape: 'circle' as const,
text: counts.sells > 1 ? `S${counts.sells}` : 'S',
size: 1,
@@ -780,18 +780,18 @@ export function AdvancedChart({
const isLimit = order.type === 'LIMIT'
// Set price line style
let lineColor = '#F0B90B' // Default yellow
let lineColor = '#E0483B' // Default vermilion
const lineStyle = 2 // dashed
let title = ''
if (isStopLoss) {
lineColor = '#F6465D' // red - stop loss
lineColor = '#D6433A' // red - stop loss
title = `SL ${order.quantity}`
} else if (isTakeProfit) {
lineColor = '#0ECB81' // green - take profit
lineColor = '#2E8B57' // green - take profit
title = `TP ${order.quantity}`
} else if (isLimit) {
lineColor = '#F0B90B' // yellow - limit order
lineColor = '#E0483B' // vermilion - limit order
title = `Limit ${order.side} ${order.quantity}`
} else {
title = `${order.type} ${order.quantity}`
@@ -918,10 +918,10 @@ export function AdvancedChart({
<div
className="relative shadow-xl"
style={{
background: 'linear-gradient(180deg, #0F1215 0%, #0B0E11 100%)',
background: '#F1ECE2',
borderRadius: '12px',
overflow: 'hidden',
border: '1px solid rgba(43, 49, 57, 0.5)',
border: '1px solid rgba(26, 24, 19, 0.14)',
height: '100%',
display: 'flex',
flexDirection: 'column',
@@ -930,19 +930,19 @@ export function AdvancedChart({
{/* Compact Professional Header */}
<div
className="flex items-center justify-between px-4 py-2"
style={{ borderBottom: '1px solid rgba(43, 49, 57, 0.6)', background: '#0D1117', flexShrink: 0 }}
style={{ borderBottom: '1px solid rgba(26, 24, 19, 0.14)', background: '#F7F4EC', flexShrink: 0 }}
>
{/* Left: Symbol Info + Price */}
<div className="flex items-center gap-4">
{/* Symbol & Interval */}
<div className="flex items-center gap-2">
<span className="text-sm font-bold text-white">{symbol}</span>
<span className="text-[10px] px-1.5 py-0.5 rounded bg-[#1F2937] text-gray-400">{interval}</span>
<span className="text-sm font-bold text-nofx-text">{symbol}</span>
<span className="text-[10px] px-1.5 py-0.5 rounded bg-nofx-bg-deeper text-nofx-text-muted">{interval}</span>
<span
className="text-[10px] px-1.5 py-0.5 rounded font-medium uppercase"
style={{
background: exchange === 'hyperliquid' ? 'rgba(80, 227, 194, 0.1)' : 'rgba(243, 186, 47, 0.1)',
color: exchange === 'hyperliquid' ? '#50E3C2' : '#F3BA2F',
background: 'rgba(224, 72, 59, 0.1)',
color: '#E0483B',
}}
>
{exchange?.toUpperCase()}
@@ -951,10 +951,10 @@ export function AdvancedChart({
{/* Price Display */}
{marketStats && (
<div className="flex items-center gap-3 pl-3 border-l border-[#2B3139]">
<div className="flex items-center gap-3 pl-3 border-l border-[rgba(26,24,19,0.14)]">
<span
className="text-base font-bold tabular-nums"
style={{ color: marketStats.priceChange >= 0 ? '#10B981' : '#EF4444' }}
style={{ color: marketStats.priceChange >= 0 ? '#2E8B57' : '#D6433A' }}
>
{marketStats.price.toLocaleString(undefined, {
minimumFractionDigits: 2,
@@ -964,19 +964,19 @@ export function AdvancedChart({
<span
className="text-xs font-medium px-1.5 py-0.5 rounded tabular-nums"
style={{
background: marketStats.priceChange >= 0 ? 'rgba(16, 185, 129, 0.1)' : 'rgba(239, 68, 68, 0.1)',
color: marketStats.priceChange >= 0 ? '#10B981' : '#EF4444',
background: marketStats.priceChange >= 0 ? 'rgba(46, 139, 87, 0.1)' : 'rgba(214, 67, 58, 0.1)',
color: marketStats.priceChange >= 0 ? '#2E8B57' : '#D6433A',
}}
>
{marketStats.priceChange >= 0 ? '+' : ''}{marketStats.priceChangePercent.toFixed(2)}%
</span>
{/* Compact H/L */}
<div className="flex items-center gap-2 text-[11px] text-gray-500">
<span>H <span className="text-gray-300">{marketStats.high.toFixed(2)}</span></span>
<span>L <span className="text-gray-300">{marketStats.low.toFixed(2)}</span></span>
<div className="flex items-center gap-2 text-[11px] text-nofx-text-muted">
<span>H <span className="text-nofx-text">{marketStats.high.toFixed(2)}</span></span>
<span>L <span className="text-nofx-text">{marketStats.low.toFixed(2)}</span></span>
{marketStats.volume > 0 && baseUnit && (
<span>Vol <span className="text-gray-300">{formatVolume(marketStats.volume)}</span></span>
<span>Vol <span className="text-nofx-text">{formatVolume(marketStats.volume)}</span></span>
)}
</div>
</div>
@@ -986,7 +986,7 @@ export function AdvancedChart({
{/* Right: Controls */}
<div className="flex items-center gap-1.5">
{loading && (
<span className="text-[10px] text-yellow-400 animate-pulse mr-2">
<span className="text-[10px] text-nofx-gold animate-pulse mr-2">
{t('advancedChart.updating', language)}
</span>
)}
@@ -994,8 +994,8 @@ export function AdvancedChart({
onClick={() => setShowIndicatorPanel(!showIndicatorPanel)}
className="flex items-center gap-1 px-2 py-1 rounded text-[11px] font-medium transition-all"
style={{
background: showIndicatorPanel ? 'rgba(96, 165, 250, 0.15)' : 'transparent',
color: showIndicatorPanel ? '#60A5FA' : '#6B7280',
background: showIndicatorPanel ? 'rgba(224, 72, 59, 0.12)' : 'transparent',
color: showIndicatorPanel ? '#E0483B' : '#8A8478',
}}
>
<Settings className="w-3 h-3" />
@@ -1006,8 +1006,8 @@ export function AdvancedChart({
onClick={() => setShowOrderMarkers(!showOrderMarkers)}
className="flex items-center gap-1 px-2 py-1 rounded text-[11px] font-medium transition-all"
style={{
background: showOrderMarkers ? 'rgba(16, 185, 129, 0.15)' : 'transparent',
color: showOrderMarkers ? '#10B981' : '#6B7280',
background: showOrderMarkers ? 'rgba(46, 139, 87, 0.15)' : 'transparent',
color: showOrderMarkers ? '#2E8B57' : '#8A8478',
}}
title={t('advancedChart.orderMarkers', language)}
>
@@ -1021,8 +1021,8 @@ export function AdvancedChart({
<div
className="absolute top-16 right-4 z-10 rounded-lg shadow-2xl backdrop-blur-sm"
style={{
background: 'linear-gradient(135deg, #1A1E23 0%, #0F1215 100%)',
border: '1px solid rgba(240, 185, 11, 0.2)',
background: '#F7F4EC',
border: '1px solid rgba(224, 72, 59, 0.2)',
maxHeight: '500px',
minWidth: '280px',
overflowY: 'auto',
@@ -1031,17 +1031,17 @@ export function AdvancedChart({
{/* Title bar */}
<div
className="flex items-center justify-between px-4 py-3 border-b"
style={{ borderColor: 'rgba(43, 49, 57, 0.5)' }}
style={{ borderColor: 'rgba(26, 24, 19, 0.14)' }}
>
<div className="flex items-center gap-2">
<BarChart2 className="w-4 h-4 text-yellow-400" />
<h4 className="text-sm font-bold text-white">
<BarChart2 className="w-4 h-4 text-nofx-gold" />
<h4 className="text-sm font-bold text-nofx-text">
{t('advancedChart.technicalIndicators', language)}
</h4>
</div>
<button
onClick={() => setShowIndicatorPanel(false)}
className="text-gray-400 hover:text-white transition-colors"
className="text-nofx-text-muted hover:text-nofx-text transition-colors"
>
<span className="text-lg">×</span>
</button>
@@ -1052,25 +1052,25 @@ export function AdvancedChart({
{indicators.map(indicator => (
<label
key={indicator.id}
className="flex items-center gap-3 p-2.5 rounded-md hover:bg-white/5 cursor-pointer transition-all group"
className="flex items-center gap-3 p-2.5 rounded-md hover:bg-black/5 cursor-pointer transition-all group"
>
<div className="relative">
<input
type="checkbox"
checked={indicator.enabled}
onChange={() => toggleIndicator(indicator.id)}
className="w-4 h-4 rounded border-gray-600 text-yellow-500 focus:ring-2 focus:ring-yellow-500/50"
className="w-4 h-4 rounded border-[rgba(26,24,19,0.3)] text-nofx-gold focus:ring-2 focus:ring-nofx-gold/50"
/>
</div>
<div
className="w-8 h-3 rounded-sm border border-white/10"
className="w-8 h-3 rounded-sm border border-[rgba(26,24,19,0.14)]"
style={{ backgroundColor: indicator.color }}
></div>
<span className="text-sm text-gray-300 group-hover:text-white transition-colors flex-1">
<span className="text-sm text-nofx-text-muted group-hover:text-nofx-text transition-colors flex-1">
{indicator.name}
</span>
{indicator.enabled && (
<span className="text-xs text-yellow-400"></span>
<span className="text-xs text-nofx-gold"></span>
)}
</label>
))}
@@ -1078,8 +1078,8 @@ export function AdvancedChart({
{/* Bottom hint */}
<div
className="px-4 py-2 text-xs text-gray-500 border-t"
style={{ borderColor: 'rgba(43, 49, 57, 0.5)' }}
className="px-4 py-2 text-xs text-nofx-text-muted border-t"
style={{ borderColor: 'rgba(26, 24, 19, 0.14)' }}
>
{t('advancedChart.clickToToggle', language)}
</div>
@@ -1099,19 +1099,19 @@ export function AdvancedChart({
left: '10px',
top: '10px',
padding: '8px 12px',
background: 'rgba(15, 18, 21, 0.95)',
border: '1px solid rgba(240, 185, 11, 0.3)',
background: 'rgba(247, 244, 236, 0.95)',
border: '1px solid rgba(224, 72, 59, 0.3)',
borderRadius: '6px',
color: '#EAECEF',
color: '#1A1813',
fontSize: '12px',
fontFamily: 'monospace',
pointerEvents: 'none',
zIndex: 10,
backdropFilter: 'blur(10px)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.5)',
boxShadow: '0 4px 12px rgba(26, 24, 19, 0.15)',
}}
>
<div style={{ marginBottom: '6px', color: '#F0B90B', fontWeight: 'bold', fontSize: '11px' }}>
<div style={{ marginBottom: '6px', color: '#E0483B', fontWeight: 'bold', fontSize: '11px' }}>
{new Date((tooltipData.time as number) * 1000).toLocaleString(language === 'zh' ? 'zh-CN' : 'en-US', {
month: 'short',
day: 'numeric',
@@ -1120,18 +1120,18 @@ export function AdvancedChart({
})}
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '4px 12px', fontSize: '11px' }}>
<span style={{ color: '#848E9C' }}>O:</span>
<span style={{ color: '#EAECEF', fontWeight: '500' }}>{tooltipData.open?.toFixed(2)}</span>
<span style={{ color: '#8A8478' }}>O:</span>
<span style={{ color: '#1A1813', fontWeight: '500' }}>{tooltipData.open?.toFixed(2)}</span>
<span style={{ color: '#848E9C' }}>H:</span>
<span style={{ color: '#0ECB81', fontWeight: '500' }}>{tooltipData.high?.toFixed(2)}</span>
<span style={{ color: '#8A8478' }}>H:</span>
<span style={{ color: '#2E8B57', fontWeight: '500' }}>{tooltipData.high?.toFixed(2)}</span>
<span style={{ color: '#848E9C' }}>L:</span>
<span style={{ color: '#F6465D', fontWeight: '500' }}>{tooltipData.low?.toFixed(2)}</span>
<span style={{ color: '#8A8478' }}>L:</span>
<span style={{ color: '#D6433A', fontWeight: '500' }}>{tooltipData.low?.toFixed(2)}</span>
<span style={{ color: '#848E9C' }}>C:</span>
<span style={{ color: '#8A8478' }}>C:</span>
<span style={{
color: tooltipData.close >= tooltipData.open ? '#0ECB81' : '#F6465D',
color: tooltipData.close >= tooltipData.open ? '#2E8B57' : '#D6433A',
fontWeight: 'bold'
}}>
{tooltipData.close?.toFixed(2)}
@@ -1139,8 +1139,8 @@ export function AdvancedChart({
{tooltipData.volume > 0 && baseUnit && (
<>
<span style={{ color: '#848E9C' }}>V({baseUnit}):</span>
<span style={{ color: '#3B82F6', fontWeight: '500' }}>
<span style={{ color: '#8A8478' }}>V({baseUnit}):</span>
<span style={{ color: '#E0483B', fontWeight: '500' }}>
{formatVolume(tooltipData.volume)}
</span>
</>
@@ -1148,8 +1148,8 @@ export function AdvancedChart({
{tooltipData.quoteVolume > 0 && quoteUnit && (
<>
<span style={{ color: '#848E9C' }}>V({quoteUnit}):</span>
<span style={{ color: '#3B82F6', fontWeight: '500' }}>
<span style={{ color: '#8A8478' }}>V({quoteUnit}):</span>
<span style={{ color: '#E0483B', fontWeight: '500' }}>
{formatVolume(tooltipData.quoteVolume)}
</span>
</>
@@ -1173,10 +1173,9 @@ export function AdvancedChart({
style={{
fontSize: '56px',
fontWeight: '700',
color: 'rgba(240, 185, 11, 0.12)',
color: 'rgba(224, 72, 59, 0.12)',
letterSpacing: '4px',
fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, sans-serif',
textShadow: '0 2px 30px rgba(240, 185, 11, 0.2)',
}}
>
NOFX
@@ -1188,11 +1187,11 @@ export function AdvancedChart({
{error && (
<div
className="absolute inset-0 flex items-center justify-center"
style={{ background: 'rgba(11, 14, 17, 0.9)' }}
style={{ background: 'rgba(241, 236, 226, 0.9)' }}
>
<div className="text-center">
<div className="text-2xl mb-2"></div>
<div style={{ color: '#F6465D' }}>{error}</div>
<div style={{ color: '#D6433A' }}>{error}</div>
</div>
</div>
)}

View File

@@ -148,7 +148,7 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
}
return (
<div className={`nofx-glass rounded-lg border border-white/5 relative z-10 w-full flex flex-col transition-all duration-300 ${typeof window !== 'undefined' && window.innerWidth < 768 ? 'h-[500px]' : 'h-[600px]'
<div className={`nofx-glass rounded-lg border border-[rgba(26,24,19,0.1)] relative z-10 w-full flex flex-col transition-all duration-300 ${typeof window !== 'undefined' && window.innerWidth < 768 ? 'h-[500px]' : 'h-[600px]'
}`}>
{/*
Premium Professional Toolbar
@@ -156,16 +156,16 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
Desktop: Standard flex-wrap/nowrap
*/}
<div
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)' }}
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-nofx-bg/80 rounded-t-lg"
style={{ borderBottom: '1px solid rgba(26, 24, 19, 0.14)' }}
>
{/* Left: Tab Switcher */}
<div className="flex flex-wrap items-center gap-1">
<button
onClick={() => setActiveTab('equity')}
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'
? 'bg-nofx-gold/10 text-nofx-gold border border-nofx-gold/20'
: 'text-nofx-text-muted hover:text-nofx-text-main hover:bg-black/5'
}`}
>
<BarChart3 className="w-3.5 h-3.5" />
@@ -176,8 +176,8 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
<button
onClick={() => setActiveTab('kline')}
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'
? 'bg-nofx-gold/10 text-nofx-gold border border-nofx-gold/20'
: 'text-nofx-text-muted hover:text-nofx-text-main hover:bg-black/5'
}`}
>
<CandlestickChart className="w-3.5 h-3.5" />
@@ -187,7 +187,7 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
{/* Market Type Pills - Only when kline active, HIDDEN on mobile to save space */}
{activeTab === 'kline' && (
<div className="hidden md:flex items-center gap-1 ml-2 border-l border-white/10 pl-2">
<div className="hidden md:flex items-center gap-1 ml-2 border-l border-[rgba(26,24,19,0.14)] pl-2">
{(Object.keys(MARKET_CONFIG) as MarketType[]).map((type) => {
const config = MARKET_CONFIG[type]
const isActive = marketType === type
@@ -196,8 +196,8 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
key={type}
onClick={() => handleMarketTypeChange(type)}
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'
? 'bg-nofx-gold/10 text-nofx-gold border-nofx-gold/20'
: 'text-nofx-text-muted border-transparent hover:text-nofx-text-main hover:bg-black/5'
}`}
>
<span className="mr-1 opacity-70">{config.icon}</span>
@@ -218,22 +218,22 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
<>
<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"
className="flex items-center gap-1.5 px-2.5 py-1 bg-nofx-bg-deeper border border-[rgba(26,24,19,0.14)] rounded text-[11px] font-bold text-nofx-text-main hover:border-nofx-gold/30 hover:text-nofx-gold transition-all"
>
<span>{chartSymbolDisplay}</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">
<div className="absolute top-full right-0 mt-2 w-64 bg-nofx-bg-lighter border border-[rgba(26,24,19,0.14)] rounded-lg shadow-[0_10px_40px_-10px_rgba(26,24,19,0.2)] z-50 overflow-hidden nofx-glass ring-1 ring-[rgba(26,24,19,0.08)]">
<div className="p-2 border-b border-[rgba(26,24,19,0.1)]">
<div className="flex items-center gap-2 px-2 py-1.5 bg-nofx-bg-deeper rounded border border-[rgba(26,24,19,0.14)] 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"
className="flex-1 bg-transparent text-[11px] text-nofx-text placeholder-nofx-text-muted focus:outline-none font-mono"
autoFocus
/>
</div>
@@ -245,12 +245,12 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
const labels: Record<string, string> = { crypto: 'Crypto', stock: 'Stocks', forex: 'Forex', commodity: 'Commodities', index: 'Indices', pre_ipo: 'Pre-IPO' }
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>
<div className="px-3 py-1.5 text-[9px] font-bold text-nofx-text-muted/60 bg-black/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'}`}
className={`w-full px-3 py-2 text-left text-[11px] font-mono hover:bg-black/5 transition-all flex items-center justify-between ${chartSymbol === s.symbol ? 'bg-nofx-gold/10 text-nofx-gold' : 'text-nofx-text-muted'}`}
>
<span>{s.display || s.symbol}</span>
<span className="text-[9px] opacity-40">{s.name}</span>
@@ -264,19 +264,19 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
)}
</>
) : (
<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>
<span className="px-2.5 py-1 bg-nofx-bg-deeper border border-[rgba(26,24,19,0.14)] rounded text-[11px] font-bold text-nofx-text-main font-mono">{chartSymbol}</span>
)}
</div>
{/* 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">
<div className="flex items-center bg-nofx-bg-deeper rounded border border-[rgba(26,24,19,0.14)] 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-nofx-gold/20 text-nofx-gold'
: 'text-nofx-text-muted hover:text-white hover:bg-white/5'
: 'text-nofx-text-muted hover:text-nofx-text hover:bg-black/5'
}`}
>
{int.label}
@@ -291,9 +291,9 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
value={symbolInput}
onChange={(e) => setSymbolInput(e.target.value)}
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"
className="w-16 px-2 py-1 bg-nofx-bg-deeper border border-[rgba(26,24,19,0.14)] rounded-l text-[10px] text-nofx-text placeholder-nofx-text-muted focus:outline-none focus:border-nofx-gold/50 font-mono transition-colors"
/>
<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">
<button type="submit" className="px-2 py-1 bg-black/5 border border-[rgba(26,24,19,0.14)] border-l-0 rounded-r text-[10px] text-nofx-text-muted hover:text-nofx-text hover:bg-black/10 transition-all">
Go
</button>
</form>
@@ -302,7 +302,7 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
</div>
{/* Tab Content - Chart autosizes to this container */}
<div className="relative flex-1 bg-[#0B0E11]/50 rounded-b-lg overflow-hidden h-full min-h-0">
<div className="relative flex-1 bg-nofx-bg/50 rounded-b-lg overflow-hidden h-full min-h-0">
<AnimatePresence mode="wait">
{activeTab === 'equity' ? (
<motion.div

View File

@@ -210,21 +210,21 @@ export function ChartWithOrders({
width: chartContainerRef.current.clientWidth,
height: height,
layout: {
background: { color: '#0B0E11' },
textColor: '#EAECEF',
background: { color: '#F1ECE2' },
textColor: '#1A1813',
},
grid: {
vertLines: { color: 'rgba(43, 49, 57, 0.5)' },
horzLines: { color: 'rgba(43, 49, 57, 0.5)' },
vertLines: { color: 'rgba(26, 24, 19, 0.08)' },
horzLines: { color: 'rgba(26, 24, 19, 0.08)' },
},
crosshair: {
mode: 1, // Normal crosshair
},
rightPriceScale: {
borderColor: '#2B3139',
borderColor: 'rgba(26, 24, 19, 0.14)',
},
timeScale: {
borderColor: '#2B3139',
borderColor: 'rgba(26, 24, 19, 0.14)',
timeVisible: true,
secondsVisible: false,
},
@@ -246,12 +246,12 @@ export function ChartWithOrders({
// Create candlestick series (using v5 API)
const candlestickSeries = chart.addSeries(CandlestickSeries, {
upColor: '#0ECB81',
downColor: '#F6465D',
borderUpColor: '#0ECB81',
borderDownColor: '#F6465D',
wickUpColor: '#0ECB81',
wickDownColor: '#F6465D',
upColor: '#2E8B57',
downColor: '#D6433A',
borderUpColor: '#2E8B57',
borderDownColor: '#D6433A',
wickUpColor: '#2E8B57',
wickDownColor: '#D6433A',
})
candlestickSeriesRef.current = candlestickSeries as any
@@ -380,7 +380,7 @@ export function ChartWithOrders({
markers.push({
time: alignedTime as Time,
position: 'belowBar' as const,
color: isBuy ? '#0ECB81' : '#F6465D',
color: isBuy ? '#2E8B57' : '#D6433A',
shape: 'circle' as const,
text: isBuy ? 'B' : 'S',
price: order.price,
@@ -431,17 +431,17 @@ export function ChartWithOrders({
}, [symbol, interval, traderID, language])
return (
<div className="relative" style={{ background: '#0B0E11', borderRadius: '8px', overflow: 'hidden' }}>
<div className="relative" style={{ background: '#F1ECE2', borderRadius: '8px', overflow: 'hidden' }}>
{/* Title bar */}
<div className="flex items-center justify-between p-4" style={{ borderBottom: '1px solid #2B3139' }}>
<div className="flex items-center justify-between p-4" style={{ borderBottom: '1px solid rgba(26, 24, 19, 0.14)' }}>
<div className="flex items-center gap-3">
<span className="text-xl">📈</span>
<h3 className="text-lg font-bold" style={{ color: '#EAECEF' }}>
<h3 className="text-lg font-bold" style={{ color: '#1A1813' }}>
{symbol} {interval}
</h3>
</div>
{loading && (
<div className="text-sm" style={{ color: '#848E9C' }}>
<div className="text-sm" style={{ color: '#8A8478' }}>
{t('chartWithOrders.loading', language)}
</div>
)}
@@ -460,19 +460,19 @@ export function ChartWithOrders({
left: '10px',
top: '10px',
padding: '8px 12px',
background: 'rgba(15, 18, 21, 0.95)',
border: '1px solid rgba(240, 185, 11, 0.3)',
background: 'rgba(247, 244, 236, 0.95)',
border: '1px solid rgba(224, 72, 59, 0.3)',
borderRadius: '6px',
color: '#EAECEF',
color: '#1A1813',
fontSize: '12px',
fontFamily: 'monospace',
pointerEvents: 'none',
zIndex: 10,
backdropFilter: 'blur(10px)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.5)',
boxShadow: '0 4px 12px rgba(26, 24, 19, 0.15)',
}}
>
<div style={{ marginBottom: '6px', color: '#F0B90B', fontWeight: 'bold', fontSize: '11px' }}>
<div style={{ marginBottom: '6px', color: '#E0483B', fontWeight: 'bold', fontSize: '11px' }}>
{new Date((tooltipData.time as number) * 1000).toLocaleString(language === 'zh' ? 'zh-CN' : 'en-US', {
month: 'short',
day: 'numeric',
@@ -481,18 +481,18 @@ export function ChartWithOrders({
})}
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '4px 12px', fontSize: '11px' }}>
<span style={{ color: '#848E9C' }}>O:</span>
<span style={{ color: '#EAECEF', fontWeight: '500' }}>{tooltipData.open?.toFixed(2)}</span>
<span style={{ color: '#8A8478' }}>O:</span>
<span style={{ color: '#1A1813', fontWeight: '500' }}>{tooltipData.open?.toFixed(2)}</span>
<span style={{ color: '#848E9C' }}>H:</span>
<span style={{ color: '#0ECB81', fontWeight: '500' }}>{tooltipData.high?.toFixed(2)}</span>
<span style={{ color: '#8A8478' }}>H:</span>
<span style={{ color: '#2E8B57', fontWeight: '500' }}>{tooltipData.high?.toFixed(2)}</span>
<span style={{ color: '#848E9C' }}>L:</span>
<span style={{ color: '#F6465D', fontWeight: '500' }}>{tooltipData.low?.toFixed(2)}</span>
<span style={{ color: '#8A8478' }}>L:</span>
<span style={{ color: '#D6433A', fontWeight: '500' }}>{tooltipData.low?.toFixed(2)}</span>
<span style={{ color: '#848E9C' }}>C:</span>
<span style={{ color: '#8A8478' }}>C:</span>
<span style={{
color: tooltipData.close >= tooltipData.open ? '#0ECB81' : '#F6465D',
color: tooltipData.close >= tooltipData.open ? '#2E8B57' : '#D6433A',
fontWeight: 'bold'
}}>
{tooltipData.close?.toFixed(2)}
@@ -506,23 +506,23 @@ export function ChartWithOrders({
{error && (
<div
className="absolute inset-0 flex items-center justify-center"
style={{ background: 'rgba(11, 14, 17, 0.9)' }}
style={{ background: 'rgba(241, 236, 226, 0.9)' }}
>
<div className="text-center">
<div className="text-2xl mb-2"></div>
<div style={{ color: '#F6465D' }}>{error}</div>
<div style={{ color: '#D6433A' }}>{error}</div>
</div>
</div>
)}
{/* Legend */}
<div className="flex items-center gap-4 p-4 text-xs" style={{ borderTop: '1px solid #2B3139', color: '#848E9C' }}>
<div className="flex items-center gap-4 p-4 text-xs" style={{ borderTop: '1px solid rgba(26, 24, 19, 0.14)', color: '#8A8478' }}>
<div className="flex items-center gap-2">
<span className="font-bold" style={{ color: '#0ECB81' }}>B</span>
<span className="font-bold" style={{ color: '#2E8B57' }}>B</span>
<span>{t('chartWithOrders.buy', language)}</span>
</div>
<div className="flex items-center gap-2">
<span className="font-bold" style={{ color: '#F6465D' }}>S</span>
<span className="font-bold" style={{ color: '#D6433A' }}>S</span>
<span>{t('chartWithOrders.sell', language)}</span>
</div>
</div>

View File

@@ -26,7 +26,7 @@ export function ChartWithOrdersSimple({
setError(null)
try {
// 从我们自己的服务获取K线数据
// Fetch kline data from our own service
const limit = 100
const klineUrl = `/api/klines?symbol=${symbol}&interval=${interval}&limit=${limit}`
@@ -40,7 +40,7 @@ export function ChartWithOrdersSimple({
console.log('[ChartSimple] Received klines:', klineResult.data.length)
setKlineCount(klineResult.data.length)
// 测试获取订单数据
// Test fetching order data
if (traderID) {
const tradesUrl = `/api/trades?trader_id=${traderID}&symbol=${symbol}&limit=100`
console.log('[ChartSimple] Fetching trades from:', tradesUrl)
@@ -66,51 +66,51 @@ export function ChartWithOrdersSimple({
}, [symbol, interval, traderID])
return (
<div className="relative" style={{ background: '#0B0E11', borderRadius: '8px', overflow: 'hidden', minHeight: height }}>
{/* 标题栏 */}
<div className="flex items-center justify-between p-4" style={{ borderBottom: '1px solid #2B3139' }}>
<div className="relative" style={{ background: '#F1ECE2', borderRadius: '8px', overflow: 'hidden', minHeight: height }}>
{/* Title bar */}
<div className="flex items-center justify-between p-4" style={{ borderBottom: '1px solid rgba(26, 24, 19, 0.14)' }}>
<div className="flex items-center gap-3">
<span className="text-xl">📈</span>
<h3 className="text-lg font-bold" style={{ color: '#EAECEF' }}>
{symbol} {interval} ()
<h3 className="text-lg font-bold" style={{ color: '#1A1813' }}>
{symbol} {interval} (Test Mode)
</h3>
</div>
{loading && (
<div className="text-sm" style={{ color: '#848E9C' }}>
...
<div className="text-sm" style={{ color: '#8A8478' }}>
Loading...
</div>
)}
</div>
{/* 测试信息 */}
{/* Test info */}
<div className="p-8 space-y-4">
{error ? (
<div className="text-center">
<div className="text-2xl mb-2"></div>
<div style={{ color: '#F6465D' }}>{error}</div>
<div style={{ color: '#D6433A' }}>{error}</div>
</div>
) : (
<>
<div className="p-4 rounded" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
<div className="text-sm mb-2" style={{ color: '#848E9C' }}>K线数据</div>
<div className="text-2xl font-bold" style={{ color: '#0ECB81' }}>
{klineCount} K线
<div className="p-4 rounded" style={{ background: '#F7F4EC', border: '1px solid rgba(26, 24, 19, 0.14)' }}>
<div className="text-sm mb-2" style={{ color: '#8A8478' }}>Binance Kline Data</div>
<div className="text-2xl font-bold" style={{ color: '#2E8B57' }}>
{klineCount} klines
</div>
</div>
{traderID && (
<div className="p-4 rounded" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
<div className="text-sm mb-2" style={{ color: '#848E9C' }}></div>
<div className="text-2xl font-bold" style={{ color: '#F0B90B' }}>
{orderCount}
<div className="p-4 rounded" style={{ background: '#F7F4EC', border: '1px solid rgba(26, 24, 19, 0.14)' }}>
<div className="text-sm mb-2" style={{ color: '#8A8478' }}>Historical Order Data</div>
<div className="text-2xl font-bold" style={{ color: '#E0483B' }}>
{orderCount} orders
</div>
</div>
)}
<div className="p-4 rounded" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
<div className="text-sm mb-2" style={{ color: '#848E9C' }}></div>
<div className="text-lg" style={{ color: '#EAECEF' }}>
<div className="p-4 rounded" style={{ background: '#F7F4EC', border: '1px solid rgba(26, 24, 19, 0.14)' }}>
<div className="text-sm mb-2" style={{ color: '#8A8478' }}>Status</div>
<div className="text-lg" style={{ color: '#1A1813' }}>
Data fetched successfully, chart component in development
</div>
</div>
</>

View File

@@ -191,11 +191,11 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
<div className="flex flex-col items-center justify-center py-20">
<div className="relative">
<div className="w-16 h-16 border-4 border-t-transparent rounded-full animate-spin"
style={{ borderColor: '#F0B90B', borderTopColor: 'transparent' }} />
style={{ borderColor: '#E0483B', borderTopColor: 'transparent' }} />
<TrendingUp className="w-6 h-6 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
style={{ color: '#F0B90B' }} />
style={{ color: '#E0483B' }} />
</div>
<div className="text-sm mt-4 font-medium" style={{ color: '#848E9C' }}>
<div className="text-sm mt-4 font-medium" style={{ color: '#8A8478' }}>
{t('loadingChartData', language) || 'Loading chart data...'}
</div>
</div>
@@ -206,13 +206,13 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
return (
<div className="flex flex-col items-center justify-center py-20">
<div className="w-20 h-20 rounded-2xl flex items-center justify-center mb-4"
style={{ background: 'rgba(240, 185, 11, 0.1)' }}>
<BarChart3 className="w-10 h-10" style={{ color: '#F0B90B', opacity: 0.6 }} />
style={{ background: 'rgba(224, 72, 59, 0.1)' }}>
<BarChart3 className="w-10 h-10" style={{ color: '#E0483B', opacity: 0.6 }} />
</div>
<div className="text-lg font-bold mb-2" style={{ color: '#EAECEF' }}>
<div className="text-lg font-bold mb-2" style={{ color: '#1A1813' }}>
{t('noHistoricalData', language)}
</div>
<div className="text-sm text-center max-w-xs" style={{ color: '#848E9C' }}>
<div className="text-sm text-center max-w-xs" style={{ color: '#8A8478' }}>
{t('dataWillAppear', language)}
</div>
</div>
@@ -264,14 +264,14 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
<div
className="rounded-xl p-4 shadow-2xl backdrop-blur-sm"
style={{
background: 'rgba(30, 35, 41, 0.95)',
border: '1px solid rgba(240, 185, 11, 0.2)',
background: 'rgba(247, 244, 236, 0.95)',
border: '1px solid rgba(224, 72, 59, 0.2)',
minWidth: '200px'
}}
>
<div className="flex items-center gap-2 mb-3 pb-2" style={{ borderBottom: '1px solid #2B3139' }}>
<Zap className="w-3.5 h-3.5" style={{ color: '#F0B90B' }} />
<span className="text-xs font-medium" style={{ color: '#F0B90B' }}>
<div className="flex items-center gap-2 mb-3 pb-2" style={{ borderBottom: '1px solid rgba(26, 24, 19, 0.14)' }}>
<Zap className="w-3.5 h-3.5" style={{ color: '#E0483B' }} />
<span className="text-xs font-medium" style={{ color: '#E0483B' }}>
{dateStr} {data.time}
</span>
</div>
@@ -288,17 +288,17 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
<div className="w-2.5 h-2.5 rounded-full"
style={{ background: traderColor(trader.trader_id) }} />
<span className="text-xs font-medium truncate max-w-[100px]"
style={{ color: '#EAECEF' }}>
style={{ color: '#1A1813' }}>
{trader.trader_name}
</span>
</div>
<div className="text-right">
<div className="text-sm font-bold mono flex items-center gap-1"
style={{ color: isPositive ? '#0ECB81' : '#F6465D' }}>
style={{ color: isPositive ? '#2E8B57' : '#D6433A' }}>
{isPositive ? <TrendingUp className="w-3 h-3" /> : <TrendingDown className="w-3 h-3" />}
{isPositive ? '+' : ''}{pnlPct.toFixed(2)}%
</div>
<div className="text-[10px] mono" style={{ color: '#5E6673' }}>
<div className="text-[10px] mono" style={{ color: '#8A8478' }}>
${equity?.toFixed(2)}
</div>
</div>
@@ -346,10 +346,10 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
className="px-3 py-1.5 text-xs font-medium rounded-lg transition-all"
style={{
background: selectedPeriod === period.key
? 'rgba(240, 185, 11, 0.2)'
: 'rgba(43, 49, 57, 0.5)',
color: selectedPeriod === period.key ? '#F0B90B' : '#848E9C',
border: `1px solid ${selectedPeriod === period.key ? 'rgba(240, 185, 11, 0.4)' : '#2B3139'}`,
? 'rgba(224, 72, 59, 0.15)'
: 'rgba(26, 24, 19, 0.04)',
color: selectedPeriod === period.key ? '#E0483B' : '#8A8478',
border: `1px solid ${selectedPeriod === period.key ? 'rgba(224, 72, 59, 0.4)' : 'rgba(26, 24, 19, 0.14)'}`,
}}
>
{t(`comparisonChart.${period.key}`, language)}
@@ -363,17 +363,17 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
<div key={trader.trader_id}
className="flex items-center gap-2 px-3 py-1.5 rounded-full transition-all hover:scale-105"
style={{
background: idx === 0 ? 'rgba(240, 185, 11, 0.15)' : 'rgba(43, 49, 57, 0.5)',
border: `1px solid ${idx === 0 ? 'rgba(240, 185, 11, 0.3)' : '#2B3139'}`
background: idx === 0 ? 'rgba(224, 72, 59, 0.15)' : 'rgba(26, 24, 19, 0.04)',
border: `1px solid ${idx === 0 ? 'rgba(224, 72, 59, 0.3)' : 'rgba(26, 24, 19, 0.14)'}`
}}>
<div className="w-2 h-2 rounded-full"
style={{ background: traderColor(trader.trader_id) }} />
<span className="text-xs font-medium truncate max-w-[80px]"
style={{ color: '#EAECEF' }}>
style={{ color: '#1A1813' }}>
{trader.trader_name}
</span>
<span className="text-xs font-bold mono"
style={{ color: trader.currentPnl >= 0 ? '#0ECB81' : '#F6465D' }}>
style={{ color: trader.currentPnl >= 0 ? '#2E8B57' : '#D6433A' }}>
{trader.currentPnl >= 0 ? '+' : ''}{trader.currentPnl.toFixed(2)}%
</span>
</div>
@@ -383,7 +383,7 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
{/* Chart */}
<div className="relative rounded-xl overflow-hidden"
style={{ background: 'linear-gradient(180deg, rgba(11, 14, 17, 0.8) 0%, rgba(11, 14, 17, 1) 100%)' }}>
style={{ background: '#F1ECE2' }}>
{/* Watermark */}
<div style={{
position: 'absolute',
@@ -392,7 +392,7 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
transform: 'translate(-50%, -50%)',
fontSize: '80px',
fontWeight: 'bold',
color: 'rgba(240, 185, 11, 0.03)',
color: 'rgba(224, 72, 59, 0.04)',
zIndex: 1,
pointerEvents: 'none',
fontFamily: 'monospace',
@@ -427,20 +427,20 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
</filter>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#1E2329" vertical={false} />
<CartesianGrid strokeDasharray="3 3" stroke="rgba(26, 24, 19, 0.10)" vertical={false} />
<XAxis
dataKey="time"
stroke="#2B3139"
tick={{ fill: '#5E6673', fontSize: 10 }}
stroke="#6B6557"
tick={{ fill: '#6B6557', fontSize: 10 }}
tickLine={false}
axisLine={{ stroke: '#2B3139' }}
axisLine={{ stroke: 'rgba(26, 24, 19, 0.14)' }}
interval={Math.max(Math.floor(displayData.length / 8), 1)}
/>
<YAxis
stroke="#2B3139"
tick={{ fill: '#5E6673', fontSize: 10 }}
stroke="#6B6557"
tick={{ fill: '#6B6557', fontSize: 10 }}
tickLine={false}
axisLine={false}
domain={calculateYDomain()}
@@ -453,7 +453,7 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
{/* Zero reference line */}
<ReferenceLine
y={0}
stroke="#474D57"
stroke="rgba(26, 24, 19, 0.2)"
strokeDasharray="8 4"
strokeWidth={1}
/>
@@ -482,13 +482,11 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
activeDot={{
r: 6,
fill: traderColor(trader.trader_id),
stroke: '#0B0E11',
stroke: '#F1ECE2',
strokeWidth: 2,
filter: 'url(#glow)',
}}
name={trader.trader_name}
connectNulls
style={{ filter: idx === 0 ? 'url(#glow)' : undefined }}
/>
))}
@@ -515,10 +513,10 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
borderRadius: '50%',
backgroundColor: entry.color
}} />
<span style={{ color: '#EAECEF', fontSize: '12px', fontWeight: 500 }}>
<span style={{ color: '#1A1813', fontSize: '12px', fontWeight: 500 }}>
{entry.value}
<span style={{
color: pnl >= 0 ? '#0ECB81' : '#F6465D',
color: pnl >= 0 ? '#2E8B57' : '#D6433A',
marginLeft: '6px',
fontFamily: 'monospace'
}}>
@@ -539,36 +537,36 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
{/* Bottom Stats */}
<div className="grid grid-cols-4 gap-2">
<div className="p-3 rounded-lg text-center"
style={{ background: 'rgba(240, 185, 11, 0.05)', border: '1px solid rgba(240, 185, 11, 0.1)' }}>
<div className="text-[10px] uppercase tracking-wider mb-1" style={{ color: '#848E9C' }}>
style={{ background: 'rgba(224, 72, 59, 0.05)', border: '1px solid rgba(224, 72, 59, 0.1)' }}>
<div className="text-[10px] uppercase tracking-wider mb-1" style={{ color: '#8A8478' }}>
{t('leader', language)}
</div>
<div className="text-sm font-bold truncate" style={{ color: '#F0B90B' }}>
<div className="text-sm font-bold truncate" style={{ color: '#E0483B' }}>
{leader?.trader_name || '-'}
</div>
</div>
<div className="p-3 rounded-lg text-center" style={{ background: 'rgba(14, 203, 129, 0.05)' }}>
<div className="text-[10px] uppercase tracking-wider mb-1" style={{ color: '#848E9C' }}>
<div className="p-3 rounded-lg text-center" style={{ background: 'rgba(46, 139, 87, 0.05)' }}>
<div className="text-[10px] uppercase tracking-wider mb-1" style={{ color: '#8A8478' }}>
{t('leadPnL', language) || 'Lead PnL'}
</div>
<div className="text-sm font-bold mono"
style={{ color: (leader?.currentPnl || 0) >= 0 ? '#0ECB81' : '#F6465D' }}>
style={{ color: (leader?.currentPnl || 0) >= 0 ? '#2E8B57' : '#D6433A' }}>
{(leader?.currentPnl || 0) >= 0 ? '+' : ''}{(leader?.currentPnl || 0).toFixed(2)}%
</div>
</div>
<div className="p-3 rounded-lg text-center" style={{ background: 'rgba(96, 165, 250, 0.05)' }}>
<div className="text-[10px] uppercase tracking-wider mb-1" style={{ color: '#848E9C' }}>
<div className="p-3 rounded-lg text-center" style={{ background: 'rgba(26, 24, 19, 0.04)' }}>
<div className="text-[10px] uppercase tracking-wider mb-1" style={{ color: '#8A8478' }}>
{t('currentGap', language)}
</div>
<div className="text-sm font-bold mono" style={{ color: '#60a5fa' }}>
<div className="text-sm font-bold mono" style={{ color: '#1A1813' }}>
{gap}%
</div>
</div>
<div className="p-3 rounded-lg text-center" style={{ background: 'rgba(139, 92, 246, 0.05)' }}>
<div className="text-[10px] uppercase tracking-wider mb-1" style={{ color: '#848E9C' }}>
<div className="p-3 rounded-lg text-center" style={{ background: 'rgba(26, 24, 19, 0.04)' }}>
<div className="text-[10px] uppercase tracking-wider mb-1" style={{ color: '#8A8478' }}>
{t('dataPoints', language)}
</div>
<div className="text-sm font-bold mono" style={{ color: '#8b5cf6' }}>
<div className="text-sm font-bold mono" style={{ color: '#1A1813' }}>
{displayData.length}
</div>
</div>

View File

@@ -33,7 +33,7 @@ interface EquityPoint {
interface EquityChartProps {
traderId?: string
embedded?: boolean // 嵌入模式(不显示外层卡片)
embedded?: boolean // Embedded mode (does not show the outer card)
}
export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
@@ -45,7 +45,7 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
user && token && traderId ? `equity-history-${traderId}` : null,
() => api.getEquityHistory(traderId, true),
{
refreshInterval: 30000, // 30秒刷新历史数据更新频率较低
refreshInterval: 30000, // Refresh every 30s (historical data updates less frequently)
revalidateOnFocus: false,
dedupingInterval: 20000,
}
@@ -55,7 +55,7 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
user && token && traderId ? `account-${traderId}` : null,
() => api.getAccount(traderId, true),
{
refreshInterval: 15000, // 15秒刷新配合后端缓存
refreshInterval: 15000, // Refresh every 15s (matches backend cache)
revalidateOnFocus: false,
dedupingInterval: 10000,
}
@@ -66,7 +66,7 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
return (
<div className={embedded ? 'p-6' : 'binance-card p-6'}>
{!embedded && (
<h3 className="text-lg font-semibold mb-6" style={{ color: '#EAECEF' }}>
<h3 className="text-lg font-semibold mb-6" style={{ color: '#1A1813' }}>
{t('accountEquityCurve', language)}
</h3>
)}
@@ -83,16 +83,16 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
<div
className="flex items-center gap-3 p-4 rounded"
style={{
background: 'rgba(246, 70, 93, 0.1)',
border: '1px solid rgba(246, 70, 93, 0.2)',
background: 'rgba(214, 67, 58, 0.1)',
border: '1px solid rgba(214, 67, 58, 0.2)',
}}
>
<AlertTriangle className="w-6 h-6" style={{ color: '#F6465D' }} />
<AlertTriangle className="w-6 h-6" style={{ color: '#D6433A' }} />
<div>
<div className="font-semibold" style={{ color: '#F6465D' }}>
<div className="font-semibold" style={{ color: '#D6433A' }}>
{t('loadingError', language)}
</div>
<div className="text-sm" style={{ color: '#848E9C' }}>
<div className="text-sm" style={{ color: '#8A8478' }}>
{error.message}
</div>
</div>
@@ -101,18 +101,18 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
)
}
// 过滤掉无效数据total_equity为0或小于1的数据点API失败导致
// Filter out invalid data: points where total_equity is 0 or less than 1 (caused by API failures)
const validHistory = history?.filter((point) => point.total_equity > 1) || []
if (!validHistory || validHistory.length === 0) {
return (
<div className={embedded ? 'p-6' : 'binance-card p-6'}>
{!embedded && (
<h3 className="text-lg font-semibold mb-6" style={{ color: '#EAECEF' }}>
<h3 className="text-lg font-semibold mb-6" style={{ color: '#1A1813' }}>
{t('accountEquityCurve', language)}
</h3>
)}
<div className="text-center py-16" style={{ color: '#848E9C' }}>
<div className="text-center py-16" style={{ color: '#8A8478' }}>
<div className="mb-4 flex justify-center opacity-50">
<BarChart3 className="w-16 h-16" />
</div>
@@ -125,23 +125,23 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
)
}
// 限制显示最近的数据点(性能优化)
// 如果数据超过2000个点只显示最近2000
// Limit to the most recent data points (performance optimization)
// If there are more than 2000 points, only show the most recent 2000
const MAX_DISPLAY_POINTS = 2000
const displayHistory =
validHistory.length > MAX_DISPLAY_POINTS
? validHistory.slice(-MAX_DISPLAY_POINTS)
: validHistory
// 计算初始余额(优先从 account 获取配置的初始余额,备选从历史数据反推)
// Compute the initial balance (prefer the configured value from account, fall back to deriving from history)
const initialBalance =
account?.initial_balance || // 从交易员配置读取真实初始余额
account?.initial_balance || // Read the real initial balance from the trader config
(validHistory[0]
? validHistory[0].total_equity - validHistory[0].pnl
: undefined) || // 备选:淨值 - 盈亏
1000 // 默认值(与创建交易员时的默认配置一致)
: undefined) || // Fallback: equity - pnl
1000 // Default value (matches the default config used when creating a trader)
// 转换数据格式
// Transform the data format
const chartData = displayHistory.map((point, index) => {
const pnl = point.total_equity - initialBalance
const pnlPct = ((pnl / initialBalance) * 100).toFixed(2)
@@ -161,45 +161,45 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
const currentValue = chartData[chartData.length - 1]
const isProfit = currentValue.raw_pnl >= 0
// 计算Y轴范围
// Compute the Y-axis range
const calculateYDomain = () => {
if (displayMode === 'percent') {
// 百分比模式找到最大最小值留20%余量
// Percent mode: find the min/max values, leave a 20% margin
const values = chartData.map((d) => d.value)
const minVal = Math.min(...values)
const maxVal = Math.max(...values)
const range = Math.max(Math.abs(maxVal), Math.abs(minVal))
const padding = Math.max(range * 0.2, 1) // 至少留1%余量
const padding = Math.max(range * 0.2, 1) // Leave at least a 1% margin
return [Math.floor(minVal - padding), Math.ceil(maxVal + padding)]
} else {
// 美元模式以初始余额为基准上下留10%余量
// Dollar mode: anchor on the initial balance, leave a 10% margin above and below
const values = chartData.map((d) => d.value)
const minVal = Math.min(...values, initialBalance)
const maxVal = Math.max(...values, initialBalance)
const range = maxVal - minVal
const padding = Math.max(range * 0.15, initialBalance * 0.01) // 至少留1%余量
const padding = Math.max(range * 0.15, initialBalance * 0.01) // Leave at least a 1% margin
return [Math.floor(minVal - padding), Math.ceil(maxVal + padding)]
}
}
// 自定义Tooltip - Binance Style
// Custom Tooltip - Binance Style
const CustomTooltip = ({ active, payload }: any) => {
if (active && payload && payload.length) {
const data = payload[0].payload
return (
<div
className="rounded p-3 shadow-xl"
style={{ background: '#1E2329', border: '1px solid #2B3139' }}
style={{ background: '#F7F4EC', border: '1px solid rgba(26, 24, 19, 0.14)' }}
>
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>
<div className="text-xs mb-1" style={{ color: '#8A8478' }}>
Cycle #{data.cycle != null ? data.cycle : '—'}
</div>
<div className="font-bold mono" style={{ color: '#EAECEF' }}>
<div className="font-bold mono" style={{ color: '#1A1813' }}>
{data.raw_equity.toFixed(2)} USDT
</div>
<div
className="text-sm mono font-bold"
style={{ color: data.raw_pnl >= 0 ? '#0ECB81' : '#F6465D' }}
style={{ color: data.raw_pnl >= 0 ? '#2E8B57' : '#D6433A' }}
>
{data.raw_pnl >= 0 ? '+' : ''}
{data.raw_pnl.toFixed(2)} USDT ({data.raw_pnl_pct >= 0 ? '+' : ''}
@@ -219,7 +219,7 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
{!embedded && (
<h3
className="text-base sm:text-lg font-bold mb-2"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
{t('accountEquityCurve', language)}
</h3>
@@ -227,12 +227,12 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
<div className="flex flex-col sm:flex-row sm:items-baseline gap-2 sm:gap-4">
<span
className="text-2xl sm:text-3xl font-bold mono"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
{account?.total_equity.toFixed(2) || '0.00'}
<span
className="text-base sm:text-lg ml-1"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
USDT
</span>
@@ -241,14 +241,14 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
<span
className="text-sm sm:text-lg font-bold mono px-2 sm:px-3 py-1 rounded flex items-center gap-1"
style={{
color: isProfit ? '#0ECB81' : '#F6465D',
color: isProfit ? '#2E8B57' : '#D6433A',
background: isProfit
? 'rgba(14, 203, 129, 0.1)'
: 'rgba(246, 70, 93, 0.1)',
? 'rgba(46, 139, 87, 0.1)'
: 'rgba(214, 67, 58, 0.1)',
border: `1px solid ${
isProfit
? 'rgba(14, 203, 129, 0.2)'
: 'rgba(246, 70, 93, 0.2)'
? 'rgba(46, 139, 87, 0.2)'
: 'rgba(214, 67, 58, 0.2)'
}`,
}}
>
@@ -262,7 +262,7 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
</span>
<span
className="text-xs sm:text-sm mono"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
({isProfit ? '+' : ''}
{currentValue.raw_pnl.toFixed(2)} USDT)
@@ -274,7 +274,7 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
{/* Display Mode Toggle */}
<div
className="flex gap-0.5 sm:gap-1 rounded p-0.5 sm:p-1 self-start sm:self-auto"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
style={{ background: '#E8E2D5', border: '1px solid rgba(26, 24, 19, 0.14)' }}
>
<button
onClick={() => setDisplayMode('dollar')}
@@ -282,11 +282,10 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
style={
displayMode === 'dollar'
? {
background: '#F0B90B',
color: '#000',
boxShadow: '0 2px 8px rgba(240, 185, 11, 0.4)',
background: '#E0483B',
color: '#F1ECE2',
}
: { background: 'transparent', color: '#848E9C' }
: { background: 'transparent', color: '#8A8478' }
}
>
<DollarSign className="w-4 h-4" /> USDT
@@ -297,11 +296,10 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
style={
displayMode === 'percent'
? {
background: '#F0B90B',
color: '#000',
boxShadow: '0 2px 8px rgba(240, 185, 11, 0.4)',
background: '#E0483B',
color: '#F1ECE2',
}
: { background: 'transparent', color: '#848E9C' }
: { background: 'transparent', color: '#8A8478' }
}
>
<Percent className="w-4 h-4" />
@@ -326,7 +324,7 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
right: '15px',
fontSize: '20px',
fontWeight: 'bold',
color: 'rgba(240, 185, 11, 0.15)',
color: 'rgba(224, 72, 59, 0.15)',
zIndex: 10,
pointerEvents: 'none',
fontFamily: 'monospace',
@@ -341,25 +339,25 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
>
<defs>
<linearGradient id="colorGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#F0B90B" stopOpacity={0.8} />
<stop offset="95%" stopColor="#FCD535" stopOpacity={0.2} />
<stop offset="5%" stopColor="#E0483B" stopOpacity={0.8} />
<stop offset="95%" stopColor="#E0483B" stopOpacity={0.2} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#2B3139" />
<CartesianGrid strokeDasharray="3 3" stroke="rgba(26, 24, 19, 0.10)" />
<XAxis
dataKey="time"
stroke="#5E6673"
tick={{ fill: '#848E9C', fontSize: 11 }}
tickLine={{ stroke: '#2B3139' }}
stroke="#6B6557"
tick={{ fill: '#6B6557', fontSize: 11 }}
tickLine={{ stroke: 'rgba(26, 24, 19, 0.14)' }}
interval={Math.floor(chartData.length / 10)}
angle={-15}
textAnchor="end"
height={60}
/>
<YAxis
stroke="#5E6673"
tick={{ fill: '#848E9C', fontSize: 12 }}
tickLine={{ stroke: '#2B3139' }}
stroke="#6B6557"
tick={{ fill: '#6B6557', fontSize: 12 }}
tickLine={{ stroke: 'rgba(26, 24, 19, 0.14)' }}
domain={calculateYDomain()}
tickFormatter={(value) =>
displayMode === 'dollar' ? `$${value.toFixed(0)}` : `${value}%`
@@ -368,14 +366,14 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
<Tooltip content={<CustomTooltip />} />
<ReferenceLine
y={displayMode === 'dollar' ? initialBalance : 0}
stroke="#474D57"
stroke="rgba(26, 24, 19, 0.2)"
strokeDasharray="3 3"
label={{
value:
displayMode === 'dollar'
? t('initialBalance', language).split(' ')[0]
: '0%',
fill: '#848E9C',
fill: '#8A8478',
fontSize: 12,
}}
/>
@@ -384,11 +382,11 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
dataKey="value"
stroke="url(#colorGradient)"
strokeWidth={3}
dot={chartData.length > 50 ? false : { fill: '#F0B90B', r: 3 }}
dot={chartData.length > 50 ? false : { fill: '#E0483B', r: 3 }}
activeDot={{
r: 6,
fill: '#FCD535',
stroke: '#F0B90B',
fill: '#E0483B',
stroke: '#F1ECE2',
strokeWidth: 2,
}}
connectNulls={true}
@@ -400,72 +398,72 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
{/* Footer Stats */}
<div
className="mt-3 grid grid-cols-2 sm:grid-cols-4 gap-2 sm:gap-3 pt-3"
style={{ borderTop: '1px solid #2B3139' }}
style={{ borderTop: '1px solid rgba(26, 24, 19, 0.14)' }}
>
<div
className="p-2 rounded transition-all hover:bg-opacity-50"
style={{ background: 'rgba(240, 185, 11, 0.05)' }}
style={{ background: 'rgba(224, 72, 59, 0.05)' }}
>
<div
className="text-xs mb-1 uppercase tracking-wider"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
{t('initialBalance', language)}
</div>
<div
className="text-xs sm:text-sm font-bold mono"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
{initialBalance.toFixed(2)} USDT
</div>
</div>
<div
className="p-2 rounded transition-all hover:bg-opacity-50"
style={{ background: 'rgba(240, 185, 11, 0.05)' }}
style={{ background: 'rgba(224, 72, 59, 0.05)' }}
>
<div
className="text-xs mb-1 uppercase tracking-wider"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
{t('currentEquity', language)}
</div>
<div
className="text-xs sm:text-sm font-bold mono"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
{currentValue.raw_equity.toFixed(2)} USDT
</div>
</div>
<div
className="p-2 rounded transition-all hover:bg-opacity-50"
style={{ background: 'rgba(240, 185, 11, 0.05)' }}
style={{ background: 'rgba(224, 72, 59, 0.05)' }}
>
<div
className="text-xs mb-1 uppercase tracking-wider"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
{t('historicalCycles', language)}
</div>
<div
className="text-xs sm:text-sm font-bold mono"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
{validHistory.length} {t('cycles', language)}
</div>
</div>
<div
className="p-2 rounded transition-all hover:bg-opacity-50"
style={{ background: 'rgba(240, 185, 11, 0.05)' }}
style={{ background: 'rgba(224, 72, 59, 0.05)' }}
>
<div
className="text-xs mb-1 uppercase tracking-wider"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
{t('displayRange', language)}
</div>
<div
className="text-xs sm:text-sm font-bold mono"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
{validHistory.length > MAX_DISPLAY_POINTS
? `${t('recent', language)} ${MAX_DISPLAY_POINTS}`

View File

@@ -3,7 +3,7 @@ import { useLanguage } from '../../contexts/LanguageContext'
import { t } from '../../i18n/translations'
import { ChevronDown, TrendingUp, X } from 'lucide-react'
// 支持的交易所列表 (合约格式)
// Supported exchanges list (futures format)
const EXCHANGES = [
{ id: 'BINANCE', name: 'Binance', prefix: 'BINANCE:', suffix: '.P' },
{ id: 'BYBIT', name: 'Bybit', prefix: 'BYBIT:', suffix: '.P' },
@@ -13,7 +13,7 @@ const EXCHANGES = [
{ id: 'GATEIO', name: 'Gate.io', prefix: 'GATEIO:', suffix: '.P' },
] as const
// 热门交易对
// Popular trading pairs
const POPULAR_SYMBOLS = [
'BTCUSDT',
'ETHUSDT',
@@ -29,7 +29,7 @@ const POPULAR_SYMBOLS = [
'LTCUSDT',
]
// 时间周期选项
// Time interval options
const INTERVALS = [
{ id: '1', label: '1m' },
{ id: '5', label: '5m' },
@@ -46,7 +46,7 @@ interface TradingViewChartProps {
defaultExchange?: string
height?: number
showToolbar?: boolean
embedded?: boolean // 嵌入模式(不显示外层卡片)
embedded?: boolean // Embedded mode (does not show the outer card)
}
function TradingViewChartComponent({
@@ -66,26 +66,26 @@ function TradingViewChartComponent({
const [showSymbolDropdown, setShowSymbolDropdown] = useState(false)
const [isFullscreen, setIsFullscreen] = useState(false)
// 当外部传入的 defaultSymbol 变化时,更新内部 symbol
// Update the internal symbol when the external defaultSymbol changes
useEffect(() => {
if (defaultSymbol && defaultSymbol !== symbol) {
// console.log('[TradingViewChart] 更新币种:', defaultSymbol)
// console.log('[TradingViewChart] Updating symbol:', defaultSymbol)
setSymbol(defaultSymbol)
}
}, [defaultSymbol])
// 当外部传入的 defaultExchange 变化时,更新内部 exchange
// Update the internal exchange when the external defaultExchange changes
useEffect(() => {
if (defaultExchange && defaultExchange !== exchange) {
const normalizedExchange = defaultExchange.toUpperCase()
// console.log('[TradingViewChart] 更新交易所:', normalizedExchange)
// console.log('[TradingViewChart] Updating exchange:', normalizedExchange)
if (EXCHANGES.some(e => e.id === normalizedExchange)) {
setExchange(normalizedExchange)
}
}
}, [defaultExchange])
// 获取完整的交易对符号 (合约格式: BINANCE:BTCUSDT.P)
// Get the full trading pair symbol (futures format: BINANCE:BTCUSDT.P)
const getFullSymbol = () => {
const exchangeInfo = EXCHANGES.find((e) => e.id === exchange)
const prefix = exchangeInfo?.prefix || 'BINANCE:'
@@ -93,14 +93,14 @@ function TradingViewChartComponent({
return `${prefix}${symbol}${suffix}`
}
// 加载 TradingView Widget
// Load the TradingView Widget
useEffect(() => {
if (!containerRef.current) return
// 清空容器
// Clear the container
containerRef.current.innerHTML = ''
// 创建 widget 容器
// Create the widget container
const widgetContainer = document.createElement('div')
widgetContainer.className = 'tradingview-widget-container'
widgetContainer.style.height = '100%'
@@ -114,7 +114,7 @@ function TradingViewChartComponent({
widgetContainer.appendChild(widgetDiv)
containerRef.current.appendChild(widgetContainer)
// 加载 TradingView 脚本
// Load the TradingView script
const script = document.createElement('script')
script.src =
'https://s3.tradingview.com/external-embedding/embed-widget-advanced-chart.js'
@@ -126,12 +126,12 @@ function TradingViewChartComponent({
symbol: getFullSymbol(),
interval: timeInterval,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'Asia/Shanghai',
theme: 'dark',
theme: 'light',
style: '1',
locale: language === 'zh' ? 'zh_CN' : 'en',
enable_publishing: false,
backgroundColor: 'rgba(11, 14, 17, 1)',
gridColor: 'rgba(43, 49, 57, 0.5)',
backgroundColor: 'rgba(241, 236, 226, 1)',
gridColor: 'rgba(26, 24, 19, 0.08)',
hide_top_toolbar: !showToolbar,
hide_legend: false,
save_image: false,
@@ -149,11 +149,11 @@ function TradingViewChartComponent({
}
}, [exchange, symbol, timeInterval, language, showToolbar])
// 处理自定义交易对输入
// Handle custom trading pair input
const handleCustomSymbolSubmit = () => {
if (customSymbol.trim()) {
let sym = customSymbol.trim().toUpperCase()
// 如果没有 USDT 后缀,自动加上
// If there is no USDT suffix, add it automatically
if (!sym.endsWith('USDT')) {
sym = sym + 'USDT'
}
@@ -169,19 +169,19 @@ function TradingViewChartComponent({
? 'fixed inset-0 z-50 rounded-none flex flex-col'
: ''
}`}
style={isFullscreen ? { background: '#0B0E11' } : undefined}
style={isFullscreen ? { background: '#F1ECE2' } : undefined}
>
{/* Header */}
<div
className="flex flex-wrap items-center gap-2 p-3 sm:p-4"
style={{ borderBottom: embedded ? 'none' : '1px solid #2B3139' }}
style={{ borderBottom: embedded ? 'none' : '1px solid rgba(26, 24, 19, 0.14)' }}
>
{!embedded && (
<div className="flex items-center gap-2">
<TrendingUp className="w-5 h-5" style={{ color: '#F0B90B' }} />
<TrendingUp className="w-5 h-5" style={{ color: '#E0483B' }} />
<h3
className="text-base sm:text-lg font-bold"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
{t('marketChart', language)}
</h3>
@@ -199,21 +199,21 @@ function TradingViewChartComponent({
}}
className="flex items-center gap-1 px-3 py-1.5 rounded text-sm font-medium transition-all"
style={{
background: '#1E2329',
border: '1px solid #2B3139',
color: '#EAECEF',
background: '#F7F4EC',
border: '1px solid rgba(26, 24, 19, 0.14)',
color: '#1A1813',
}}
>
{EXCHANGES.find((e) => e.id === exchange)?.name || exchange}
<ChevronDown className="w-4 h-4" style={{ color: '#848E9C' }} />
<ChevronDown className="w-4 h-4" style={{ color: '#8A8478' }} />
</button>
{showExchangeDropdown && (
<div
className="absolute top-full left-0 mt-1 py-1 rounded-lg shadow-xl z-20 min-w-[120px]"
style={{
background: '#1E2329',
border: '1px solid #2B3139',
background: '#F7F4EC',
border: '1px solid rgba(26, 24, 19, 0.14)',
}}
>
{EXCHANGES.map((ex) => (
@@ -225,10 +225,10 @@ function TradingViewChartComponent({
}}
className="w-full px-4 py-2 text-left text-sm transition-all hover:bg-opacity-50"
style={{
color: exchange === ex.id ? '#F0B90B' : '#EAECEF',
color: exchange === ex.id ? '#E0483B' : '#1A1813',
background:
exchange === ex.id
? 'rgba(240, 185, 11, 0.1)'
? 'rgba(224, 72, 59, 0.1)'
: 'transparent',
}}
>
@@ -248,9 +248,9 @@ function TradingViewChartComponent({
}}
className="flex items-center gap-1 px-3 py-1.5 rounded text-sm font-bold transition-all"
style={{
background: 'rgba(240, 185, 11, 0.1)',
border: '1px solid rgba(240, 185, 11, 0.3)',
color: '#F0B90B',
background: 'rgba(224, 72, 59, 0.1)',
border: '1px solid rgba(224, 72, 59, 0.3)',
color: '#E0483B',
}}
>
{symbol}
@@ -261,12 +261,12 @@ function TradingViewChartComponent({
<div
className="absolute top-full left-0 mt-1 py-2 rounded-lg shadow-xl z-20 w-[280px]"
style={{
background: '#1E2329',
border: '1px solid #2B3139',
background: '#F7F4EC',
border: '1px solid rgba(26, 24, 19, 0.14)',
}}
>
{/* Custom Input */}
<div className="px-3 pb-2" style={{ borderBottom: '1px solid #2B3139' }}>
<div className="px-3 pb-2" style={{ borderBottom: '1px solid rgba(26, 24, 19, 0.14)' }}>
<div className="flex gap-2">
<input
type="text"
@@ -276,17 +276,17 @@ function TradingViewChartComponent({
placeholder={t('enterSymbol', language)}
className="flex-1 px-3 py-1.5 rounded text-sm"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
background: '#F1ECE2',
border: '1px solid rgba(26, 24, 19, 0.14)',
color: '#1A1813',
}}
/>
<button
onClick={handleCustomSymbolSubmit}
className="px-3 py-1.5 rounded text-sm font-medium"
style={{
background: '#F0B90B',
color: '#0B0E11',
background: '#E0483B',
color: '#F1ECE2',
}}
>
OK
@@ -298,7 +298,7 @@ function TradingViewChartComponent({
<div className="px-2 pt-2">
<div
className="text-xs px-2 py-1 mb-1"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
{t('popularSymbols', language)}
</div>
@@ -312,11 +312,11 @@ function TradingViewChartComponent({
}}
className="px-2 py-1.5 rounded text-xs font-medium transition-all"
style={{
color: symbol === sym ? '#F0B90B' : '#EAECEF',
color: symbol === sym ? '#E0483B' : '#1A1813',
background:
symbol === sym
? 'rgba(240, 185, 11, 0.1)'
: 'rgba(43, 49, 57, 0.3)',
? 'rgba(224, 72, 59, 0.1)'
: 'rgba(26, 24, 19, 0.04)',
}}
>
{sym.replace('USDT', '')}
@@ -331,7 +331,7 @@ function TradingViewChartComponent({
{/* Interval Selector */}
<div
className="flex gap-0.5 p-0.5 rounded"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
style={{ background: '#E8E2D5', border: '1px solid rgba(26, 24, 19, 0.14)' }}
>
{INTERVALS.map((int) => (
<button
@@ -339,8 +339,8 @@ function TradingViewChartComponent({
onClick={() => setTimeInterval(int.id)}
className="px-2 py-1 rounded text-xs font-medium transition-all"
style={{
background: timeInterval === int.id ? '#F0B90B' : 'transparent',
color: timeInterval === int.id ? '#0B0E11' : '#848E9C',
background: timeInterval === int.id ? '#E0483B' : 'transparent',
color: timeInterval === int.id ? '#F1ECE2' : '#8A8478',
}}
>
{int.label}
@@ -353,9 +353,9 @@ function TradingViewChartComponent({
onClick={() => setIsFullscreen(!isFullscreen)}
className="p-1.5 rounded transition-all"
style={{
background: isFullscreen ? '#F0B90B' : 'transparent',
color: isFullscreen ? '#0B0E11' : '#848E9C',
border: '1px solid #2B3139',
background: isFullscreen ? '#E0483B' : 'transparent',
color: isFullscreen ? '#F1ECE2' : '#8A8478',
border: '1px solid rgba(26, 24, 19, 0.14)',
}}
title={isFullscreen ? t('exitFullscreen', language) : t('fullscreen', language)}
>
@@ -375,7 +375,7 @@ function TradingViewChartComponent({
ref={containerRef}
style={{
height: isFullscreen ? 'calc(100vh - 65px)' : height,
background: '#0B0E11',
background: '#F1ECE2',
overflow: 'hidden',
}}
/>
@@ -394,5 +394,5 @@ function TradingViewChartComponent({
)
}
// 使用 memo 避免不必要的重渲染
// Use memo to avoid unnecessary re-renders
export const TradingViewChart = memo(TradingViewChartComponent)

View File

@@ -5,16 +5,16 @@ interface ContainerProps {
className?: string
as?: 'div' | 'main' | 'header' | 'section'
style?: CSSProperties
/** 是否充满宽度(取消 max-width */
/** Whether to fill the full width (removes max-width) */
fluid?: boolean
/** 是否取消水平内边距 */
/** Whether to remove horizontal padding */
noPadding?: boolean
/** 自定义最大宽度类(默认 max-w-[1920px] */
/** Custom max-width class (default max-w-[1920px]) */
maxWidthClass?: string
}
/**
* 统一的容器组件,确保所有页面元素使用一致的最大宽度和内边距
* Unified container component that ensures all page elements use a consistent max width and padding
* - max-width: 1920px
* - padding: 24px (mobile) -> 32px (tablet) -> 48px (desktop)
*/

View File

@@ -9,32 +9,17 @@ interface DeepVoidBackgroundProps extends React.HTMLAttributes<HTMLDivElement> {
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: use a much lighter static stack when animations are disabled */}
{/* Background layers: neutralized to a plain cream surface for the light theme */}
{disableAnimation ? (
<>
<div className="absolute inset-0 pointer-events-none z-0 bg-[radial-gradient(circle_at_top,rgba(240,185,11,0.08),transparent_38%),linear-gradient(180deg,rgba(12,14,20,0.98),rgba(8,10,15,1))]"></div>
<div className="absolute inset-0 pointer-events-none z-0 opacity-[0.035] bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:36px_36px]"></div>
<div className="absolute inset-0 pointer-events-none z-0 bg-nofx-bg"></div>
<div className="absolute inset-0 pointer-events-none z-0 opacity-[0.035] bg-[linear-gradient(to_right,rgba(26,24,19,0.08)_1px,transparent_1px),linear-gradient(to_bottom,rgba(26,24,19,0.08)_1px,transparent_1px)] bg-[size:36px_36px]"></div>
</>
) : (
<>
{/* 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 */}
{/* Faint grid system on cream */}
<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 className="absolute inset-x-0 bottom-0 h-[50vh] bg-[linear-gradient(to_right,rgba(26,24,19,0.07)_1px,transparent_1px),linear-gradient(to_bottom,rgba(26,24,19,0.07)_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>
</>
)}

View File

@@ -6,7 +6,7 @@ interface IconProps {
className?: string
}
// 本地图标路径映射
// Local icon path mapping
const ICON_PATHS: Record<string, string> = {
binance: '/exchange-icons/binance.jpg',
bybit: '/exchange-icons/bybit.png',
@@ -20,7 +20,7 @@ const ICON_PATHS: Record<string, string> = {
indodax: '/exchange-icons/indodax.png',
}
// 通用图标组件
// Generic icon component
const ExchangeImage: React.FC<IconProps & { src: string; alt: string }> = ({
width = 24,
height = 24,
@@ -36,7 +36,7 @@ const ExchangeImage: React.FC<IconProps & { src: string; alt: string }> = ({
borderRadius: 6,
overflow: 'hidden',
flexShrink: 0,
background: '#2B3139',
background: '#E8E2D5',
}}
>
<img
@@ -51,7 +51,7 @@ const ExchangeImage: React.FC<IconProps & { src: string; alt: string }> = ({
</div>
)
// Fallback 图标
// Fallback icon
const FallbackIcon: React.FC<IconProps & { label: string }> = ({
width = 24,
height = 24,
@@ -64,13 +64,13 @@ const FallbackIcon: React.FC<IconProps & { label: string }> = ({
width,
height,
borderRadius: 6,
background: '#2B3139',
background: '#E8E2D5',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: Math.max(10, (width || 24) * 0.4),
fontWeight: 'bold',
color: '#EAECEF',
color: '#1A1813',
flexShrink: 0,
}}
>
@@ -78,7 +78,7 @@ const FallbackIcon: React.FC<IconProps & { label: string }> = ({
</div>
)
// 获取交易所图标的函数
// Returns the icon for an exchange
export const getExchangeIcon = (
exchangeType: string,
props: IconProps = {}

View File

@@ -19,11 +19,11 @@ export function Header({ simple = false }: HeaderProps) {
<img src="/icons/nofx.svg" alt="NoFx Logo" className="w-8 h-8" />
</div>
<div>
<h1 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
<h1 className="text-xl font-bold" style={{ color: '#1A1813' }}>
{t('appTitle', language)}
</h1>
{!simple && (
<p className="text-xs mono" style={{ color: '#848E9C' }}>
<p className="text-xs mono" style={{ color: '#8A8478' }}>
{t('subtitle', language)}
</p>
)}
@@ -33,26 +33,26 @@ export function Header({ simple = false }: HeaderProps) {
{/* Right - Language Toggle (always show) */}
<div
className="flex gap-1 rounded p-1"
style={{ background: '#1E2329' }}
style={{ background: '#E8E2D5' }}
>
<button
onClick={() => setLanguage('zh')}
className="px-3 py-1.5 rounded text-xs font-semibold transition-all"
style={
language === 'zh'
? { background: '#F0B90B', color: '#000' }
: { background: 'transparent', color: '#848E9C' }
? { background: '#E0483B', color: '#F1ECE2' }
: { background: 'transparent', color: '#8A8478' }
}
>
Chinese
</button>
<button
onClick={() => setLanguage('en')}
className="px-3 py-1.5 rounded text-xs font-semibold transition-all"
style={
language === 'en'
? { background: '#F0B90B', color: '#000' }
: { background: 'transparent', color: '#848E9C' }
? { background: '#E0483B', color: '#F1ECE2' }
: { background: 'transparent', color: '#8A8478' }
}
>
EN
@@ -62,8 +62,8 @@ export function Header({ simple = false }: HeaderProps) {
className="px-3 py-1.5 rounded text-xs font-semibold transition-all"
style={
language === 'id'
? { background: '#F0B90B', color: '#000' }
: { background: 'transparent', color: '#848E9C' }
? { background: '#E0483B', color: '#F1ECE2' }
: { background: 'transparent', color: '#8A8478' }
}
>
ID

View File

@@ -93,8 +93,10 @@ 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 text-nofx-gold">NOFX</span>
<span className="flex items-center justify-center w-8 h-8 rounded-md overflow-hidden shrink-0" style={{ background: '#fff', border: '1px solid rgba(26,24,19,0.12)' }}>
<img src="/icons/nofx.svg" alt="NOFX Logo" className="w-8 h-8" />
</span>
<span className="text-lg font-bold text-nofx-gold tracking-wide">NOFX</span>
</div>
{/* Desktop Menu */}
@@ -117,7 +119,7 @@ export default function HeaderBar({
path: ROUTES.data,
label:
language === 'zh'
? '数据'
? 'Data'
: language === 'id'
? 'Data'
: 'Data',
@@ -128,7 +130,7 @@ export default function HeaderBar({
path: ROUTES.strategyMarket,
label:
language === 'zh'
? '策略市场'
? 'Market'
: language === 'id'
? 'Pasar'
: 'Market',
@@ -201,6 +203,8 @@ export default function HeaderBar({
</button>
))
})()}
{/* Dashboard context slot — terminal selector + status portals in here */}
<div id="dash-header-slot" className="hidden lg:flex items-center" />
</div>
{/* Right Side - Social Links and User Actions */}
@@ -216,7 +220,7 @@ export default function HeaderBar({
href={OFFICIAL_LINKS.github}
target="_blank"
rel="noopener noreferrer"
className="p-2 rounded-lg transition-all hover:scale-110 text-nofx-text-muted hover:text-white hover:bg-white/5"
className="p-2 rounded-lg transition-all hover:scale-110 text-nofx-text-muted hover:text-nofx-text hover:bg-[rgba(26,24,19,0.06)]"
title="GitHub"
>
<svg
@@ -265,7 +269,7 @@ export default function HeaderBar({
</div>
{/* Divider */}
<div className="h-5 w-px" style={{ background: '#2B3139' }} />
<div className="h-5 w-px" style={{ background: 'rgba(26,24,19,0.15)' }} />
{/* User Info and Actions */}
{isLoggedIn && user ? (
@@ -274,9 +278,9 @@ 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 bg-nofx-bg-lighter border border-nofx-gold/20 hover:bg-white/5"
className="flex items-center gap-2 px-3 py-2 rounded transition-colors bg-nofx-bg-lighter border border-nofx-gold/20 hover:bg-[rgba(26,24,19,0.06)]"
>
<div className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold bg-nofx-gold text-black">
<div className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold bg-nofx-gold text-white">
{user.email[0].toUpperCase()}
</div>
<span className="text-sm text-nofx-text-muted">
@@ -300,7 +304,7 @@ export default function HeaderBar({
navigateInApp(ROUTES.settings)
setUserDropdownOpen(false)
}}
className="w-full flex items-center gap-2 px-3 py-2 text-sm transition-colors hover:bg-white/5 text-nofx-text-muted hover:text-white"
className="w-full flex items-center gap-2 px-3 py-2 text-sm transition-colors hover:bg-[rgba(26,24,19,0.06)] text-nofx-text-muted hover:text-nofx-text"
>
<Settings className="w-3.5 h-3.5" />
Settings
@@ -311,15 +315,15 @@ export default function HeaderBar({
userMode === 'beginner' ? 'advanced' : 'beginner'
)
}
className="w-full flex items-center gap-2 px-3 py-2 text-sm transition-colors hover:bg-white/5 text-nofx-text-muted hover:text-white"
className="w-full flex items-center gap-2 px-3 py-2 text-sm transition-colors hover:bg-[rgba(26,24,19,0.06)] text-nofx-text-muted hover:text-nofx-text"
>
<Settings className="w-3.5 h-3.5" />
{userMode === 'beginner'
? language === 'zh'
? '切到老手模式'
? 'Switch to Advanced'
: 'Switch to Advanced'
: language === 'zh'
? '切到新手模式'
? 'Switch to Beginner'
: 'Switch to Beginner'}
</button>
{onLogout && (
@@ -345,7 +349,7 @@ export default function HeaderBar({
<button
type="button"
onClick={() => navigateInApp(ROUTES.login)}
className="px-3 py-2 text-sm font-medium transition-colors rounded text-nofx-text-muted hover:text-white"
className="px-3 py-2 text-sm font-medium transition-colors rounded text-nofx-text-muted hover:text-nofx-text"
>
{t('signIn', language)}
</button>
@@ -372,19 +376,19 @@ export default function HeaderBar({
onLanguageChange?.('zh')
setLanguageDropdownOpen(false)
}}
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'}`}
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors text-nofx-text-muted hover:text-nofx-text
${language === 'zh' ? 'bg-nofx-gold/10' : 'hover:bg-[rgba(26,24,19,0.06)]'}`}
>
<span className="text-base">🇨🇳</span>
<span className="text-sm"></span>
<span className="text-sm">Chinese</span>
</button>
<button
onClick={() => {
onLanguageChange?.('en')
setLanguageDropdownOpen(false)
}}
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'}`}
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors text-nofx-text-muted hover:text-nofx-text
${language === 'en' ? 'bg-nofx-gold/10' : 'hover:bg-[rgba(26,24,19,0.06)]'}`}
>
<span className="text-base">🇺🇸</span>
<span className="text-sm">English</span>
@@ -394,8 +398,8 @@ export default function HeaderBar({
onLanguageChange?.('id')
setLanguageDropdownOpen(false)
}}
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors text-nofx-text-muted hover:text-white
${language === 'id' ? 'bg-nofx-gold/10' : 'hover:bg-white/5'}`}
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors text-nofx-text-muted hover:text-nofx-text
${language === 'id' ? 'bg-nofx-gold/10' : 'hover:bg-[rgba(26,24,19,0.06)]'}`}
>
<span className="text-base">🇮🇩</span>
<span className="text-sm">Bahasa</span>
@@ -409,7 +413,7 @@ export default function HeaderBar({
{/* Mobile Menu Button */}
<motion.button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="md:hidden text-nofx-text-muted hover:text-white"
className="md:hidden text-nofx-text-muted hover:text-nofx-text"
whileTap={{ scale: 0.9 }}
>
{mobileMenuOpen ? (
@@ -453,7 +457,7 @@ export default function HeaderBar({
path: ROUTES.data,
label:
language === 'zh'
? '数据'
? 'Data'
: language === 'id'
? 'Data'
: 'Data',
@@ -464,7 +468,7 @@ export default function HeaderBar({
path: ROUTES.strategyMarket,
label:
language === 'zh'
? '策略市场'
? 'Market'
: language === 'id'
? 'Pasar'
: 'Market',
@@ -657,7 +661,7 @@ export default function HeaderBar({
navigateInApp(ROUTES.login)
setMobileMenuOpen(false)
}}
className="flex items-center justify-center bg-nofx-gold text-black rounded-lg font-bold text-sm hover:bg-yellow-400 transition-colors"
className="flex items-center justify-center bg-nofx-gold text-white rounded-lg font-bold text-sm hover:opacity-90 transition-colors"
>
{t('signIn', language)}
</button>

View File

@@ -76,7 +76,7 @@ function buildAgentName(nowMs: number) {
return `${AGENT_NAME} valid_until ${nowMs + AGENT_VALIDITY_MS}`
}
const HYPERLIQUID_BUILDER_ADDRESS = '0x891dc6f05ad47a3c1a05da55e7a7517971faaf0d'
// 0.05% (5). Must match the server's defaultHyperliquidBuilderMaxFee and
// 0.05% (5 bps). Must match the server's defaultHyperliquidBuilderMaxFee and
// the BuilderInfo.Fee=50 (= 5 bps) used at order placement. The user signs
// this exact string when approving the builder during wallet connect.
const HYPERLIQUID_BUILDER_MAX_FEE = '0.05%'
@@ -131,7 +131,7 @@ function getPreferredWalletProvider(): WalletProvider | undefined {
function walletSupportLabel(language: Language) {
return language === 'zh'
? '支持 MetaMaskRabbyCoinbasePhantomBraveBackpackOKXTrust 等 EVM 钱包。'
? 'Supports MetaMask, Rabby, Coinbase, Phantom, Brave, Backpack, OKX, Trust and other EVM wallets.'
: 'Supports MetaMask, Rabby, Coinbase Wallet, Phantom, Brave, Backpack, OKX, Trust and other EVM wallets.'
}
@@ -241,53 +241,53 @@ export function HyperliquidWalletConnect({
const [hasWalletProvider, setHasWalletProvider] = useState(false)
const text = useMemo(
() => ({
title: language === 'zh' ? 'Hyperliquid 钱包' : 'Hyperliquid Wallet',
connect: language === 'zh' ? '连接 Hyperliquid' : 'Connect Hyperliquid',
connected: language === 'zh' ? '已连接' : 'Connected',
mainWallet: language === 'zh' ? 'EVM 主钱包' : 'EVM main wallet',
title: language === 'zh' ? 'Hyperliquid Wallet' : 'Hyperliquid Wallet',
connect: language === 'zh' ? 'Connect Hyperliquid' : 'Connect Hyperliquid',
connected: language === 'zh' ? 'Connected' : 'Connected',
mainWallet: language === 'zh' ? 'EVM main wallet' : 'EVM main wallet',
generateAgent:
language === 'zh'
? '生成 NOFX Agent 钱包'
? 'Generate NOFX agent wallet'
: 'Generate NOFX agent wallet',
approveAgent:
language === 'zh' ? '授权 Agent 交易' : 'Authorize agent trading',
language === 'zh' ? 'Authorize agent trading' : 'Authorize agent trading',
approveBuilder:
language === 'zh' ? '完成交易授权' : 'Finalize trading authorization',
save: language === 'zh' ? '保存到 NOFX' : 'Save to NOFX',
done: language === 'zh' ? '流程已完成' : 'Flow complete',
balance: language === 'zh' ? 'Hyperliquid 余额' : 'Hyperliquid balance',
withdrawable: language === 'zh' ? '可用' : 'Withdrawable',
equity: language === 'zh' ? '权益' : 'Equity',
marginUsed: language === 'zh' ? '已用保证金' : 'Margin used',
unrealizedPnl: language === 'zh' ? '未实现盈亏' : 'Unrealized PnL',
refresh: language === 'zh' ? '刷新' : 'Refresh',
language === 'zh' ? 'Finalize trading authorization' : 'Finalize trading authorization',
save: language === 'zh' ? 'Save to NOFX' : 'Save to NOFX',
done: language === 'zh' ? 'Flow complete' : 'Flow complete',
balance: language === 'zh' ? 'Hyperliquid balance' : 'Hyperliquid balance',
withdrawable: language === 'zh' ? 'Withdrawable' : 'Withdrawable',
equity: language === 'zh' ? 'Equity' : 'Equity',
marginUsed: language === 'zh' ? 'Margin used' : 'Margin used',
unrealizedPnl: language === 'zh' ? 'Unrealized PnL' : 'Unrealized PnL',
refresh: language === 'zh' ? 'Refresh' : 'Refresh',
noCustody:
language === 'zh'
? '资金保留在你的 Hyperliquid 账户NOFX 只保存已授权 Agent 钱包。'
? 'Funds stay in your Hyperliquid account; NOFX only stores the authorized agent wallet.'
: 'Funds stay in your Hyperliquid account; NOFX only stores the authorized agent wallet.',
agentExpiry:
language === 'zh' ? 'Agent 授权到期' : 'Agent authorization expires',
agentExpired: language === 'zh' ? '已过期' : 'Expired',
language === 'zh' ? 'Agent authorization expires' : 'Agent authorization expires',
agentExpired: language === 'zh' ? 'Expired' : 'Expired',
agentNoAuth:
language === 'zh'
? '未检测到 NOFX Agent 授权'
? 'No NOFX agent authorization found'
: 'No NOFX agent authorization found',
renewAgent:
language === 'zh'
? '续期 Agent 授权(+180 天)'
? 'Renew agent authorization (+180d)'
: 'Renew agent authorization (+180d)',
renewHint:
language === 'zh'
? 'Hyperliquid 不允许重复使用同一个 Agent续期会生成一个新的 Agent 并以 180 天有效期授权,然后自动更新 NOFX 保存的私钥(需登录)。'
? 'Hyperliquid forbids reusing an agent, so renewal creates a new agent approved for 180 days, then updates the stored key in NOFX (sign-in required).'
: 'Hyperliquid forbids reusing an agent, so renewal creates a new agent approved for 180 days, then updates the stored key in NOFX (sign-in required).',
noWalletTitle:
language === 'zh' ? '未检测到 EVM 钱包' : 'No EVM wallet detected',
language === 'zh' ? 'No EVM wallet detected' : 'No EVM wallet detected',
noWalletDetail:
language === 'zh'
? '先安装 Rabby MetaMask,创建或导入钱包,然后回到这里连接 Hyperliquid'
? 'Install Rabby or MetaMask, create or import a wallet, then return here to connect Hyperliquid.'
: 'Install Rabby or MetaMask, create or import a wallet, then return here to connect Hyperliquid.',
installRabby: language === 'zh' ? '安装 Rabby' : 'Install Rabby',
installMetaMask: language === 'zh' ? '安装 MetaMask' : 'Install MetaMask',
installRabby: language === 'zh' ? 'Install Rabby' : 'Install Rabby',
installMetaMask: language === 'zh' ? 'Install MetaMask' : 'Install MetaMask',
}),
[language]
)
@@ -500,7 +500,7 @@ export function HyperliquidWalletConnect({
if (!provider) {
setError(
language === 'zh'
? '未检测到 EVM 钱包,请安装 MetaMask / Rabby / OKX / Coinbase Wallet'
? 'No EVM wallet detected. Install MetaMask, Rabby, OKX or Coinbase Wallet.'
: 'No EVM wallet detected. Install MetaMask, Rabby, OKX or Coinbase Wallet.'
)
return
@@ -625,7 +625,7 @@ export function HyperliquidWalletConnect({
if (!isLoggedIn) {
setError(
language === 'zh'
? '续期需要先登录 NOFXHyperliquid 不允许重复使用同一个 Agent续期会生成新的 Agent 并更新已保存的私钥。'
? 'Renewal requires signing in: Hyperliquid forbids reusing the same agent, so renewal creates a new agent and updates the stored key.'
: 'Renewal requires signing in: Hyperliquid forbids reusing the same agent, so renewal creates a new agent and updates the stored key.'
)
return
@@ -680,7 +680,7 @@ export function HyperliquidWalletConnect({
}))
throw new Error(
language === 'zh'
? '新 Agent 已授权,但未找到对应的 NOFX 配置,请用“保存到 NOFX”手动保存。'
? 'New agent approved, but no matching NOFX config was found. Use "Save to NOFX" to store it.'
: 'New agent approved, but no matching NOFX config was found. Use "Save to NOFX" to store it.'
)
}
@@ -712,7 +712,7 @@ export function HyperliquidWalletConnect({
}))
toast.success(
language === 'zh'
? 'Agent 已续期(新 Agent有效期 180 天)'
? 'Agent renewed (new agent, valid 180 days)'
: 'Agent renewed (new agent, valid 180 days)'
)
await refreshAgentInfo()
@@ -721,7 +721,7 @@ export function HyperliquidWalletConnect({
err instanceof Error
? err.message
: language === 'zh'
? 'Agent 续期失败'
? 'Agent renewal failed'
: 'Agent renewal failed'
)
} finally {
@@ -773,14 +773,14 @@ export function HyperliquidWalletConnect({
: undefined,
}))
toast.success(
language === 'zh' ? '交易授权已完成' : 'Trading authorization finalized'
language === 'zh' ? 'Trading authorization finalized' : 'Trading authorization finalized'
)
} catch (err) {
setError(
err instanceof Error
? err.message
: language === 'zh'
? '交易授权失败'
? 'Trading authorization failed'
: 'Trading authorization failed'
)
} finally {
@@ -793,7 +793,7 @@ export function HyperliquidWalletConnect({
if (!isLoggedIn) {
setError(
language === 'zh'
? '请先登录 NOFX再保存 Agent 钱包用于交易。'
? 'Please sign in before saving the agent wallet for trading.'
: 'Please sign in before saving the agent wallet for trading.'
)
return
@@ -899,7 +899,7 @@ export function HyperliquidWalletConnect({
onClick={() => setOpen((value) => !value)}
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-bold transition-all border ${
complete
? 'bg-emerald-500/10 border-emerald-400/30 text-emerald-300'
? 'bg-nofx-success/10 border-nofx-success/30 text-nofx-success'
: 'bg-nofx-gold/10 border-nofx-gold/30 text-nofx-gold hover:bg-nofx-gold/20'
}`}
>
@@ -913,11 +913,11 @@ export function HyperliquidWalletConnect({
{(open || inline) && (
<div
className={`${inline ? 'relative w-full' : 'absolute right-0 top-full mt-2 w-[420px] shadow-2xl shadow-black/50'} rounded-2xl border border-nofx-gold/20 bg-[#11151B] z-[80] overflow-hidden`}
className={`${inline ? 'relative w-full' : 'absolute right-0 top-full mt-2 w-[420px] shadow-2xl shadow-black/10'} rounded-2xl border border-[rgba(26,24,19,0.14)] bg-nofx-bg-lighter z-[80] overflow-hidden`}
>
<div className="flex items-center justify-between p-4 border-b border-white/10">
<div className="flex items-center justify-between p-4 border-b border-[rgba(26,24,19,0.14)]">
<div>
<div className="font-bold text-white">{text.title}</div>
<div className="font-bold text-nofx-text">{text.title}</div>
<div className="text-xs text-nofx-text-muted mt-1">
{text.noCustody}
</div>
@@ -929,7 +929,7 @@ export function HyperliquidWalletConnect({
<button
type="button"
onClick={() => setOpen(false)}
className="p-1 rounded hover:bg-white/10 text-zinc-500"
className="p-1 rounded hover:bg-[rgba(26,24,19,0.06)] text-nofx-text-muted"
>
<X className="w-4 h-4" />
</button>
@@ -943,10 +943,10 @@ export function HyperliquidWalletConnect({
<div
className={`w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold ${
step.status === 'done'
? 'bg-emerald-400 text-black'
? 'bg-nofx-success text-white'
: step.status === 'active'
? 'bg-nofx-gold text-black'
: 'bg-zinc-800 text-zinc-500'
? 'bg-nofx-gold text-white'
: 'bg-nofx-bg-deeper text-nofx-text-muted'
}`}
>
{step.status === 'done' ? (
@@ -958,8 +958,8 @@ export function HyperliquidWalletConnect({
<span
className={
step.status === 'pending'
? 'text-zinc-500'
: 'text-zinc-200'
? 'text-nofx-text-muted'
: 'text-nofx-text'
}
>
{step.label}
@@ -969,14 +969,14 @@ export function HyperliquidWalletConnect({
</div>
{error && (
<div className="rounded-lg border border-red-500/30 bg-red-500/10 p-3 text-xs text-red-300">
<div className="rounded-lg border border-nofx-danger/30 bg-nofx-danger/10 p-3 text-xs text-nofx-danger">
{error}
</div>
)}
{!state.mainWallet && !hasWalletProvider && (
<div className="rounded-xl border border-nofx-gold/20 bg-nofx-gold/5 p-3">
<div className="text-sm font-semibold text-zinc-100">
<div className="text-sm font-semibold text-nofx-text">
{text.noWalletTitle}
</div>
<p className="mt-1 text-xs leading-5 text-nofx-text-muted">
@@ -987,7 +987,7 @@ export function HyperliquidWalletConnect({
href="https://rabby.io/"
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2 rounded-lg border border-white/10 bg-white/[0.04] px-3 py-2 text-xs font-semibold text-zinc-200 hover:border-white/20 hover:bg-white/[0.07]"
className="inline-flex items-center gap-2 rounded-lg border border-[rgba(26,24,19,0.14)] bg-nofx-bg-deeper px-3 py-2 text-xs font-semibold text-nofx-text hover:border-[rgba(26,24,19,0.24)] hover:bg-nofx-bg"
>
<Download className="h-3.5 w-3.5" />
{text.installRabby}
@@ -996,7 +996,7 @@ export function HyperliquidWalletConnect({
href="https://metamask.io/download/"
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2 rounded-lg border border-white/10 bg-white/[0.04] px-3 py-2 text-xs font-semibold text-zinc-200 hover:border-white/20 hover:bg-white/[0.07]"
className="inline-flex items-center gap-2 rounded-lg border border-[rgba(26,24,19,0.14)] bg-nofx-bg-deeper px-3 py-2 text-xs font-semibold text-nofx-text hover:border-[rgba(26,24,19,0.24)] hover:bg-nofx-bg"
>
<ExternalLink className="h-3.5 w-3.5" />
{text.installMetaMask}
@@ -1005,14 +1005,14 @@ export function HyperliquidWalletConnect({
</div>
)}
<div className="rounded-xl border border-white/10 bg-black/25 p-3 space-y-2 text-xs">
<div className="rounded-xl border border-[rgba(26,24,19,0.14)] bg-nofx-bg-deeper p-3 space-y-2 text-xs">
{state.mainWallet && (
<div className="flex items-center justify-between gap-3">
<span className="text-zinc-500">Main</span>
<span className="text-nofx-text-muted">Main</span>
<button
type="button"
onClick={() => copy(state.mainWallet!, 'Main wallet')}
className="font-mono text-zinc-200 hover:text-nofx-gold flex items-center gap-1"
className="font-mono text-nofx-text hover:text-nofx-gold flex items-center gap-1"
>
{shortAddress(state.mainWallet)}{' '}
<Copy className="w-3 h-3" />
@@ -1021,11 +1021,11 @@ export function HyperliquidWalletConnect({
)}
{state.agentAddress && (
<div className="flex items-center justify-between gap-3">
<span className="text-zinc-500">Agent</span>
<span className="text-nofx-text-muted">Agent</span>
<button
type="button"
onClick={() => copy(state.agentAddress!, 'Agent wallet')}
className="font-mono text-zinc-200 hover:text-nofx-gold flex items-center gap-1"
className="font-mono text-nofx-text hover:text-nofx-gold flex items-center gap-1"
>
{shortAddress(state.agentAddress)}{' '}
<Copy className="w-3 h-3" />
@@ -1033,16 +1033,16 @@ export function HyperliquidWalletConnect({
</div>
)}
<div className="flex items-center justify-between gap-3">
<span className="text-zinc-500">Network</span>
<span className="font-mono text-zinc-300">
<span className="text-nofx-text-muted">Network</span>
<span className="font-mono text-nofx-text">
Hyperliquid Mainnet
</span>
</div>
{state.mainWallet && (
<div className="flex items-center justify-between gap-3 border-t border-white/10 pt-2">
<span className="text-zinc-500">{text.agentExpiry}</span>
<div className="flex items-center justify-between gap-3 border-t border-[rgba(26,24,19,0.14)] pt-2">
<span className="text-nofx-text-muted">{text.agentExpiry}</span>
{agentInfoLoading && !agentInfo ? (
<span className="font-mono text-zinc-400">Loading</span>
<span className="font-mono text-nofx-text-muted">Loading</span>
) : agentInfo ? (
(() => {
const { dateStr, daysLeft } = formatAgentExpiry(
@@ -1052,10 +1052,10 @@ export function HyperliquidWalletConnect({
const expired = daysLeft < 0
const soon = daysLeft >= 0 && daysLeft <= 14
const tone = expired
? 'text-red-300'
? 'text-nofx-danger'
: soon
? 'text-amber-300'
: 'text-zinc-200'
? 'text-nofx-gold'
: 'text-nofx-text'
return (
<span className={`font-mono text-right ${tone}`}>
{dateStr}
@@ -1066,7 +1066,7 @@ export function HyperliquidWalletConnect({
)
})()
) : (
<span className="font-mono text-zinc-500">
<span className="font-mono text-nofx-text-muted">
{text.agentNoAuth}
</span>
)}
@@ -1098,14 +1098,14 @@ export function HyperliquidWalletConnect({
{state.mainWallet && (
<div className="rounded-xl border border-nofx-gold/20 bg-nofx-gold/5 p-3 space-y-3 text-xs">
<div className="flex items-center justify-between gap-3">
<span className="font-bold text-zinc-100">
<span className="font-bold text-nofx-text">
{text.balance}
</span>
<button
type="button"
onClick={() => void refreshBalance()}
disabled={balanceLoading}
className="flex items-center gap-1 text-zinc-400 hover:text-nofx-gold disabled:opacity-60"
className="flex items-center gap-1 text-nofx-text-muted hover:text-nofx-gold disabled:opacity-60"
>
<RefreshCw
className={`w-3 h-3 ${balanceLoading ? 'animate-spin' : ''}`}
@@ -1114,37 +1114,37 @@ export function HyperliquidWalletConnect({
</button>
</div>
{balanceError ? (
<div className="rounded-lg border border-red-500/30 bg-red-500/10 p-2 text-red-300">
<div className="rounded-lg border border-nofx-danger/30 bg-nofx-danger/10 p-2 text-nofx-danger">
{balanceError}
</div>
) : (
<div className="grid grid-cols-2 gap-2">
<div className="rounded-lg bg-black/25 p-2">
<div className="text-zinc-500">{text.withdrawable}</div>
<div className="mt-1 font-mono text-sm font-bold text-emerald-300">
<div className="rounded-lg bg-nofx-bg-deeper p-2">
<div className="text-nofx-text-muted">{text.withdrawable}</div>
<div className="mt-1 font-mono text-sm font-bold text-nofx-success">
{balanceLoading && !account
? 'Loading…'
: `${formatUSDC(account?.withdrawable)} USDC`}
</div>
</div>
<div className="rounded-lg bg-black/25 p-2">
<div className="text-zinc-500">{text.equity}</div>
<div className="mt-1 font-mono text-sm font-bold text-zinc-100">
<div className="rounded-lg bg-nofx-bg-deeper p-2">
<div className="text-nofx-text-muted">{text.equity}</div>
<div className="mt-1 font-mono text-sm font-bold text-nofx-text">
{balanceLoading && !account
? 'Loading…'
: `${formatUSDC(account?.accountValue)} USDC`}
</div>
</div>
<div className="rounded-lg bg-black/25 p-2">
<div className="text-zinc-500">{text.marginUsed}</div>
<div className="mt-1 font-mono text-sm font-bold text-zinc-100">
<div className="rounded-lg bg-nofx-bg-deeper p-2">
<div className="text-nofx-text-muted">{text.marginUsed}</div>
<div className="mt-1 font-mono text-sm font-bold text-nofx-text">
{formatUSDC(account?.totalMarginUsed)} USDC
</div>
</div>
<div className="rounded-lg bg-black/25 p-2">
<div className="text-zinc-500">{text.unrealizedPnl}</div>
<div className="rounded-lg bg-nofx-bg-deeper p-2">
<div className="text-nofx-text-muted">{text.unrealizedPnl}</div>
<div
className={`mt-1 font-mono text-sm font-bold ${(account?.unrealizedPnl ?? 0) >= 0 ? 'text-emerald-300' : 'text-red-300'}`}
className={`mt-1 font-mono text-sm font-bold ${(account?.unrealizedPnl ?? 0) >= 0 ? 'text-nofx-success' : 'text-nofx-danger'}`}
>
{formatSignedUSDC(account?.unrealizedPnl)} USDC
</div>
@@ -1192,7 +1192,7 @@ export function HyperliquidWalletConnect({
)}
{complete && (
<>
<div className="rounded-lg border border-emerald-400/30 bg-emerald-500/10 p-3 text-sm text-emerald-200 flex items-center gap-2">
<div className="rounded-lg border border-nofx-success/30 bg-nofx-success/10 p-3 text-sm text-nofx-success flex items-center gap-2">
<Shield className="w-4 h-4" /> {text.done}
</div>
<button
@@ -1201,26 +1201,26 @@ export function HyperliquidWalletConnect({
className="w-full flex items-center justify-center gap-2 rounded-xl border border-nofx-gold/30 bg-nofx-gold/10 px-4 py-3 text-sm font-bold text-nofx-gold transition hover:bg-nofx-gold/20"
>
{language === 'zh'
? '重新授权交易'
? 'Re-authorize trading'
: 'Re-authorize trading'}
</button>
</>
)}
</div>
<div className="flex items-center justify-between pt-2 border-t border-white/10">
<div className="flex items-center justify-between pt-2 border-t border-[rgba(26,24,19,0.14)]">
<a
href="https://app.hyperliquid.xyz/"
target="_blank"
rel="noopener noreferrer"
className="text-xs text-zinc-500 hover:text-nofx-gold flex items-center gap-1"
className="text-xs text-nofx-text-muted hover:text-nofx-gold flex items-center gap-1"
>
Open Hyperliquid <ExternalLink className="w-3 h-3" />
</a>
<button
type="button"
onClick={resetFlow}
className="text-xs text-zinc-500 hover:text-red-300"
className="text-xs text-nofx-text-muted hover:text-nofx-danger"
>
Reset
</button>
@@ -1246,7 +1246,7 @@ function ActionButton({
type="button"
disabled={busy}
onClick={onClick}
className="w-full flex items-center justify-center gap-2 rounded-xl bg-nofx-gold px-4 py-3 text-sm font-bold text-black transition hover:bg-yellow-400 disabled:opacity-60 disabled:cursor-not-allowed"
className="w-full flex items-center justify-center gap-2 rounded-xl bg-nofx-gold px-4 py-3 text-sm font-bold text-white transition hover:opacity-90 disabled:opacity-60 disabled:cursor-not-allowed"
>
{busy ? <Loader2 className="w-4 h-4 animate-spin" /> : null}
{label}

View File

@@ -3,7 +3,7 @@ import { useLanguage } from '../../contexts/LanguageContext'
import type { Language } from '../../i18n/translations'
const languages: { code: Language; label: string }[] = [
{ code: 'zh', label: '中文' },
{ code: 'zh', label: 'Chinese' },
{ code: 'en', label: 'EN' },
{ code: 'id', label: 'ID' },
]
@@ -12,8 +12,8 @@ export function LanguageSwitcher() {
const { language, setLanguage } = useLanguage()
return (
<div className="absolute top-4 right-4 z-50 flex items-center gap-1 rounded-lg p-1 border border-white/10 bg-white/5 backdrop-blur-sm">
<Globe size={14} className="text-zinc-500 ml-1.5 mr-0.5" />
<div className="absolute top-4 right-4 z-50 flex items-center gap-1 rounded-lg p-1 border border-[rgba(26,24,19,0.14)] bg-nofx-bg-lighter backdrop-blur-sm">
<Globe size={14} className="text-nofx-text-muted ml-1.5 mr-0.5" />
{languages.map(({ code, label }) => (
<button
key={code}
@@ -22,7 +22,7 @@ export function LanguageSwitcher() {
className={`px-2.5 py-1 rounded text-xs font-semibold transition-all ${
language === code
? 'bg-nofx-gold/15 text-nofx-gold'
: 'text-zinc-500 hover:text-zinc-300 bg-transparent'
: 'text-nofx-text-muted hover:text-nofx-text bg-transparent'
}`}
>
{label}

View File

@@ -19,122 +19,122 @@ export const METRIC_DEFINITIONS: Record<string, MetricDefinition> = {
total_return: {
key: 'total_return',
nameEn: 'Total Return',
nameZh: '总收益率',
nameZh: 'Total Return',
formula: 'R_{total} = \\frac{V_{end} - V_{start}}{V_{start}} \\times 100\\%',
descriptionEn: 'Measures overall portfolio performance from start to end',
descriptionZh: '衡量投资组合从开始到结束的整体收益表现',
descriptionZh: 'Measures overall portfolio performance from start to end',
},
annualized_return: {
key: 'annualized_return',
nameEn: 'Annualized Return',
nameZh: '年化收益率',
nameZh: 'Annualized Return',
formula: 'R_{ann} = \\left(1 + R_{total}\\right)^{\\frac{252}{n}} - 1',
descriptionEn: 'Standardized yearly return rate (252 trading days)',
descriptionZh: '标准化年度收益率按252个交易日计算',
descriptionZh: 'Standardized yearly return rate (252 trading days)',
},
max_drawdown: {
key: 'max_drawdown',
nameEn: 'Maximum Drawdown',
nameZh: '最大回撤',
nameZh: 'Maximum Drawdown',
formula: 'MDD = \\max_{t} \\left( \\frac{Peak_t - Trough_t}{Peak_t} \\right)',
descriptionEn: 'Largest peak-to-trough decline during the period',
descriptionZh: '期间内从峰值到谷底的最大跌幅',
descriptionZh: 'Largest peak-to-trough decline during the period',
},
sharpe_ratio: {
key: 'sharpe_ratio',
nameEn: 'Sharpe Ratio',
nameZh: '夏普比率',
nameZh: 'Sharpe Ratio',
formula: 'SR = \\frac{\\bar{r} - r_f}{\\sigma}',
descriptionEn: 'Risk-adjusted return per unit of volatility (r̄=avg return, rf=risk-free rate, σ=std dev)',
descriptionZh: '单位波动风险下的超额收益r̄=平均收益rf=无风险利率,σ=标准差)',
descriptionZh: 'Risk-adjusted return per unit of volatility (r̄=avg return, rf=risk-free rate, σ=std dev)',
},
sortino_ratio: {
key: 'sortino_ratio',
nameEn: 'Sortino Ratio',
nameZh: '索提诺比率',
nameZh: 'Sortino Ratio',
formula: 'Sortino = \\frac{\\bar{r} - r_f}{\\sigma_d}',
descriptionEn: 'Return per unit of downside risk (σd=downside deviation)',
descriptionZh: '单位下行风险的收益σd=下行标准差)',
descriptionZh: 'Return per unit of downside risk (σd=downside deviation)',
},
calmar_ratio: {
key: 'calmar_ratio',
nameEn: 'Calmar Ratio',
nameZh: '卡玛比率',
nameZh: 'Calmar Ratio',
formula: 'Calmar = \\frac{R_{ann}}{|MDD|}',
descriptionEn: 'Annualized return divided by maximum drawdown',
descriptionZh: '年化收益率与最大回撤的比值',
descriptionZh: 'Annualized return divided by maximum drawdown',
},
win_rate: {
key: 'win_rate',
nameEn: 'Win Rate',
nameZh: '胜率',
nameZh: 'Win Rate',
formula: 'WinRate = \\frac{N_{win}}{N_{total}} \\times 100\\%',
descriptionEn: 'Percentage of profitable trades',
descriptionZh: '盈利交易占总交易数的百分比',
descriptionZh: 'Percentage of profitable trades',
},
profit_factor: {
key: 'profit_factor',
nameEn: 'Profit Factor',
nameZh: '盈亏比',
nameZh: 'Profit Factor',
formula: 'PF = \\frac{\\sum Profits}{|\\sum Losses|}',
descriptionEn: 'Ratio of gross profit to gross loss',
descriptionZh: '总盈利与总亏损的比值',
descriptionZh: 'Ratio of gross profit to gross loss',
},
volatility: {
key: 'volatility',
nameEn: 'Volatility',
nameZh: '波动率',
nameZh: 'Volatility',
formula: '\\sigma = \\sqrt{\\frac{1}{n}\\sum_{i=1}^{n}(r_i - \\bar{r})^2}',
descriptionEn: 'Standard deviation of returns',
descriptionZh: '收益率的标准差',
descriptionZh: 'Standard deviation of returns',
},
var_95: {
key: 'var_95',
nameEn: 'VaR (95%)',
nameZh: '风险价值',
nameZh: 'VaR (95%)',
formula: 'P(R < VaR_{95\\%}) = 5\\%',
descriptionEn: '95% confidence level maximum expected loss',
descriptionZh: '95%置信水平下的最大预期损失',
descriptionZh: '95% confidence level maximum expected loss',
},
alpha: {
key: 'alpha',
nameEn: 'Alpha',
nameZh: '超额收益',
nameZh: 'Alpha',
formula: '\\alpha = R_{portfolio} - R_{benchmark}',
descriptionEn: 'Excess return over benchmark',
descriptionZh: '相对于基准的超额收益',
descriptionZh: 'Excess return over benchmark',
},
beta: {
key: 'beta',
nameEn: 'Beta',
nameZh: '贝塔系数',
nameZh: 'Beta',
formula: '\\beta = \\frac{Cov(R_p, R_m)}{Var(R_m)}',
descriptionEn: 'Portfolio sensitivity to market movements',
descriptionZh: '投资组合对市场波动的敏感度',
descriptionZh: 'Portfolio sensitivity to market movements',
},
information_ratio: {
key: 'information_ratio',
nameEn: 'Information Ratio',
nameZh: '信息比率',
nameZh: 'Information Ratio',
formula: 'IR = \\frac{\\alpha}{\\sigma_{tracking}}',
descriptionEn: 'Alpha per unit of tracking error',
descriptionZh: '单位跟踪误差的超额收益',
descriptionZh: 'Alpha per unit of tracking error',
},
avg_trade_pnl: {
key: 'avg_trade_pnl',
nameEn: 'Avg Trade PnL',
nameZh: '平均盈亏',
nameZh: 'Avg Trade PnL',
formula: '\\bar{PnL} = \\frac{\\sum PnL_i}{N}',
descriptionEn: 'Average profit/loss per trade',
descriptionZh: '每笔交易的平均盈亏',
descriptionZh: 'Average profit/loss per trade',
},
expectancy: {
key: 'expectancy',
nameEn: 'Expectancy',
nameZh: '期望收益',
nameZh: 'Expectancy',
formula: 'E = (WinRate \\times \\bar{W}) - (LossRate \\times \\bar{L})',
descriptionEn: 'Expected return per trade',
descriptionZh: '每笔交易的期望收益',
descriptionZh: 'Expected return per trade',
},
}
@@ -259,11 +259,11 @@ export function MetricTooltip({
>
<div
style={{
background: 'linear-gradient(145deg, #1E2329 0%, #2B3139 100%)',
border: '1px solid #3B4149',
background: '#F7F4EC',
border: '1px solid rgba(26,24,19,0.14)',
borderRadius: '12px',
padding: '16px',
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.8)',
boxShadow: '0 25px 50px -12px rgba(26, 24, 19, 0.18)',
}}
>
{/* Header */}
@@ -273,27 +273,27 @@ export function MetricTooltip({
gap: '8px',
marginBottom: '12px',
paddingBottom: '8px',
borderBottom: '1px solid #3B4149'
borderBottom: '1px solid rgba(26,24,19,0.14)'
}}>
<div style={{
width: '8px',
height: '8px',
borderRadius: '50%',
background: '#F0B90B'
background: '#E0483B'
}} />
<span style={{ fontWeight: 'bold', fontSize: '14px', color: '#EAECEF' }}>
<span style={{ fontWeight: 'bold', fontSize: '14px', color: '#1A1813' }}>
{name}
</span>
</div>
{/* Formula */}
<div style={{
background: 'rgba(0,0,0,0.3)',
background: '#E8E2D5',
borderRadius: '8px',
padding: '12px',
marginBottom: '12px'
}}>
<div style={{ fontSize: '12px', color: '#848E9C', marginBottom: '8px' }}>
<div style={{ fontSize: '12px', color: '#8A8478', marginBottom: '8px' }}>
{formulaLabel}
</div>
<div style={{
@@ -301,7 +301,7 @@ export function MetricTooltip({
justifyContent: 'center',
alignItems: 'center',
padding: '8px 4px',
color: '#EAECEF',
color: '#1A1813',
overflowX: 'auto',
overflowY: 'hidden',
maxWidth: '100%',
@@ -312,7 +312,7 @@ export function MetricTooltip({
</div>
{/* Description */}
<p style={{ fontSize: '12px', lineHeight: '1.5', color: '#B7BDC6', margin: 0 }}>
<p style={{ fontSize: '12px', lineHeight: '1.5', color: '#8A8478', margin: 0 }}>
{description}
</p>
</div>
@@ -333,8 +333,8 @@ export function MetricTooltip({
}
setShow(!show)
}}
className={`p-0.5 rounded-full transition-colors hover:bg-white/10 ${className}`}
style={{ color: '#848E9C' }}
className={`p-0.5 rounded-full transition-colors hover:bg-[rgba(26,24,19,0.06)] ${className}`}
style={{ color: '#8A8478' }}
aria-label={`Info about ${name}`}
>
<HelpCircle size={size} />

View File

@@ -17,9 +17,9 @@ const MODEL_COLORS: Record<string, string> = {
claw402: '#7C3AED',
}
// 获取AI模型图标的函数
// Returns the icon for an AI model
export const getModelIcon = (modelType: string, props: IconProps = {}) => {
// 支持完整ID或类型名
// Supports full ID or type name
const type = modelType.includes('_') ? modelType.split('_').pop() : modelType
let iconPath: string | null = null
@@ -67,8 +67,8 @@ export const getModelIcon = (modelType: string, props: IconProps = {}) => {
)
}
// 获取模型颜色用于没有图标时的fallback
// Returns the model color (fallback for when there is no icon)
export const getModelColor = (modelType: string): string => {
const type = modelType.includes('_') ? modelType.split('_').pop() : modelType
return MODEL_COLORS[type || ''] || '#60a5fa'
return MODEL_COLORS[type || ''] || '#E0483B'
}

View File

@@ -9,11 +9,11 @@ export function SiteFooter({ language }: SiteFooterProps) {
return (
<footer
className="mt-16"
style={{ borderTop: '1px solid #2B3139', background: '#181A20' }}
style={{ borderTop: '1px solid rgba(26,24,19,0.14)', background: '#F7F4EC' }}
>
<div
className="max-w-[1920px] mx-auto px-6 py-6 text-center text-sm"
style={{ color: '#5E6673' }}
style={{ color: '#8A8478' }}
>
<p>{t('footerTitle', language)}</p>
<p className="mt-1">{t('footerWarning', language)}</p>
@@ -24,19 +24,19 @@ export function SiteFooter({ language }: SiteFooterProps) {
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
style={{
background: '#1E2329',
color: '#848E9C',
border: '1px solid #2B3139',
background: '#F1ECE2',
color: '#8A8478',
border: '1px solid rgba(26,24,19,0.14)',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#2B3139'
e.currentTarget.style.color = '#EAECEF'
e.currentTarget.style.borderColor = '#F0B90B'
e.currentTarget.style.background = '#E8E2D5'
e.currentTarget.style.color = '#1A1813'
e.currentTarget.style.borderColor = '#E0483B'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#1E2329'
e.currentTarget.style.color = '#848E9C'
e.currentTarget.style.borderColor = '#2B3139'
e.currentTarget.style.background = '#F1ECE2'
e.currentTarget.style.color = '#8A8478'
e.currentTarget.style.borderColor = 'rgba(26,24,19,0.14)'
}}
>
<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor">
@@ -50,19 +50,19 @@ export function SiteFooter({ language }: SiteFooterProps) {
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
style={{
background: '#1E2329',
color: '#848E9C',
border: '1px solid #2B3139',
background: '#F1ECE2',
color: '#8A8478',
border: '1px solid rgba(26,24,19,0.14)',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#2B3139'
e.currentTarget.style.color = '#EAECEF'
e.currentTarget.style.background = '#E8E2D5'
e.currentTarget.style.color = '#1A1813'
e.currentTarget.style.borderColor = '#1DA1F2'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#1E2329'
e.currentTarget.style.color = '#848E9C'
e.currentTarget.style.borderColor = '#2B3139'
e.currentTarget.style.background = '#F1ECE2'
e.currentTarget.style.color = '#8A8478'
e.currentTarget.style.borderColor = 'rgba(26,24,19,0.14)'
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
@@ -76,19 +76,19 @@ export function SiteFooter({ language }: SiteFooterProps) {
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
style={{
background: '#1E2329',
color: '#848E9C',
border: '1px solid #2B3139',
background: '#F1ECE2',
color: '#8A8478',
border: '1px solid rgba(26,24,19,0.14)',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#2B3139'
e.currentTarget.style.color = '#EAECEF'
e.currentTarget.style.background = '#E8E2D5'
e.currentTarget.style.color = '#1A1813'
e.currentTarget.style.borderColor = '#0088cc'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#1E2329'
e.currentTarget.style.color = '#848E9C'
e.currentTarget.style.borderColor = '#2B3139'
e.currentTarget.style.background = '#F1ECE2'
e.currentTarget.style.color = '#8A8478'
e.currentTarget.style.borderColor = 'rgba(26,24,19,0.14)'
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">

View File

@@ -73,15 +73,15 @@ export function WebCryptoEnvironmentCheck({
const isCompact = variant === 'compact'
const containerClass = isCompact
? 'p-3 rounded border border-gray-700 bg-gray-900 space-y-3'
: 'p-4 rounded border border-[#2B3139] bg-[#0B0E11] space-y-4'
? 'p-3 rounded border border-[rgba(26,24,19,0.14)] bg-nofx-bg-lighter space-y-3'
: 'p-4 rounded border border-[rgba(26,24,19,0.14)] bg-nofx-bg-lighter space-y-4'
const descriptionColor = isCompact ? '#CBD5F5' : '#A1AEC8'
const descriptionColor = isCompact ? '#8A8478' : '#8A8478'
const showInfo = status !== 'idle'
const statusRendererMap: Record<WebCryptoCheckStatus, () => ReactNode> = {
secure: () => (
<div className="flex items-start gap-2 text-green-400 text-xs">
<div className="flex items-start gap-2 text-[#2E8B57] text-xs">
<ShieldCheck className="w-4 h-4 flex-shrink-0" />
<div>
<div className="font-semibold">
@@ -111,7 +111,7 @@ export function WebCryptoEnvironmentCheck({
</div>
),
unsupported: () => (
<div className="text-xs" style={{ color: '#F87171' }}>
<div className="text-xs" style={{ color: '#D6433A' }}>
<div className="flex items-start gap-2 mb-1">
<ShieldAlert className="w-4 h-4 flex-shrink-0" />
<div className="font-semibold">
@@ -122,7 +122,7 @@ export function WebCryptoEnvironmentCheck({
</div>
),
disabled: () => (
<div className="flex items-start gap-2 text-gray-400 text-xs">
<div className="flex items-start gap-2 text-nofx-text-muted text-xs">
<ShieldMinus className="w-4 h-4 flex-shrink-0" />
<div>
<div className="font-semibold">
@@ -135,7 +135,7 @@ export function WebCryptoEnvironmentCheck({
checking: () => (
<div
className="flex items-center gap-2 text-xs"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
<Loader2 className="w-4 h-4 animate-spin" />
<span>{t('environmentCheck.checking', language)}</span>

View File

@@ -19,11 +19,9 @@ export function WhitelistFullPage({ onBack }: WhitelistFullPageProps) {
}
return (
<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>
<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>
<div className="min-h-screen bg-nofx-bg-deeper text-nofx-text font-mono relative overflow-hidden flex items-center justify-center px-4">
{/* Background Grid */}
<div className="fixed inset-0 bg-[linear-gradient(to_right,rgba(26,24,19,0.04)_1px,transparent_1px),linear-gradient(to_bottom,rgba(26,24,19,0.04)_1px,transparent_1px)] bg-[size:24px_24px] pointer-events-none"></div>
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
@@ -31,15 +29,15 @@ export function WhitelistFullPage({ onBack }: WhitelistFullPageProps) {
transition={{ duration: 0.5 }}
className="max-w-lg w-full relative z-10"
>
<div className="bg-zinc-900/40 backdrop-blur-md border border-red-500/30 rounded-lg overflow-hidden relative group">
<div className="bg-nofx-bg-lighter border border-[#D6433A]/30 rounded-lg overflow-hidden relative group">
{/* Top Bar */}
<div className="flex items-center justify-between px-4 py-2 bg-red-900/20 border-b border-red-500/30">
<div className="flex items-center justify-between px-4 py-2 bg-[#D6433A]/10 border-b border-[#D6433A]/30">
<div className="flex gap-1.5 opacity-50">
<div className="w-2.5 h-2.5 rounded-full bg-red-500"></div>
<div className="w-2.5 h-2.5 rounded-full bg-zinc-600"></div>
<div className="w-2.5 h-2.5 rounded-full bg-zinc-600"></div>
<div className="w-2.5 h-2.5 rounded-full bg-[#D6433A]"></div>
<div className="w-2.5 h-2.5 rounded-full bg-[rgba(26,24,19,0.25)]"></div>
<div className="w-2.5 h-2.5 rounded-full bg-[rgba(26,24,19,0.25)]"></div>
</div>
<div className="text-[10px] text-red-400 font-mono tracking-widest animate-pulse">
<div className="text-[10px] text-[#D6433A] font-mono tracking-widest animate-pulse">
ACCESS_DENIED // ERROR_403
</div>
</div>
@@ -47,22 +45,21 @@ export function WhitelistFullPage({ onBack }: WhitelistFullPageProps) {
<div className="p-8 text-center">
{/* Icon */}
<div className="relative mx-auto mb-8 w-20 h-20 flex items-center justify-center">
<div className="absolute inset-0 bg-red-500/20 rounded-full animate-ping opacity-50"></div>
<div className="relative z-10 p-4 border-2 border-red-500/50 rounded-full bg-black/50">
<ShieldAlert className="w-8 h-8 text-red-500" />
<div className="relative z-10 p-4 border-2 border-[#D6433A]/50 rounded-full bg-nofx-bg-deeper">
<ShieldAlert className="w-8 h-8 text-[#D6433A]" />
</div>
</div>
{/* Title */}
<h1 className="text-2xl font-bold mb-2 tracking-widest text-white uppercase glitch-text">
<span className="text-red-500">RESTRICTED</span> ACCESS
<h1 className="text-2xl font-bold mb-2 tracking-widest text-nofx-text uppercase">
<span className="text-[#D6433A]">RESTRICTED</span> ACCESS
</h1>
<div className="h-[1px] w-full bg-gradient-to-r from-transparent via-red-900/50 to-transparent my-4"></div>
<div className="h-[1px] w-full bg-gradient-to-r from-transparent via-[#D6433A]/40 to-transparent my-4"></div>
{/* Description */}
<p className="text-xs text-zinc-400 mb-8 leading-relaxed font-mono px-4">
<span className="text-red-400">[SYSTEM_MESSAGE]:</span> YOUR
<p className="text-xs text-nofx-text-muted mb-8 leading-relaxed font-mono px-4">
<span className="text-[#D6433A]">[SYSTEM_MESSAGE]:</span> YOUR
IDENTIFIER IS NOT ON THE ACTIVE WHITELIST.
<br />
<br />
@@ -72,14 +69,14 @@ export function WhitelistFullPage({ onBack }: WhitelistFullPageProps) {
</p>
{/* Info Box */}
<div className="bg-red-950/20 border border-red-900/30 p-4 rounded mb-8 text-left">
<div className="bg-[#D6433A]/8 border border-[#D6433A]/25 p-4 rounded mb-8 text-left">
<div className="flex items-start gap-3">
<Lock className="w-4 h-4 text-red-500 mt-0.5" />
<Lock className="w-4 h-4 text-[#D6433A] mt-0.5" />
<div>
<h3 className="text-xs font-bold text-red-400 uppercase mb-1">
<h3 className="text-xs font-bold text-[#D6433A] uppercase mb-1">
Authorization Protocol
</h3>
<p className="text-[10px] text-zinc-500 leading-tight">
<p className="text-[10px] text-nofx-text-muted leading-tight">
Access is rolled out in batches. If you believe this is an
error, please verify your credentials or contact system
administrators.
@@ -92,7 +89,7 @@ export function WhitelistFullPage({ onBack }: WhitelistFullPageProps) {
<div className="space-y-3">
<button
onClick={handleBackToLogin}
className="w-full flex items-center justify-center gap-2 py-3 border border-zinc-700 bg-black hover:bg-zinc-900 hover:border-red-500 hover:text-red-500 text-zinc-400 transition-all text-xs font-bold tracking-widest uppercase group"
className="w-full flex items-center justify-center gap-2 py-3 border border-[rgba(26,24,19,0.14)] bg-nofx-bg hover:bg-nofx-bg-deeper hover:border-[#D6433A] hover:text-[#D6433A] text-nofx-text-muted transition-all text-xs font-bold tracking-widest uppercase group"
>
<ArrowLeft className="w-3 h-3 group-hover:-translate-x-1 transition-transform" />
RETURN TO LOGIN
@@ -103,7 +100,7 @@ export function WhitelistFullPage({ onBack }: WhitelistFullPageProps) {
href={OFFICIAL_LINKS.twitter}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-2 py-2 border border-zinc-800 bg-zinc-900/50 hover:bg-zinc-800 text-zinc-500 hover:text-white transition-colors text-[10px] uppercase"
className="flex items-center justify-center gap-2 py-2 border border-[rgba(26,24,19,0.14)] bg-nofx-bg hover:bg-nofx-bg-deeper text-nofx-text-muted hover:text-nofx-text transition-colors text-[10px] uppercase"
>
<Twitter className="w-3 h-3" />
Updates
@@ -112,7 +109,7 @@ export function WhitelistFullPage({ onBack }: WhitelistFullPageProps) {
href={OFFICIAL_LINKS.telegram}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-2 py-2 border border-zinc-800 bg-zinc-900/50 hover:bg-zinc-800 text-zinc-500 hover:text-white transition-colors text-[10px] uppercase"
className="flex items-center justify-center gap-2 py-2 border border-[rgba(26,24,19,0.14)] bg-nofx-bg hover:bg-nofx-bg-deeper text-nofx-text-muted hover:text-nofx-text transition-colors text-[10px] uppercase"
>
<Send className="w-3 h-3" />
Support
@@ -122,7 +119,7 @@ export function WhitelistFullPage({ onBack }: WhitelistFullPageProps) {
</div>
{/* Footer */}
<div className="bg-black/80 p-2 text-[9px] text-zinc-700 text-center border-t border-zinc-800 font-mono uppercase">
<div className="bg-nofx-bg-deeper p-2 text-[9px] text-nofx-text-muted text-center border-t border-[rgba(26,24,19,0.14)] font-mono uppercase">
ERR_CODE: WLIST_0x403 // SECURITY_LAYER_ACTIVE
</div>
</div>

View File

@@ -1,7 +1,7 @@
import { useEffect, useRef } from 'react'
import { t, type Language } from '../../i18n/translations'
import type { FAQCategory } from '../../data/faqData'
// RoadmapWidget 移除动态嵌入,按需仅展示外部链接
// RoadmapWidget: removed dynamic embedding; only show external links on demand
interface FAQContentProps {
categories: FAQCategory[]
@@ -56,11 +56,11 @@ export function FAQContent({
return (
<div className="space-y-12">
{categories.map((category) => (
<div key={category.id} className="nofx-glass p-8 rounded-xl border border-white/5">
<div key={category.id} className="nofx-glass p-8 rounded-xl border border-[rgba(26,24,19,0.14)]">
{/* Category Header */}
<div className="flex items-center gap-3 mb-6 pb-3 border-b border-white/10">
<div className="flex items-center gap-3 mb-6 pb-3 border-b border-[rgba(26,24,19,0.14)]">
<category.icon className="w-7 h-7 text-nofx-gold" />
<h2 className="text-2xl font-bold text-nofx-text-main">
<h2 className="text-2xl font-bold text-nofx-text">
{t(category.titleKey, language)}
</h2>
</div>
@@ -76,7 +76,7 @@ export function FAQContent({
className="scroll-mt-24"
>
{/* Question */}
<h3 className="text-xl font-semibold mb-3 text-nofx-text-main">
<h3 className="text-xl font-semibold mb-3 text-nofx-text">
{t(item.questionKey, language)}
</h3>
@@ -85,41 +85,42 @@ export function FAQContent({
{item.id === 'github-projects-tasks' ? (
<div className="space-y-3">
<div className="text-base">
{language === 'zh' ? '链接:' : 'Links:'}{' '}
{language === 'zh' ? 'Links:' : 'Links:'}{' '}
<a
href="https://github.com/orgs/NoFxAiOS/projects/3"
target="_blank"
rel="noreferrer"
style={{ color: '#F0B90B' }}
style={{ color: '#E0483B' }}
>
{language === 'zh' ? '路线图' : 'Roadmap'}
{language === 'zh' ? 'Roadmap' : 'Roadmap'}
</a>
{' | '}
<a
href="https://github.com/orgs/NoFxAiOS/projects/5"
target="_blank"
rel="noreferrer"
style={{ color: '#F0B90B' }}
style={{ color: '#E0483B' }}
>
{language === 'zh' ? '任务看板' : 'Task Dashboard'}
{language === 'zh' ? 'Task Dashboard' : 'Task Dashboard'}
</a>
</div>
<ol className="list-decimal pl-5 space-y-1 text-base">
{language === 'zh' ? (
<>
<li>
good first issue / help
wanted / frontend / backend
Open the links above and filter by labels (good
first issue / help wanted / frontend / backend).
</li>
<li>
Acceptance
Criteria
Open the task and read the Description &
Acceptance Criteria.
</li>
<li>assign me</li>
<li>Fork GitHub </li>
<li>
fork <code>dev</code>{' '}
Comment "assign me" or self-assign (if permitted).
</li>
<li>Fork the repository to your GitHub account.</li>
<li>
Sync your fork's <code>dev</code> with upstream:
<code className="ml-2">
git remote add upstream
https://github.com/NoFxAiOS/nofx.git
@@ -134,28 +135,28 @@ export function FAQContent({
<code>git push origin dev</code>
</li>
<li>
fork <code>dev</code>
Create a feature branch from your fork's{' '}
<code>dev</code>:
<code className="ml-2">
git checkout -b feat/your-topic
</code>
</li>
<li>
fork
Push to your fork:
<code className="ml-2">
git push origin feat/your-topic
</code>
</li>
<li>
PRbase <code>NoFxAiOS/nofx:dev</code>{' '}
compare {' '}
<code>/nofx:feat/your-topic</code>
Open a PR: base <code>NoFxAiOS/nofx:dev</code>
compare{' '}
<code>your-username/nofx:feat/your-topic</code>.
</li>
<li>
PR Issue
<code className="ml-1">Closes #123</code>
PR {' '}
<code>upstream/dev</code>{' '}
rebase
In PR, reference the Issue (e.g.,{' '}
<code className="ml-1">Closes #123</code>) and
choose the proper PR template; rebase onto{' '}
<code>upstream/dev</code> as needed.
</li>
</>
) : (
@@ -218,38 +219,13 @@ export function FAQContent({
<div
className="rounded p-3 mt-3"
style={{
background: 'rgba(240, 185, 11, 0.08)',
border: '1px solid rgba(240, 185, 11, 0.25)',
background: 'rgba(224, 72, 59, 0.08)',
border: '1px solid rgba(224, 72, 59, 0.25)',
}}
>
{language === 'zh' ? (
<div className="text-sm">
<strong style={{ color: '#F0B90B' }}></strong>{' '}
Bounty/
Review/
<a
href="https://github.com/NoFxAiOS/nofx/labels/bounty"
target="_blank"
rel="noreferrer"
style={{ color: '#F0B90B' }}
>
bounty
</a>
<a
href="https://github.com/NoFxAiOS/nofx/blob/dev/.github/ISSUE_TEMPLATE/bounty_claim.md"
target="_blank"
rel="noreferrer"
style={{ color: '#F0B90B' }}
>
Bounty Claim
</a>
</div>
) : (
<div className="text-sm">
<strong style={{ color: '#F0B90B' }}>Note:</strong>{' '}
<strong style={{ color: '#E0483B' }}>Note:</strong>{' '}
Contribution incentives are available (e.g., cash
bounties, badges & shout-outs, priority
review/merge, beta access). Prefer tasks with
@@ -257,7 +233,7 @@ export function FAQContent({
href="https://github.com/NoFxAiOS/nofx/labels/bounty"
target="_blank"
rel="noreferrer"
style={{ color: '#F0B90B' }}
style={{ color: '#E0483B' }}
>
bounty label
</a>
@@ -266,7 +242,32 @@ export function FAQContent({
href="https://github.com/NoFxAiOS/nofx/blob/dev/.github/ISSUE_TEMPLATE/bounty_claim.md"
target="_blank"
rel="noreferrer"
style={{ color: '#F0B90B' }}
style={{ color: '#E0483B' }}
>
Bounty Claim
</a>
after completion.
</div>
) : (
<div className="text-sm">
<strong style={{ color: '#E0483B' }}>Note:</strong>{' '}
Contribution incentives are available (e.g., cash
bounties, badges & shout-outs, priority
review/merge, beta access). Prefer tasks with
<a
href="https://github.com/NoFxAiOS/nofx/labels/bounty"
target="_blank"
rel="noreferrer"
style={{ color: '#E0483B' }}
>
bounty label
</a>
, or file a
<a
href="https://github.com/NoFxAiOS/nofx/blob/dev/.github/ISSUE_TEMPLATE/bounty_claim.md"
target="_blank"
rel="noreferrer"
style={{ color: '#E0483B' }}
>
Bounty Claim
</a>
@@ -278,7 +279,7 @@ export function FAQContent({
) : item.id === 'contribute-pr-guidelines' ? (
<div className="space-y-3">
<div className="text-base">
{language === 'zh' ? '参考文档:' : 'References:'}{' '}
{language === 'zh' ? 'References:' : 'References:'}{' '}
<a
href="https://github.com/NoFxAiOS/nofx/blob/dev/CONTRIBUTING.md"
target="_blank"
@@ -301,35 +302,37 @@ export function FAQContent({
{language === 'zh' ? (
<>
<li>
Fork fork <code>dev</code>{' '}
<code>main</code>{' '}
After forking, branch from your fork's{' '}
<code>dev</code>; avoid direct commits to upstream{' '}
<code>main</code>.
</li>
<li>
feat/fix/docs/
Conventional Commits
Branch naming: feat/…, fix/…, docs/…; commit
messages follow Conventional Commits.
</li>
<li>
Run checks before PR:
<code className="ml-2">
npm --prefix web run lint && npm --prefix web
run build
</code>
</li>
<li> UI </li>
<li>
PR
frontend/backend/docs/general
For UI changes, attach screenshots or a short
video.
</li>
<li>
PR Issue
<code className="ml-1">Closes #123</code>PR
<code>NoFxAiOS/nofx:dev</code>
Choose the proper PR template
(frontend/backend/docs/general).
</li>
<li>
<code>upstream/dev</code>{' '}
rebase CI PR
Link the Issue in PR (e.g.,{' '}
<code className="ml-1">Closes #123</code>) and
target <code>NoFxAiOS/nofx:dev</code>.
</li>
<li>
Keep rebasing onto <code>upstream/dev</code>,
ensure CI passes; prefer small and focused PRs.
</li>
</>
) : (
@@ -375,30 +378,6 @@ export function FAQContent({
{language === 'zh' ? (
<div className="text-sm">
<strong className="text-nofx-gold">Note:</strong>{' '}
我们为高质量贡献提供激励Bounty/奖金、荣誉徽章与鸣谢、优先
Review/合并与内测资格 等)。 详情可关注带
<a
href="https://github.com/NoFxAiOS/nofx/labels/bounty"
target="_blank"
rel="noreferrer"
style={{ color: '#F0B90B' }}
>
bounty 标签
</a>
的任务,或使用
<a
href="https://github.com/NoFxAiOS/nofx/blob/dev/.github/ISSUE_TEMPLATE/bounty_claim.md"
target="_blank"
rel="noreferrer"
style={{ color: '#F0B90B' }}
>
Bounty Claim 模板
</a>
提交申请。
</div>
) : (
<div className="text-sm">
<strong style={{ color: '#F0B90B' }}>Note:</strong>{' '}
We offer contribution incentives (bounties, badges,
shout-outs, priority review/merge, beta access).
Look for tasks with
@@ -406,7 +385,7 @@ export function FAQContent({
href="https://github.com/NoFxAiOS/nofx/labels/bounty"
target="_blank"
rel="noreferrer"
style={{ color: '#F0B90B' }}
style={{ color: '#E0483B' }}
>
bounty label
</a>
@@ -415,7 +394,32 @@ export function FAQContent({
href="https://github.com/NoFxAiOS/nofx/blob/dev/.github/ISSUE_TEMPLATE/bounty_claim.md"
target="_blank"
rel="noreferrer"
style={{ color: '#F0B90B' }}
style={{ color: '#E0483B' }}
>
Bounty Claim template
</a>
when ready.
</div>
) : (
<div className="text-sm">
<strong style={{ color: '#E0483B' }}>Note:</strong>{' '}
We offer contribution incentives (bounties, badges,
shout-outs, priority review/merge, beta access).
Look for tasks with
<a
href="https://github.com/NoFxAiOS/nofx/labels/bounty"
target="_blank"
rel="noreferrer"
style={{ color: '#E0483B' }}
>
bounty label
</a>
, or submit a
<a
href="https://github.com/NoFxAiOS/nofx/blob/dev/.github/ISSUE_TEMPLATE/bounty_claim.md"
target="_blank"
rel="noreferrer"
style={{ color: '#E0483B' }}
>
Bounty Claim
</a>
@@ -430,7 +434,7 @@ export function FAQContent({
</div>
{/* Divider */}
<div className="mt-6 h-px bg-white/5" />
<div className="mt-6 h-px bg-[rgba(26,24,19,0.14)]" />
</section>
))}
</div>

View File

@@ -63,11 +63,11 @@ export function FAQLayout({ language }: FAQLayoutProps) {
{/* 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 className="w-16 h-16 rounded-full flex items-center justify-center bg-nofx-gold shadow-lg">
<HelpCircle className="w-8 h-8 text-nofx-bg" />
</div>
</div>
<h1 className="text-4xl font-bold mb-4 text-nofx-text-main">
<h1 className="text-4xl font-bold mb-4 text-nofx-text">
{t('faqTitle', language)}
</h1>
<p className="text-lg mb-8 text-nofx-text-muted">
@@ -80,7 +80,7 @@ export function FAQLayout({ language }: FAQLayoutProps) {
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
placeholder={
language === 'zh' ? '搜索常见问题...' : 'Search FAQ...'
language === 'zh' ? 'Search FAQ...' : 'Search FAQ...'
}
/>
</div>
@@ -108,21 +108,20 @@ export function FAQLayout({ language }: FAQLayoutProps) {
/>
) : (
<div className="text-center py-12">
<p className="text-lg" style={{ color: '#848E9C' }}>
<p className="text-lg" style={{ color: '#8A8478' }}>
{language === 'zh'
? '没有找到匹配的问题'
? 'No matching questions found'
: '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',
background: '#E0483B',
color: '#F1ECE2',
}}
>
{language === 'zh' ? '清除搜索' : 'Clear Search'}
{language === 'zh' ? 'Clear Search' : 'Clear Search'}
</button>
</div>
)}
@@ -133,15 +132,14 @@ export function FAQLayout({ language }: FAQLayoutProps) {
<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)',
background: 'rgba(224, 72, 59, 0.08)',
border: '1px solid rgba(224, 72, 59, 0.2)',
}}
>
<h3 className="text-xl font-bold mb-3" style={{ color: '#EAECEF' }}>
<h3 className="text-xl font-bold mb-3" style={{ color: '#1A1813' }}>
{t('faqStillHaveQuestions', language)}
</h3>
<p className="mb-6" style={{ color: '#848E9C' }}>
<p className="mb-6" style={{ color: '#8A8478' }}>
{t('faqContactUs', language)}
</p>
<div className="flex items-center justify-center gap-4">
@@ -151,9 +149,9 @@ export function FAQLayout({ language }: FAQLayoutProps) {
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',
background: '#F7F4EC',
color: '#1A1813',
border: '1px solid rgba(26,24,19,0.14)',
}}
>
GitHub
@@ -164,8 +162,8 @@ export function FAQLayout({ language }: FAQLayoutProps) {
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',
background: '#E0483B',
color: '#F1ECE2',
}}
>
{t('community', language)}

View File

@@ -21,12 +21,12 @@ export function FAQSearchBar({
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 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"
className="w-full pl-12 pr-12 py-3 rounded-lg text-base transition-all focus:outline-none bg-nofx-bg-lighter border border-[rgba(26,24,19,0.14)] text-nofx-text 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 text-nofx-text-muted hover:text-white transition-colors"
className="absolute right-4 top-1/2 transform -translate-y-1/2 text-nofx-text-muted hover:text-nofx-text transition-colors"
>
<X className="w-5 h-5" />
</button>

View File

@@ -19,12 +19,12 @@ export function FAQSidebar({
className="sticky top-24 h-[calc(100vh-120px)] overflow-y-auto pr-4"
style={{
scrollbarWidth: 'thin',
scrollbarColor: '#2B3139 #1E2329',
scrollbarColor: '#E8E2D5 #F7F4EC',
}}
>
<div className="space-y-6">
{categories.map((category) => (
<div key={category.id} className="nofx-glass p-4 rounded-xl border border-white/5">
<div key={category.id} className="nofx-glass p-4 rounded-xl border border-[rgba(26,24,19,0.14)]">
{/* Category Title */}
<div className="flex items-center gap-2 mb-3 px-3">
<category.icon className="w-5 h-5 text-nofx-gold" />
@@ -43,7 +43,7 @@ export function FAQSidebar({
onClick={() => onItemClick(category.id, item.id)}
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'
: 'bg-transparent text-nofx-text-muted border-transparent pl-3 hover:bg-nofx-gold/5 hover:text-nofx-text'
}`}
>
{t(item.questionKey, language)}

View File

@@ -10,27 +10,27 @@ export default function AboutSection({ language }: AboutSectionProps) {
const features = [
{
icon: Shield,
title: language === 'zh' ? '完全自主控制' : 'Full Control',
desc: language === 'zh' ? '自托管,数据安全' : 'Self-hosted, data secure',
title: language === 'zh' ? 'Full Control' : 'Full Control',
desc: language === 'zh' ? 'Self-hosted, data secure' : 'Self-hosted, data secure',
},
{
icon: Cpu,
title: language === 'zh' ? '多 AI 支持' : 'Multi-AI Support',
title: language === 'zh' ? 'Multi-AI Support' : 'Multi-AI Support',
desc: language === 'zh' ? 'DeepSeek, GPT, Claude...' : 'DeepSeek, GPT, Claude...',
},
{
icon: BarChart3,
title: language === 'zh' ? '实时监控' : 'Real-time Monitor',
desc: language === 'zh' ? '可视化交易看板' : 'Visual trading dashboard',
title: language === 'zh' ? 'Real-time Monitor' : 'Real-time Monitor',
desc: language === 'zh' ? 'Visual trading dashboard' : 'Visual trading dashboard',
},
]
return (
<section className="py-24 relative overflow-hidden" style={{ background: '#0B0E11' }}>
<section className="py-24 relative overflow-hidden" style={{ background: '#F1ECE2' }}>
{/* Background Decoration */}
<div
className="absolute top-0 right-0 w-96 h-96 rounded-full blur-3xl opacity-30"
style={{ background: 'radial-gradient(circle, rgba(240, 185, 11, 0.1) 0%, transparent 70%)' }}
style={{ background: 'radial-gradient(circle, rgba(224, 72, 59, 0.08) 0%, transparent 70%)' }}
/>
<div className="max-w-6xl mx-auto px-4">
@@ -45,21 +45,21 @@ export default function AboutSection({ language }: AboutSectionProps) {
<motion.div
className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full mb-6"
style={{
background: 'rgba(240, 185, 11, 0.1)',
border: '1px solid rgba(240, 185, 11, 0.2)',
background: 'rgba(224, 72, 59, 0.1)',
border: '1px solid rgba(224, 72, 59, 0.2)',
}}
>
<Terminal className="w-4 h-4" style={{ color: '#F0B90B' }} />
<span className="text-xs font-medium" style={{ color: '#F0B90B' }}>
<Terminal className="w-4 h-4" style={{ color: '#E0483B' }} />
<span className="text-xs font-medium" style={{ color: '#E0483B' }}>
{t('aboutNofx', language)}
</span>
</motion.div>
<h2 className="text-4xl lg:text-5xl font-bold mb-6" style={{ color: '#EAECEF' }}>
<h2 className="text-4xl lg:text-5xl font-bold mb-6" style={{ color: '#1A1813' }}>
{t('whatIsNofx', language)}
</h2>
<p className="text-lg mb-8 leading-relaxed" style={{ color: '#848E9C' }}>
<p className="text-lg mb-8 leading-relaxed" style={{ color: '#8A8478' }}>
{t('nofxNotAnotherBot', language)} {t('nofxDescription1', language)}
</p>
@@ -74,21 +74,21 @@ export default function AboutSection({ language }: AboutSectionProps) {
transition={{ delay: index * 0.1 }}
className="flex items-center gap-3 px-4 py-3 rounded-xl"
style={{
background: 'rgba(255, 255, 255, 0.03)',
border: '1px solid rgba(255, 255, 255, 0.06)',
background: '#F7F4EC',
border: '1px solid rgba(26, 24, 19, 0.14)',
}}
>
<div
className="w-10 h-10 rounded-lg flex items-center justify-center"
style={{ background: 'rgba(240, 185, 11, 0.1)' }}
style={{ background: 'rgba(224, 72, 59, 0.1)' }}
>
<feature.icon className="w-5 h-5" style={{ color: '#F0B90B' }} />
<feature.icon className="w-5 h-5" style={{ color: '#E0483B' }} />
</div>
<div>
<div className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
<div className="text-sm font-semibold" style={{ color: '#1A1813' }}>
{feature.title}
</div>
<div className="text-xs" style={{ color: '#5E6673' }}>
<div className="text-xs" style={{ color: '#8A8478' }}>
{feature.desc}
</div>
</div>
@@ -107,36 +107,36 @@ export default function AboutSection({ language }: AboutSectionProps) {
<div
className="rounded-2xl overflow-hidden"
style={{
background: '#0D1117',
border: '1px solid rgba(255, 255, 255, 0.1)',
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.5)',
background: '#F7F4EC',
border: '1px solid rgba(26, 24, 19, 0.14)',
boxShadow: '0 6px 24px rgba(26, 24, 19, 0.12)',
}}
>
{/* Terminal Header */}
<div
className="flex items-center gap-2 px-4 py-3"
style={{ background: 'rgba(255, 255, 255, 0.03)', borderBottom: '1px solid rgba(255, 255, 255, 0.06)' }}
style={{ background: '#E8E2D5', borderBottom: '1px solid rgba(26, 24, 19, 0.14)' }}
>
<div className="flex gap-2">
<div className="w-3 h-3 rounded-full" style={{ background: '#FF5F56' }} />
<div className="w-3 h-3 rounded-full" style={{ background: '#FFBD2E' }} />
<div className="w-3 h-3 rounded-full" style={{ background: '#27C93F' }} />
<div className="w-3 h-3 rounded-full" style={{ background: '#D6433A' }} />
<div className="w-3 h-3 rounded-full" style={{ background: '#E0483B' }} />
<div className="w-3 h-3 rounded-full" style={{ background: '#2E8B57' }} />
</div>
<span className="text-xs ml-2" style={{ color: '#5E6673' }}>terminal</span>
<span className="text-xs ml-2" style={{ color: '#8A8478' }}>terminal</span>
</div>
{/* Terminal Content */}
<div className="p-6 font-mono text-sm space-y-2">
<div style={{ color: '#5E6673' }}>$ git clone https://github.com/NoFxAiOS/nofx.git</div>
<div style={{ color: '#5E6673' }}>$ cd nofx && chmod +x start.sh</div>
<div style={{ color: '#5E6673' }}>$ ./start.sh start --build</div>
<div className="pt-2" style={{ color: '#F0B90B' }}>
<div style={{ color: '#8A8478' }}>$ git clone https://github.com/NoFxAiOS/nofx.git</div>
<div style={{ color: '#8A8478' }}>$ cd nofx && chmod +x start.sh</div>
<div style={{ color: '#8A8478' }}>$ ./start.sh start --build</div>
<div className="pt-2" style={{ color: '#E0483B' }}>
{t('startupMessages1', language)}
</div>
<div style={{ color: '#0ECB81' }}>
<div style={{ color: '#2E8B57' }}>
{t('startupMessages2', language)}
</div>
<div style={{ color: '#0ECB81' }}>
<div style={{ color: '#2E8B57' }}>
{t('startupMessages3', language)}
</div>
<motion.div
@@ -144,8 +144,8 @@ export default function AboutSection({ language }: AboutSectionProps) {
animate={{ opacity: [1, 0.5, 1] }}
transition={{ duration: 1.5, repeat: Infinity }}
>
<span style={{ color: '#F0B90B' }}></span>
<span style={{ color: '#EAECEF' }}>_</span>
<span style={{ color: '#E0483B' }}></span>
<span style={{ color: '#1A1813' }}>_</span>
</motion.div>
</div>
</div>

View File

@@ -4,7 +4,7 @@ import { motion, useInView } from 'framer-motion'
export default function AnimatedSection({
children,
id,
backgroundColor = 'var(--brand-black)',
backgroundColor = '#F1ECE2',
}: {
children: React.ReactNode
id?: string

View File

@@ -19,8 +19,8 @@ function TweetCard({ quote, authorName, handle, avatarUrl, tweetUrl, delay }: Tw
rel="noopener noreferrer"
className="block p-5 rounded-2xl transition-all duration-300 group"
style={{
background: '#12161C',
border: '1px solid rgba(255, 255, 255, 0.06)',
background: '#F7F4EC',
border: '1px solid rgba(26, 24, 19, 0.14)',
}}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
@@ -28,7 +28,7 @@ function TweetCard({ quote, authorName, handle, avatarUrl, tweetUrl, delay }: Tw
transition={{ delay }}
whileHover={{
y: -4,
borderColor: 'rgba(240, 185, 11, 0.3)',
borderColor: 'rgba(224, 72, 59, 0.3)',
}}
>
{/* Header */}
@@ -38,13 +38,13 @@ function TweetCard({ quote, authorName, handle, avatarUrl, tweetUrl, delay }: Tw
src={avatarUrl}
alt={authorName}
className="w-10 h-10 rounded-full object-cover"
style={{ border: '2px solid rgba(255, 255, 255, 0.1)' }}
style={{ border: '2px solid rgba(26, 24, 19, 0.14)' }}
/>
<div>
<div className="font-semibold text-sm" style={{ color: '#EAECEF' }}>
<div className="font-semibold text-sm" style={{ color: '#1A1813' }}>
{authorName}
</div>
<div className="text-xs" style={{ color: '#5E6673' }}>
<div className="text-xs" style={{ color: '#8A8478' }}>
{handle}
</div>
</div>
@@ -52,7 +52,7 @@ function TweetCard({ quote, authorName, handle, avatarUrl, tweetUrl, delay }: Tw
{/* X Logo */}
<div
className="w-6 h-6 flex items-center justify-center opacity-50 group-hover:opacity-100 transition-opacity"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
<svg viewBox="0 0 24 24" className="w-4 h-4" 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" />
@@ -63,27 +63,27 @@ function TweetCard({ quote, authorName, handle, avatarUrl, tweetUrl, delay }: Tw
{/* Content */}
<p
className="text-sm leading-relaxed mb-4 line-clamp-4"
style={{ color: '#B7BDC6' }}
style={{ color: '#1A1813' }}
>
{quote}
</p>
{/* Footer */}
<div className="flex items-center gap-6 pt-3" style={{ borderTop: '1px solid rgba(255, 255, 255, 0.05)' }}>
<div className="flex items-center gap-1.5 text-xs" style={{ color: '#5E6673' }}>
<div className="flex items-center gap-6 pt-3" style={{ borderTop: '1px solid rgba(26, 24, 19, 0.14)' }}>
<div className="flex items-center gap-1.5 text-xs" style={{ color: '#8A8478' }}>
<MessageCircle className="w-3.5 h-3.5" />
<span>Reply</span>
</div>
<div className="flex items-center gap-1.5 text-xs" style={{ color: '#5E6673' }}>
<div className="flex items-center gap-1.5 text-xs" style={{ color: '#8A8478' }}>
<Repeat2 className="w-3.5 h-3.5" />
<span>Repost</span>
</div>
<div className="flex items-center gap-1.5 text-xs" style={{ color: '#5E6673' }}>
<div className="flex items-center gap-1.5 text-xs" style={{ color: '#8A8478' }}>
<Heart className="w-3.5 h-3.5" />
<span>Like</span>
</div>
<div className="ml-auto opacity-0 group-hover:opacity-100 transition-opacity">
<ExternalLink className="w-3.5 h-3.5" style={{ color: '#F0B90B' }} />
<ExternalLink className="w-3.5 h-3.5" style={{ color: '#E0483B' }} />
</div>
</div>
</motion.a>
@@ -98,11 +98,11 @@ export default function CommunitySection({ language }: CommunitySectionProps) {
const tweets: TweetProps[] = []
return (
<section className="py-24 relative" style={{ background: '#0B0E11' }}>
<section className="py-24 relative" style={{ background: '#F1ECE2' }}>
{/* Background Decoration */}
<div
className="absolute right-0 top-1/2 -translate-y-1/2 w-96 h-96 rounded-full blur-3xl opacity-20"
style={{ background: 'radial-gradient(circle, rgba(29, 161, 242, 0.1) 0%, transparent 70%)' }}
style={{ background: 'radial-gradient(circle, rgba(224, 72, 59, 0.08) 0%, transparent 70%)' }}
/>
<div className="max-w-6xl mx-auto px-4 relative z-10">
@@ -113,11 +113,11 @@ export default function CommunitySection({ language }: CommunitySectionProps) {
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
>
<h2 className="text-4xl lg:text-5xl font-bold mb-4" style={{ color: '#EAECEF' }}>
{language === 'zh' ? '社区声音' : 'Community Voices'}
<h2 className="text-4xl lg:text-5xl font-bold mb-4" style={{ color: '#1A1813' }}>
{language === 'zh' ? 'Community Voices' : 'Community Voices'}
</h2>
<p className="text-lg" style={{ color: '#848E9C' }}>
{language === 'zh' ? '看看大家怎么说' : 'See what others are saying'}
<p className="text-lg" style={{ color: '#8A8478' }}>
{language === 'zh' ? 'See what others are saying' : 'See what others are saying'}
</p>
</motion.div>
@@ -141,15 +141,15 @@ export default function CommunitySection({ language }: CommunitySectionProps) {
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-6 py-3 rounded-xl font-medium transition-all hover:scale-105"
style={{
background: 'rgba(29, 161, 242, 0.1)',
color: '#1DA1F2',
border: '1px solid rgba(29, 161, 242, 0.3)',
background: 'rgba(224, 72, 59, 0.1)',
color: '#E0483B',
border: '1px solid rgba(224, 72, 59, 0.3)',
}}
>
<svg viewBox="0 0 24 24" className="w-5 h-5" 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" />
</svg>
{language === 'zh' ? '关注我们的 X' : 'Follow us on X'}
{language === 'zh' ? 'Follow us on X' : 'Follow us on X'}
</a>
</motion.div>
</div>

View File

@@ -10,61 +10,61 @@ export default function FeaturesSection({ language }: FeaturesSectionProps) {
const features = [
{
icon: Brain,
title: language === 'zh' ? 'AI 策略编排引擎' : 'AI Strategy Orchestration',
title: language === 'zh' ? 'AI Strategy Orchestration' : 'AI Strategy Orchestration',
desc: language === 'zh'
? '支持 DeepSeekGPTClaudeQwen 等多种大模型,自定义 Prompt 策略AI 自主分析市场并做出交易决策'
? 'Support DeepSeek, GPT, Claude, Qwen and more. Custom prompts, AI autonomously analyzes markets and makes trading decisions'
: 'Support DeepSeek, GPT, Claude, Qwen and more. Custom prompts, AI autonomously analyzes markets and makes trading decisions',
highlight: true,
badge: language === 'zh' ? '核心能力' : 'Core',
badge: language === 'zh' ? 'Core' : 'Core',
},
{
icon: Swords,
title: language === 'zh' ? '多 AI 竞技场' : 'Multi-AI Arena',
title: language === 'zh' ? 'Multi-AI Arena' : 'Multi-AI Arena',
desc: language === 'zh'
? '多个 AI 交易员同台竞技,实时 PnL 排行榜,自动优胜劣汰,让最强策略脱颖而出'
? 'Multiple AI traders compete in real-time, live PnL leaderboard, automatic survival of the fittest'
: 'Multiple AI traders compete in real-time, live PnL leaderboard, automatic survival of the fittest',
highlight: true,
badge: language === 'zh' ? '独创' : 'Unique',
badge: language === 'zh' ? 'Unique' : 'Unique',
},
{
icon: LineChart,
title: language === 'zh' ? '专业量化数据' : 'Pro Quant Data',
title: language === 'zh' ? 'Pro Quant Data' : 'Pro Quant Data',
desc: language === 'zh'
? '集成 K线、技术指标、市场深度、资金费率、持仓量等专业量化数据为 AI 决策提供全面信息'
? 'Integrated candlesticks, indicators, order book, funding rates, open interest - comprehensive data for AI decisions'
: 'Integrated candlesticks, indicators, order book, funding rates, open interest - comprehensive data for AI decisions',
highlight: true,
badge: language === 'zh' ? '专业' : 'Pro',
badge: language === 'zh' ? 'Pro' : 'Pro',
},
{
icon: Blocks,
title: language === 'zh' ? '多交易所支持' : 'Multi-Exchange Support',
title: language === 'zh' ? 'Multi-Exchange Support' : 'Multi-Exchange Support',
desc: language === 'zh'
? 'BinanceOKXBybitHyperliquidAster DEX,一套系统管理多个交易所'
? 'Binance, OKX, Bybit, Hyperliquid, Aster DEX - one system, multiple exchanges'
: 'Binance, OKX, Bybit, Hyperliquid, Aster DEX - one system, multiple exchanges',
},
{
icon: BarChart3,
title: language === 'zh' ? '实时可视化看板' : 'Real-time Dashboard',
title: language === 'zh' ? 'Real-time Dashboard' : 'Real-time Dashboard',
desc: language === 'zh'
? '交易监控、收益曲线、持仓分析、AI 决策日志,一目了然'
? 'Trade monitoring, PnL curves, position analysis, AI decision logs at a glance'
: 'Trade monitoring, PnL curves, position analysis, AI decision logs at a glance',
},
{
icon: Shield,
title: language === 'zh' ? '开源自托管' : 'Open Source & Self-Hosted',
title: language === 'zh' ? 'Open Source & Self-Hosted' : 'Open Source & Self-Hosted',
desc: language === 'zh'
? '代码完全开源可审计数据存储在本地API 密钥不经过第三方'
? 'Fully open source, data stored locally, API keys never leave your server'
: 'Fully open source, data stored locally, API keys never leave your server',
},
]
return (
<section className="py-24 relative" style={{ background: '#0B0E11' }}>
<section className="py-24 relative" style={{ background: '#F1ECE2' }}>
{/* Background */}
<div
className="absolute inset-0 opacity-[0.02]"
className="absolute inset-0 opacity-[0.04]"
style={{
backgroundImage: `linear-gradient(#F0B90B 1px, transparent 1px), linear-gradient(90deg, #F0B90B 1px, transparent 1px)`,
backgroundImage: `linear-gradient(#E0483B 1px, transparent 1px), linear-gradient(90deg, #E0483B 1px, transparent 1px)`,
backgroundSize: '40px 40px',
}}
/>
@@ -77,12 +77,12 @@ export default function FeaturesSection({ language }: FeaturesSectionProps) {
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
>
<h2 className="text-4xl lg:text-5xl font-bold mb-4" style={{ color: '#EAECEF' }}>
<h2 className="text-4xl lg:text-5xl font-bold mb-4" style={{ color: '#1A1813' }}>
{t('whyChooseNofx', language)}
</h2>
<p className="text-lg max-w-2xl mx-auto" style={{ color: '#848E9C' }}>
<p className="text-lg max-w-2xl mx-auto" style={{ color: '#8A8478' }}>
{language === 'zh'
? '不只是交易机器人,而是完整的 AI 交易操作系统'
? 'Not just a trading bot, but a complete AI trading operating system'
: 'Not just a trading bot, but a complete AI trading operating system'}
</p>
</motion.div>
@@ -102,11 +102,11 @@ export default function FeaturesSection({ language }: FeaturesSectionProps) {
`}
style={{
background: feature.highlight
? 'linear-gradient(135deg, rgba(240, 185, 11, 0.08) 0%, rgba(240, 185, 11, 0.02) 100%)'
: '#12161C',
? 'rgba(224, 72, 59, 0.06)'
: '#F7F4EC',
border: feature.highlight
? '1px solid rgba(240, 185, 11, 0.2)'
: '1px solid rgba(255, 255, 255, 0.06)',
? '1px solid rgba(224, 72, 59, 0.2)'
: '1px solid rgba(26, 24, 19, 0.14)',
}}
>
{/* Badge */}
@@ -114,8 +114,8 @@ export default function FeaturesSection({ language }: FeaturesSectionProps) {
<div
className="absolute top-4 right-4 px-2 py-1 rounded text-xs font-medium"
style={{
background: 'rgba(240, 185, 11, 0.15)',
color: '#F0B90B',
background: 'rgba(224, 72, 59, 0.15)',
color: '#E0483B',
}}
>
{feature.badge}
@@ -127,36 +127,36 @@ export default function FeaturesSection({ language }: FeaturesSectionProps) {
className="w-12 h-12 rounded-xl flex items-center justify-center mb-4"
style={{
background: feature.highlight
? 'rgba(240, 185, 11, 0.15)'
: 'rgba(240, 185, 11, 0.1)',
border: '1px solid rgba(240, 185, 11, 0.2)',
? 'rgba(224, 72, 59, 0.15)'
: 'rgba(224, 72, 59, 0.1)',
border: '1px solid rgba(224, 72, 59, 0.2)',
}}
whileHover={{ scale: 1.1, rotate: 5 }}
>
<feature.icon
className="w-6 h-6"
style={{ color: '#F0B90B' }}
style={{ color: '#E0483B' }}
/>
</motion.div>
{/* Text */}
<h3
className="text-xl font-bold mb-3"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
{feature.title}
</h3>
<p
className="text-sm leading-relaxed"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
{feature.desc}
</p>
{/* Hover Glow */}
<div
className="absolute -bottom-10 -right-10 w-32 h-32 rounded-full blur-3xl opacity-0 group-hover:opacity-30 transition-opacity duration-500"
style={{ background: '#F0B90B' }}
className="absolute -bottom-10 -right-10 w-32 h-32 rounded-full blur-3xl opacity-0 group-hover:opacity-20 transition-opacity duration-500"
style={{ background: '#E0483B' }}
/>
</motion.div>
))}
@@ -170,30 +170,28 @@ export default function FeaturesSection({ language }: FeaturesSectionProps) {
viewport={{ once: true }}
>
{[
{ value: '10+', label: language === 'zh' ? 'AI 模型支持' : 'AI Models' },
{ value: '5+', label: language === 'zh' ? '交易所集成' : 'Exchanges' },
{ value: '24/7', label: language === 'zh' ? '自动交易' : 'Auto Trading' },
{ value: '100%', label: language === 'zh' ? '开源免费' : 'Open Source' },
{ value: '10+', label: language === 'zh' ? 'AI Models' : 'AI Models' },
{ value: '5+', label: language === 'zh' ? 'Exchanges' : 'Exchanges' },
{ value: '24/7', label: language === 'zh' ? 'Auto Trading' : 'Auto Trading' },
{ value: '100%', label: language === 'zh' ? 'Open Source' : 'Open Source' },
].map((stat) => (
<div
key={stat.label}
className="text-center p-4 rounded-xl"
style={{
background: 'rgba(255, 255, 255, 0.02)',
border: '1px solid rgba(255, 255, 255, 0.05)',
background: '#F7F4EC',
border: '1px solid rgba(26, 24, 19, 0.14)',
}}
>
<div
className="text-2xl font-bold mb-1"
style={{
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
color: '#E0483B',
}}
>
{stat.value}
</div>
<div className="text-xs" style={{ color: '#5E6673' }}>
<div className="text-xs" style={{ color: '#8A8478' }}>
{stat.label}
</div>
</div>

View File

@@ -23,7 +23,7 @@ export default function FooterSection({ language }: FooterSectionProps) {
],
resources: [
{
name: language === 'zh' ? '文档' : 'Documentation',
name: language === 'zh' ? 'Documentation' : 'Documentation',
href: 'https://github.com/NoFxAiOS/nofx/blob/main/README.md',
},
{ name: 'Issues', href: 'https://github.com/NoFxAiOS/nofx/issues' },
@@ -43,7 +43,7 @@ export default function FooterSection({ language }: FooterSectionProps) {
}
return (
<footer style={{ background: '#0B0E11', borderTop: '1px solid rgba(255, 255, 255, 0.06)' }}>
<footer style={{ background: '#F1ECE2', borderTop: '1px solid rgba(26, 24, 19, 0.14)' }}>
<div className="max-w-6xl mx-auto px-4 py-8 md:py-12">
{/* Top Section */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-8 md:gap-10 mb-8 md:mb-12">
@@ -51,11 +51,11 @@ export default function FooterSection({ language }: FooterSectionProps) {
<div className="md:col-span-1">
<div className="flex items-center gap-3 mb-4">
<img src="/icons/nofx.svg" alt="NOFX Logo" className="w-8 h-8" />
<span className="text-xl font-bold" style={{ color: '#EAECEF' }}>
<span className="text-xl font-bold" style={{ color: '#1A1813' }}>
NOFX
</span>
</div>
<p className="text-sm mb-6" style={{ color: '#5E6673' }}>
<p className="text-sm mb-6" style={{ color: '#8A8478' }}>
{t('futureStandardAI', language)}
</p>
{/* Social Icons */}
@@ -68,8 +68,8 @@ export default function FooterSection({ language }: FooterSectionProps) {
rel="noopener noreferrer"
className="w-9 h-9 rounded-lg flex items-center justify-center transition-all hover:scale-110"
style={{
background: 'rgba(255, 255, 255, 0.05)',
color: '#848E9C',
background: '#E8E2D5',
color: '#8A8478',
}}
title={link.name}
>
@@ -81,7 +81,7 @@ export default function FooterSection({ language }: FooterSectionProps) {
{/* Links */}
<div>
<h4 className="text-sm font-semibold mb-4" style={{ color: '#EAECEF' }}>
<h4 className="text-sm font-semibold mb-4" style={{ color: '#1A1813' }}>
{t('links', language)}
</h4>
<ul className="space-y-3">
@@ -91,8 +91,8 @@ export default function FooterSection({ language }: FooterSectionProps) {
href={link.href}
target="_blank"
rel="noopener noreferrer"
className="text-sm transition-colors hover:text-[#F0B90B]"
style={{ color: '#5E6673' }}
className="text-sm transition-colors hover:text-[#E0483B]"
style={{ color: '#8A8478' }}
>
{link.name}
</a>
@@ -103,7 +103,7 @@ export default function FooterSection({ language }: FooterSectionProps) {
{/* Resources */}
<div>
<h4 className="text-sm font-semibold mb-4" style={{ color: '#EAECEF' }}>
<h4 className="text-sm font-semibold mb-4" style={{ color: '#1A1813' }}>
{t('resources', language)}
</h4>
<ul className="space-y-3">
@@ -113,8 +113,8 @@ export default function FooterSection({ language }: FooterSectionProps) {
href={link.href}
target="_blank"
rel="noopener noreferrer"
className="text-sm transition-colors hover:text-[#F0B90B] inline-flex items-center gap-1"
style={{ color: '#5E6673' }}
className="text-sm transition-colors hover:text-[#E0483B] inline-flex items-center gap-1"
style={{ color: '#8A8478' }}
>
{link.name}
<ExternalLink className="w-3 h-3 opacity-50" />
@@ -126,7 +126,7 @@ export default function FooterSection({ language }: FooterSectionProps) {
{/* Supporters */}
<div>
<h4 className="text-sm font-semibold mb-4" style={{ color: '#EAECEF' }}>
<h4 className="text-sm font-semibold mb-4" style={{ color: '#1A1813' }}>
{t('supporters', language)}
</h4>
<div className="flex flex-wrap gap-2">
@@ -136,8 +136,8 @@ export default function FooterSection({ language }: FooterSectionProps) {
href={link.href}
target="_blank"
rel="noopener noreferrer"
className="text-xs border border-zinc-800 bg-zinc-900/50 rounded px-3 py-1.5 transition-all hover:border-[#F0B90B] hover:text-[#F0B90B] hover:bg-[#F0B90B]/10 hover:shadow-[0_0_10px_rgba(240,185,11,0.2)]"
style={{ color: '#848E9C' }}
className="text-xs border border-[rgba(26,24,19,0.14)] bg-nofx-bg-lighter rounded px-3 py-1.5 transition-all hover:border-[#E0483B] hover:text-[#E0483B] hover:bg-[#E0483B]/10"
style={{ color: '#8A8478' }}
>
{link.name}
</a>
@@ -149,10 +149,10 @@ export default function FooterSection({ language }: FooterSectionProps) {
{/* Bottom Section */}
<div
className="pt-6 text-center text-xs"
style={{ color: '#5E6673', borderTop: '1px solid rgba(255, 255, 255, 0.06)' }}
style={{ color: '#8A8478', borderTop: '1px solid rgba(26, 24, 19, 0.14)' }}
>
<p className="mb-2">{t('footerTitle', language)}</p>
<p style={{ color: '#3C4249' }}>{t('footerWarning', language)}</p>
<p style={{ color: '#B4B2A9' }}>{t('footerWarning', language)}</p>
</div>
</div>
</footer>

View File

@@ -26,7 +26,7 @@ export default function HeroSection({ language }: HeroSectionProps) {
<div
className="absolute inset-0 opacity-[0.03]"
style={{
backgroundImage: `linear-gradient(#F0B90B 1px, transparent 1px), linear-gradient(90deg, #F0B90B 1px, transparent 1px)`,
backgroundImage: `linear-gradient(#E0483B 1px, transparent 1px), linear-gradient(90deg, #E0483B 1px, transparent 1px)`,
backgroundSize: '60px 60px',
}}
/>
@@ -35,13 +35,13 @@ export default function HeroSection({ language }: HeroSectionProps) {
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[800px] rounded-full"
style={{
background:
'radial-gradient(circle, rgba(240, 185, 11, 0.08) 0%, transparent 70%)',
'radial-gradient(circle, rgba(224, 72, 59, 0.08) 0%, transparent 70%)',
}}
/>
{/* Floating Orbs */}
<motion.div
className="absolute top-20 right-20 w-32 h-32 rounded-full blur-3xl"
style={{ background: 'rgba(240, 185, 11, 0.15)' }}
style={{ background: 'rgba(224, 72, 59, 0.12)' }}
animate={{
y: [0, 30, 0],
scale: [1, 1.1, 1],
@@ -50,7 +50,7 @@ export default function HeroSection({ language }: HeroSectionProps) {
/>
<motion.div
className="absolute bottom-40 left-20 w-48 h-48 rounded-full blur-3xl"
style={{ background: 'rgba(240, 185, 11, 0.1)' }}
style={{ background: 'rgba(224, 72, 59, 0.08)' }}
animate={{
y: [0, -40, 0],
scale: [1, 1.2, 1],
@@ -67,21 +67,20 @@ export default function HeroSection({ language }: HeroSectionProps) {
transition={{ duration: 0.6 }}
className="inline-flex items-center gap-2 px-4 py-2 rounded-full mb-8"
style={{
background: 'rgba(240, 185, 11, 0.1)',
border: '1px solid rgba(240, 185, 11, 0.3)',
background: 'rgba(224, 72, 59, 0.1)',
border: '1px solid rgba(224, 72, 59, 0.3)',
}}
>
<Zap className="w-4 h-4" style={{ color: '#F0B90B' }} />
<span className="text-sm font-medium" style={{ color: '#F0B90B' }}>
<Zap className="w-4 h-4" style={{ color: '#E0483B' }} />
<span className="text-sm font-medium" style={{ color: '#E0483B' }}>
{isLoading ? (
t('githubStarsInDays', language)
) : language === 'zh' ? (
<>
{daysOld} {' '}
<span className="font-bold tabular-nums">
{(animatedStars / 1000).toFixed(1)}K+
</span>{' '}
GitHub Stars
GitHub Stars in {daysOld} days
</>
) : (
<>
@@ -101,20 +100,18 @@ export default function HeroSection({ language }: HeroSectionProps) {
transition={{ duration: 0.6, delay: 0.1 }}
className="text-5xl sm:text-6xl lg:text-7xl font-bold mb-6 leading-tight"
>
<span style={{ color: '#EAECEF' }}>{t('heroTitle1', language)}</span>
<span style={{ color: '#1A1813' }}>{t('heroTitle1', language)}</span>
<br />
<span
className="relative inline-block"
style={{
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
color: '#E0483B',
}}
>
{t('heroTitle2', language)}
<motion.span
className="absolute -bottom-2 left-0 h-1 rounded-full"
style={{ background: 'linear-gradient(90deg, #F0B90B, #FCD535)' }}
style={{ background: '#E0483B' }}
initial={{ width: 0 }}
animate={{ width: '100%' }}
transition={{ duration: 0.8, delay: 0.8 }}
@@ -128,7 +125,7 @@ export default function HeroSection({ language }: HeroSectionProps) {
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="text-lg sm:text-xl max-w-3xl mx-auto mb-10 leading-relaxed"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
{t('heroDescription', language)}
</motion.p>
@@ -143,13 +140,13 @@ export default function HeroSection({ language }: HeroSectionProps) {
<motion.div
className="group flex items-center gap-3 px-8 py-4 rounded-xl font-bold text-lg transition-all"
style={{
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
color: '#0B0E11',
boxShadow: '0 4px 24px rgba(240, 185, 11, 0.3)',
background: '#E0483B',
color: '#F7F4EC',
boxShadow: '0 4px 24px rgba(224, 72, 59, 0.25)',
}}
whileHover={{
scale: 1.02,
boxShadow: '0 8px 32px rgba(240, 185, 11, 0.4)',
boxShadow: '0 8px 32px rgba(224, 72, 59, 0.35)',
}}
whileTap={{ scale: 0.98 }}
>
@@ -166,14 +163,14 @@ export default function HeroSection({ language }: HeroSectionProps) {
rel="noopener noreferrer"
className="group flex items-center gap-3 px-8 py-4 rounded-xl font-bold text-lg transition-all"
style={{
background: 'rgba(255, 255, 255, 0.05)',
color: '#EAECEF',
border: '1px solid rgba(255, 255, 255, 0.1)',
background: '#F7F4EC',
color: '#1A1813',
border: '1px solid rgba(26, 24, 19, 0.14)',
}}
whileHover={{
scale: 1.02,
background: 'rgba(255, 255, 255, 0.08)',
borderColor: 'rgba(240, 185, 11, 0.3)',
background: '#F2EEE4',
borderColor: 'rgba(224, 72, 59, 0.3)',
}}
whileTap={{ scale: 0.98 }}
>
@@ -192,15 +189,15 @@ export default function HeroSection({ language }: HeroSectionProps) {
{[
{ label: 'GitHub Stars', value: `${(stars / 1000).toFixed(1)}K+` },
{
label: language === 'zh' ? '支持交易所' : 'Exchanges',
label: language === 'zh' ? 'Exchanges' : 'Exchanges',
value: '5+',
},
{
label: language === 'zh' ? 'AI 模型' : 'AI Models',
label: language === 'zh' ? 'AI Models' : 'AI Models',
value: '10+',
},
{
label: language === 'zh' ? '开源免费' : 'Open Source',
label: language === 'zh' ? 'Open Source' : 'Open Source',
value: '100%',
},
].map((stat, index) => (
@@ -214,15 +211,12 @@ export default function HeroSection({ language }: HeroSectionProps) {
<div
className="text-3xl sm:text-4xl font-bold mb-1"
style={{
background:
'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
color: '#E0483B',
}}
>
{stat.value}
</div>
<div className="text-sm" style={{ color: '#5E6673' }}>
<div className="text-sm" style={{ color: '#8A8478' }}>
{stat.label}
</div>
</motion.div>
@@ -235,7 +229,7 @@ export default function HeroSection({ language }: HeroSectionProps) {
animate={{ opacity: 1 }}
transition={{ duration: 0.6, delay: 0.8 }}
className="mt-16 text-xs"
style={{ color: '#5E6673' }}
style={{ color: '#8A8478' }}
>
{t('poweredBy', language)}
</motion.p>
@@ -250,13 +244,13 @@ export default function HeroSection({ language }: HeroSectionProps) {
>
<motion.div
className="w-6 h-10 rounded-full flex justify-center pt-2"
style={{ border: '2px solid rgba(240, 185, 11, 0.3)' }}
style={{ border: '2px solid rgba(224, 72, 59, 0.3)' }}
animate={{ y: [0, 8, 0] }}
transition={{ duration: 2, repeat: Infinity }}
>
<motion.div
className="w-1.5 h-3 rounded-full"
style={{ background: '#F0B90B' }}
style={{ background: '#E0483B' }}
/>
</motion.div>
</motion.div>

View File

@@ -11,38 +11,38 @@ export default function HowItWorksSection({ language }: HowItWorksSectionProps)
{
icon: Download,
number: '01',
title: language === 'zh' ? '一键部署' : 'One-Click Deploy',
title: language === 'zh' ? 'One-Click Deploy' : 'One-Click Deploy',
desc: language === 'zh'
? '在你的服务器上运行一条命令即可完成部署'
? 'Run a single command on your server to deploy'
: 'Run a single command on your server to deploy',
code: 'curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash',
},
{
icon: Rocket,
number: '02',
title: language === 'zh' ? '访问面板' : 'Access Dashboard',
title: language === 'zh' ? 'Access Dashboard' : 'Access Dashboard',
desc: language === 'zh'
? '通过浏览器访问你的服务器'
? 'Access your server via browser'
: 'Access your server via browser',
code: 'http://YOUR_SERVER_IP:3000',
},
{
icon: TrendingUp,
number: '03',
title: language === 'zh' ? '开始交易' : 'Start Trading',
title: language === 'zh' ? 'Start Trading' : 'Start Trading',
desc: language === 'zh'
? '创建交易员,让 AI 开始工作'
? 'Create trader, let AI do the work'
: 'Create trader, let AI do the work',
code: language === 'zh' ? '配置模型 → 配置交易所 → 创建交易员' : 'Configure Model → Exchange → Create Trader',
code: language === 'zh' ? 'Configure Model → Exchange → Create Trader' : 'Configure Model → Exchange → Create Trader',
},
]
return (
<section className="py-24 relative overflow-hidden" style={{ background: '#0D1117' }}>
<section className="py-24 relative overflow-hidden" style={{ background: '#F1ECE2' }}>
{/* Background Decoration */}
<div
className="absolute left-0 top-1/2 -translate-y-1/2 w-96 h-96 rounded-full blur-3xl opacity-20"
style={{ background: 'radial-gradient(circle, rgba(240, 185, 11, 0.15) 0%, transparent 70%)' }}
style={{ background: 'radial-gradient(circle, rgba(224, 72, 59, 0.12) 0%, transparent 70%)' }}
/>
<div className="max-w-6xl mx-auto px-4 relative z-10">
@@ -53,10 +53,10 @@ export default function HowItWorksSection({ language }: HowItWorksSectionProps)
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
>
<h2 className="text-4xl lg:text-5xl font-bold mb-4" style={{ color: '#EAECEF' }}>
<h2 className="text-4xl lg:text-5xl font-bold mb-4" style={{ color: '#1A1813' }}>
{t('howToStart', language)}
</h2>
<p className="text-lg" style={{ color: '#848E9C' }}>
<p className="text-lg" style={{ color: '#8A8478' }}>
{t('fourSimpleSteps', language)}
</p>
</motion.div>
@@ -66,7 +66,7 @@ export default function HowItWorksSection({ language }: HowItWorksSectionProps)
{/* Connecting Line */}
<div
className="absolute left-[39px] top-0 bottom-0 w-px hidden lg:block"
style={{ background: 'linear-gradient(to bottom, transparent, rgba(240, 185, 11, 0.3), transparent)' }}
style={{ background: 'linear-gradient(to bottom, transparent, rgba(224, 72, 59, 0.3), transparent)' }}
/>
<div className="space-y-6">
@@ -82,8 +82,8 @@ export default function HowItWorksSection({ language }: HowItWorksSectionProps)
<div
className="flex flex-col lg:flex-row items-start gap-6 p-6 rounded-2xl transition-all duration-300 hover:translate-x-2"
style={{
background: 'rgba(255, 255, 255, 0.02)',
border: '1px solid rgba(255, 255, 255, 0.05)',
background: '#F7F4EC',
border: '1px solid rgba(26, 24, 19, 0.14)',
}}
>
{/* Number Circle */}
@@ -91,12 +91,12 @@ export default function HowItWorksSection({ language }: HowItWorksSectionProps)
<motion.div
className="w-20 h-20 rounded-2xl flex items-center justify-center"
style={{
background: 'linear-gradient(135deg, rgba(240, 185, 11, 0.2) 0%, rgba(240, 185, 11, 0.05) 100%)',
border: '1px solid rgba(240, 185, 11, 0.3)',
background: 'linear-gradient(135deg, rgba(224, 72, 59, 0.15) 0%, rgba(224, 72, 59, 0.05) 100%)',
border: '1px solid rgba(224, 72, 59, 0.3)',
}}
whileHover={{ scale: 1.1 }}
>
<step.icon className="w-8 h-8" style={{ color: '#F0B90B' }} />
<step.icon className="w-8 h-8" style={{ color: '#E0483B' }} />
</motion.div>
</div>
@@ -105,15 +105,15 @@ export default function HowItWorksSection({ language }: HowItWorksSectionProps)
<div className="flex items-center gap-3 mb-2">
<span
className="text-sm font-mono font-bold"
style={{ color: '#F0B90B' }}
style={{ color: '#E0483B' }}
>
{step.number}
</span>
<h3 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
<h3 className="text-xl font-bold" style={{ color: '#1A1813' }}>
{step.title}
</h3>
</div>
<p className="mb-4" style={{ color: '#848E9C' }}>
<p className="mb-4" style={{ color: '#8A8478' }}>
{step.desc}
</p>
@@ -121,12 +121,12 @@ export default function HowItWorksSection({ language }: HowItWorksSectionProps)
<div
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg font-mono text-sm"
style={{
background: 'rgba(0, 0, 0, 0.3)',
border: '1px solid rgba(255, 255, 255, 0.06)',
background: '#E8E2D5',
border: '1px solid rgba(26, 24, 19, 0.14)',
}}
>
<span style={{ color: '#5E6673' }}>$</span>
<span style={{ color: '#EAECEF' }}>{step.code}</span>
<span style={{ color: '#8A8478' }}>$</span>
<span style={{ color: '#1A1813' }}>{step.code}</span>
</div>
</div>
</div>
@@ -139,8 +139,8 @@ export default function HowItWorksSection({ language }: HowItWorksSectionProps)
<motion.div
className="mt-12 p-6 rounded-2xl flex items-start gap-4"
style={{
background: 'rgba(240, 185, 11, 0.05)',
border: '1px solid rgba(240, 185, 11, 0.15)',
background: 'rgba(224, 72, 59, 0.05)',
border: '1px solid rgba(224, 72, 59, 0.15)',
}}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
@@ -148,15 +148,15 @@ export default function HowItWorksSection({ language }: HowItWorksSectionProps)
>
<div
className="w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0"
style={{ background: 'rgba(240, 185, 11, 0.1)' }}
style={{ background: 'rgba(224, 72, 59, 0.1)' }}
>
<AlertTriangle className="w-6 h-6" style={{ color: '#F0B90B' }} />
<AlertTriangle className="w-6 h-6" style={{ color: '#E0483B' }} />
</div>
<div>
<div className="font-semibold mb-2" style={{ color: '#F0B90B' }}>
<div className="font-semibold mb-2" style={{ color: '#E0483B' }}>
{t('importantRiskWarning', language)}
</div>
<p className="text-sm leading-relaxed" style={{ color: '#5E6673' }}>
<p className="text-sm leading-relaxed" style={{ color: '#8A8478' }}>
{t('riskWarningText', language)}
</p>
</div>

View File

@@ -13,7 +13,7 @@ export default function LoginModal({ onClose, language }: LoginModalProps) {
return (
<motion.div
className="fixed inset-0 z-50 flex items-center justify-center p-4"
style={{ background: 'rgba(0, 0, 0, 0.8)' }}
style={{ background: 'rgba(26, 24, 19, 0.55)' }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
@@ -22,8 +22,8 @@ export default function LoginModal({ onClose, language }: LoginModalProps) {
<motion.div
className="relative max-w-md w-full rounded-2xl p-8"
style={{
background: 'var(--brand-dark-gray)',
border: '1px solid rgba(240, 185, 11, 0.2)',
background: '#F7F4EC',
border: '1px solid rgba(224, 72, 59, 0.2)',
}}
initial={{ scale: 0.9, y: 50 }}
animate={{ scale: 1, y: 0 }}
@@ -33,7 +33,7 @@ export default function LoginModal({ onClose, language }: LoginModalProps) {
<motion.button
onClick={onClose}
className="absolute top-4 right-4"
style={{ color: 'var(--text-secondary)' }}
style={{ color: '#8A8478' }}
whileHover={{ scale: 1.1, rotate: 90 }}
whileTap={{ scale: 0.9 }}
>
@@ -41,11 +41,11 @@ export default function LoginModal({ onClose, language }: LoginModalProps) {
</motion.button>
<h2
className="text-2xl font-bold mb-6"
style={{ color: 'var(--brand-light-gray)' }}
style={{ color: '#1A1813' }}
>
{t('accessNofxPlatform', language)}
</h2>
<p className="text-sm mb-6" style={{ color: 'var(--text-secondary)' }}>
<p className="text-sm mb-6" style={{ color: '#8A8478' }}>
{t('loginRegisterPrompt', language)}
</p>
<div className="space-y-3">
@@ -56,12 +56,12 @@ export default function LoginModal({ onClose, language }: LoginModalProps) {
}}
className="block w-full px-6 py-3 rounded-lg font-semibold text-center"
style={{
background: 'var(--brand-yellow)',
color: 'var(--brand-black)',
background: '#E0483B',
color: '#F7F4EC',
}}
whileHover={{
scale: 1.05,
boxShadow: '0 10px 30px rgba(240, 185, 11, 0.4)',
boxShadow: '0 10px 30px rgba(224, 72, 59, 0.3)',
}}
whileTap={{ scale: 0.95 }}
>

View File

@@ -9,47 +9,42 @@ export default function AgentTerminal() {
className="w-[380px] lg:w-[440px] relative group"
>
{/* Terminal frame */}
<div className="relative bg-[#0B0F14] rounded-2xl overflow-hidden shadow-2xl shadow-black/80 border border-zinc-800/80">
{/* Scanline overlay */}
<div className="absolute inset-0 pointer-events-none z-50 opacity-[0.02]" style={{
backgroundImage: 'repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(255,255,255,0.03) 2px, rgba(255,255,255,0.03) 4px)'
}} />
<div className="relative bg-nofx-bg-lighter rounded-2xl overflow-hidden shadow-lg border border-[rgba(26,24,19,0.14)]">
{/* Header bar - macOS style */}
<div className="flex items-center justify-between px-4 py-2.5 bg-[#0D1117] border-b border-zinc-800/60">
<div className="flex items-center justify-between px-4 py-2.5 bg-nofx-bg-deeper border-b border-[rgba(26,24,19,0.14)]">
{/* Window controls */}
<div className="flex items-center gap-2">
<div className="flex items-center gap-1.5">
<div className="w-3 h-3 rounded-full bg-[#ff5f57] hover:brightness-110 transition-all" />
<div className="w-3 h-3 rounded-full bg-[#febc2e] hover:brightness-110 transition-all" />
<div className="w-3 h-3 rounded-full bg-[#28c840] hover:brightness-110 transition-all" />
<div className="w-3 h-3 rounded-full bg-[#D6433A] hover:brightness-110 transition-all" />
<div className="w-3 h-3 rounded-full bg-[#E0483B] hover:brightness-110 transition-all" />
<div className="w-3 h-3 rounded-full bg-[#2E8B57] hover:brightness-110 transition-all" />
</div>
</div>
{/* Title */}
<div className="absolute left-1/2 -translate-x-1/2 flex items-center gap-2">
<span className="text-zinc-400 text-xs font-mono">NOFX Trader Terminal</span>
<span className="text-nofx-text-muted text-xs font-mono">NOFX Trader Terminal</span>
</div>
{/* Live indicator */}
<div className="flex items-center gap-1.5 px-2 py-0.5 rounded bg-green-500/10 border border-green-500/20">
<div className="w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse" />
<span className="text-green-400 text-[10px] font-mono uppercase tracking-wider">Live</span>
<div className="flex items-center gap-1.5 px-2 py-0.5 rounded bg-nofx-success/10 border border-nofx-success/20">
<div className="w-1.5 h-1.5 bg-nofx-success rounded-full animate-pulse" />
<span className="text-nofx-success text-[10px] font-mono uppercase tracking-wider">Live</span>
</div>
</div>
{/* Portfolio PnL Section */}
<div className="p-4 border-b border-zinc-800/40">
<div className="p-4 border-b border-[rgba(26,24,19,0.14)]">
<div className="flex items-center justify-between mb-3">
<span className="text-zinc-500 text-xs font-mono uppercase tracking-wider">Portfolio PnL</span>
<span className="text-nofx-text-muted text-xs font-mono uppercase tracking-wider">Portfolio PnL</span>
<div className="flex gap-1">
<button className="px-2 py-0.5 bg-nofx-gold/20 border border-nofx-gold/30 rounded text-[10px] text-nofx-gold font-mono">24H</button>
<button className="px-2 py-0.5 text-[10px] text-zinc-600 font-mono hover:text-zinc-400 transition-colors">7D</button>
<button className="px-2 py-0.5 text-[10px] text-zinc-600 font-mono hover:text-zinc-400 transition-colors">30D</button>
<button className="px-2 py-0.5 text-[10px] text-nofx-text-muted font-mono hover:text-nofx-text transition-colors">7D</button>
<button className="px-2 py-0.5 text-[10px] text-nofx-text-muted font-mono hover:text-nofx-text transition-colors">30D</button>
</div>
</div>
<div className="flex items-baseline gap-3">
<span className="text-3xl font-bold text-green-400 font-mono tracking-tight">+$12,847.50</span>
<span className="text-green-500/80 text-sm font-mono">+8.42%</span>
<span className="text-3xl font-bold text-nofx-success font-mono tracking-tight">+$12,847.50</span>
<span className="text-nofx-success/80 text-sm font-mono">+8.42%</span>
</div>
{/* Chart Area */}
@@ -57,8 +52,8 @@ export default function AgentTerminal() {
<svg className="w-full h-full" preserveAspectRatio="none" viewBox="0 0 400 64">
<defs>
<linearGradient id="chartGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="#22C55E" stopOpacity="0.2" />
<stop offset="100%" stopColor="#22C55E" stopOpacity="0" />
<stop offset="0%" stopColor="#2E8B57" stopOpacity="0.2" />
<stop offset="100%" stopColor="#2E8B57" stopOpacity="0" />
</linearGradient>
</defs>
<path
@@ -68,7 +63,7 @@ export default function AgentTerminal() {
<path
d="M0,56 C40,52 80,48 120,40 C160,32 200,28 240,24 C280,20 320,16 360,12 L400,8"
fill="none"
stroke="#22C55E"
stroke="#2E8B57"
strokeWidth="1.5"
/>
</svg>
@@ -76,32 +71,32 @@ export default function AgentTerminal() {
</div>
{/* Metrics Row */}
<div className="grid grid-cols-3 divide-x divide-zinc-800/40 border-b border-zinc-800/40">
<div className="grid grid-cols-3 divide-x divide-[rgba(26,24,19,0.14)] border-b border-[rgba(26,24,19,0.14)]">
<div className="p-3 text-center">
<div className="text-zinc-500 text-[10px] font-mono uppercase tracking-wider mb-1">OI</div>
<div className="text-white font-bold font-mono">$847M</div>
<div className="text-green-500 text-[10px] font-mono"> 2.1%</div>
<div className="text-nofx-text-muted text-[10px] font-mono uppercase tracking-wider mb-1">OI</div>
<div className="text-nofx-text font-bold font-mono">$847M</div>
<div className="text-nofx-success text-[10px] font-mono"> 2.1%</div>
</div>
<div className="p-3 text-center">
<div className="text-zinc-500 text-[10px] font-mono uppercase tracking-wider mb-1">Netflow</div>
<div className="text-green-400 font-bold font-mono">+$124M</div>
<div className="text-zinc-500 text-[10px] font-mono">24h inflow</div>
<div className="text-nofx-text-muted text-[10px] font-mono uppercase tracking-wider mb-1">Netflow</div>
<div className="text-nofx-success font-bold font-mono">+$124M</div>
<div className="text-nofx-text-muted text-[10px] font-mono">24h inflow</div>
</div>
<div className="p-3 text-center">
<div className="text-zinc-500 text-[10px] font-mono uppercase tracking-wider mb-1">L/S Ratio</div>
<div className="text-white font-bold font-mono">1.24</div>
<div className="text-nofx-text-muted text-[10px] font-mono uppercase tracking-wider mb-1">L/S Ratio</div>
<div className="text-nofx-text font-bold font-mono">1.24</div>
<div className="flex gap-0.5 mt-1 px-2">
<div className="h-1 bg-green-500/60 rounded-l flex-[55]" />
<div className="h-1 bg-red-500/60 rounded-r flex-[45]" />
<div className="h-1 bg-nofx-success/60 rounded-l flex-[55]" />
<div className="h-1 bg-nofx-danger/60 rounded-r flex-[45]" />
</div>
</div>
</div>
{/* Order Book */}
<div className="p-4 border-b border-zinc-800/40">
<div className="p-4 border-b border-[rgba(26,24,19,0.14)]">
<div className="flex items-center justify-between mb-3">
<span className="text-zinc-400 text-xs font-mono uppercase tracking-wider">Order Book</span>
<span className="text-zinc-600 text-[10px] font-mono">Spread: <span className="text-nofx-gold">0.02%</span></span>
<span className="text-nofx-text text-xs font-mono uppercase tracking-wider">Order Book</span>
<span className="text-nofx-text-muted text-[10px] font-mono">Spread: <span className="text-nofx-gold">0.02%</span></span>
</div>
<div className="grid grid-cols-2 gap-3">
{/* Asks */}
@@ -112,9 +107,9 @@ export default function AgentTerminal() {
{ price: '97,251.00', amount: '0.945', depth: 30 },
].map((ask, i) => (
<div key={i} className="relative flex justify-between text-[11px] py-1 px-1.5 rounded">
<div className="absolute inset-0 bg-red-500/10 rounded-sm" style={{ width: `${ask.depth}%` }} />
<span className="relative text-red-400 font-mono">{ask.price}</span>
<span className="relative text-zinc-500 font-mono">{ask.amount}</span>
<div className="absolute inset-0 bg-nofx-danger/10 rounded-sm" style={{ width: `${ask.depth}%` }} />
<span className="relative text-nofx-danger font-mono">{ask.price}</span>
<span className="relative text-nofx-text-muted font-mono">{ask.amount}</span>
</div>
))}
</div>
@@ -126,9 +121,9 @@ export default function AgentTerminal() {
{ price: '97,198.00', amount: '1.845', depth: 50 },
].map((bid, i) => (
<div key={i} className="relative flex justify-between text-[11px] py-1 px-1.5 rounded">
<div className="absolute inset-0 bg-green-500/10 rounded-sm" style={{ width: `${bid.depth}%` }} />
<span className="relative text-green-400 font-mono">{bid.price}</span>
<span className="relative text-zinc-500 font-mono">{bid.amount}</span>
<div className="absolute inset-0 bg-nofx-success/10 rounded-sm" style={{ width: `${bid.depth}%` }} />
<span className="relative text-nofx-success font-mono">{bid.price}</span>
<span className="relative text-nofx-text-muted font-mono">{bid.amount}</span>
</div>
))}
</div>
@@ -138,8 +133,8 @@ export default function AgentTerminal() {
{/* Active Positions */}
<div className="p-4">
<div className="flex items-center justify-between mb-3">
<span className="text-zinc-400 text-xs font-mono uppercase tracking-wider">Positions</span>
<span className="text-green-400 text-xs font-mono font-medium">+$12,847</span>
<span className="text-nofx-text text-xs font-mono uppercase tracking-wider">Positions</span>
<span className="text-nofx-success text-xs font-mono font-medium">+$12,847</span>
</div>
<div className="space-y-2">
{[
@@ -147,7 +142,7 @@ export default function AgentTerminal() {
{ coin: 'ETH', name: 'ETH-PERP', size: '3.2', profit: '+$4,127', percent: '+7.6%', color: '#627EEA' },
{ coin: 'BNB', name: 'BNB-PERP', size: '8.5', profit: '+$2,300', percent: '+5.2%', color: '#F3BA2F' },
].map((pos, i) => (
<div key={i} className="flex items-center justify-between py-2 px-2 rounded-lg bg-zinc-900/50 hover:bg-zinc-800/50 transition-colors">
<div key={i} className="flex items-center justify-between py-2 px-2 rounded-lg bg-nofx-bg-deeper hover:bg-nofx-bg transition-colors">
<div className="flex items-center gap-3">
<div
className="w-8 h-8 rounded-lg flex items-center justify-center text-xs font-bold border"
@@ -160,16 +155,16 @@ export default function AgentTerminal() {
{pos.coin}
</div>
<div>
<div className="text-white text-sm font-mono">{pos.name}</div>
<div className="text-nofx-text text-sm font-mono">{pos.name}</div>
<div className="flex items-center gap-2 text-[10px]">
<span className="text-green-400 bg-green-500/10 px-1.5 py-0.5 rounded font-mono">LONG</span>
<span className="text-zinc-500 font-mono">{pos.size} {pos.coin}</span>
<span className="text-nofx-success bg-nofx-success/10 px-1.5 py-0.5 rounded font-mono">LONG</span>
<span className="text-nofx-text-muted font-mono">{pos.size} {pos.coin}</span>
</div>
</div>
</div>
<div className="text-right">
<div className="text-green-400 font-mono font-medium">{pos.profit}</div>
<div className="text-green-500/70 text-[10px] font-mono">{pos.percent}</div>
<div className="text-nofx-success font-mono font-medium">{pos.profit}</div>
<div className="text-nofx-success/70 text-[10px] font-mono">{pos.percent}</div>
</div>
</div>
))}
@@ -177,15 +172,15 @@ export default function AgentTerminal() {
</div>
{/* Footer status bar */}
<div className="px-4 py-2 bg-[#0D1117] border-t border-zinc-800/60 flex items-center justify-between">
<div className="flex items-center gap-3 text-[10px] font-mono text-zinc-600">
<div className="px-4 py-2 bg-nofx-bg-deeper border-t border-[rgba(26,24,19,0.14)] flex items-center justify-between">
<div className="flex items-center gap-3 text-[10px] font-mono text-nofx-text-muted">
<span className="flex items-center gap-1">
<div className="w-1.5 h-1.5 bg-green-500 rounded-full" />
<div className="w-1.5 h-1.5 bg-nofx-success rounded-full" />
Connected
</span>
<span>Latency: 12ms</span>
</div>
<div className="text-[10px] font-mono text-zinc-600">
<div className="text-[10px] font-mono text-nofx-text-muted">
mainnet v2.4.0
</div>
</div>

View File

@@ -36,14 +36,14 @@ const features = [
export default function BrandFeatures() {
return (
<section id="features" className="py-24 bg-zinc-950 relative">
<section id="features" className="py-24 bg-nofx-bg relative">
<div className="max-w-[1920px] mx-auto px-6 lg:px-16">
<div className="mb-16 border-l-4 border-nofx-gold pl-6">
<h2 className="text-4xl md:text-5xl font-black text-white uppercase tracking-tighter mb-4">
Core Protocol <span className="text-zinc-600">Specs</span>
<h2 className="text-4xl md:text-5xl font-black text-nofx-text uppercase tracking-tighter mb-4">
Core Protocol <span className="text-nofx-text-muted">Specs</span>
</h2>
<p className="text-xl text-zinc-400 font-mono">
<p className="text-xl text-nofx-text-muted font-mono">
Next generation infrastructure for algorithmic dominance.
</p>
</div>
@@ -52,7 +52,7 @@ export default function BrandFeatures() {
{features.map((f, i) => (
<motion.div
key={i}
className="group relative bg-zinc-900 border border-zinc-800 p-8 hover:bg-zinc-800 transition-colors cursor-default overflow-hidden"
className="group relative bg-nofx-bg-lighter border border-[rgba(26,24,19,0.14)] p-8 hover:bg-nofx-bg-deeper transition-colors cursor-default overflow-hidden"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
@@ -64,11 +64,11 @@ export default function BrandFeatures() {
<f.icon className="w-10 h-10 text-nofx-gold mb-6" />
<h3 className="text-xl font-bold text-white mb-3 uppercase flex items-center gap-2">
<h3 className="text-xl font-bold text-nofx-text mb-3 uppercase flex items-center gap-2">
{f.title}
</h3>
<p className="text-zinc-400 leading-relaxed text-sm md:text-base">
<p className="text-nofx-text-muted leading-relaxed text-sm md:text-base">
{f.description}
</p>

View File

@@ -16,7 +16,7 @@ export default function BrandHero() {
<section className="relative w-full min-h-screen bg-nofx-bg text-nofx-text overflow-hidden flex flex-col pt-16">
{/* Top Marquee */}
<div className="w-full bg-nofx-gold text-black font-bold py-2 border-y border-black z-20">
<div className="w-full bg-nofx-gold text-nofx-bg-lighter font-bold py-2 border-y border-nofx-text/20 z-20">
<Marquee speed={40}>
<span className="mx-8 text-sm md:text-base uppercase tracking-widest">NOFX AI TRADING AUTOMATED WEALTH DECENTRALIZED INTELLIGENCE PUNK ETHOS </span>
<span className="mx-8 text-sm md:text-base uppercase tracking-widest">NOFX AI TRADING AUTOMATED WEALTH DECENTRALIZED INTELLIGENCE PUNK ETHOS </span>
@@ -37,17 +37,17 @@ export default function BrandHero() {
<span className="text-nofx-gold">EVOLVED</span>
</h1>
<p className="text-xl md:text-2xl text-zinc-400 max-w-xl mb-10 font-mono leading-relaxed">
<p className="text-xl md:text-2xl text-nofx-text-muted max-w-xl mb-10 font-mono leading-relaxed">
Autonomous trading agents. High-frequency execution.
<br />
Institutional-grade strategies for the
<span className="text-white font-bold ml-2 bg-nofx-accent px-2 py-0.5">DEGENERATES</span>.
<span className="text-nofx-bg-lighter font-bold ml-2 bg-nofx-accent px-2 py-0.5">DEGENERATES</span>.
</p>
<div className="flex flex-wrap gap-4">
<button
onClick={handleScroll}
className="bg-nofx-gold text-black text-lg font-black px-8 py-4 uppercase tracking-wider hover:bg-white hover:scale-105 transition-all flex items-center gap-2 clip-path-slant"
className="bg-nofx-gold text-nofx-bg-lighter text-lg font-black px-8 py-4 uppercase tracking-wider hover:bg-nofx-text hover:scale-105 transition-all flex items-center gap-2 clip-path-slant"
style={{ clipPath: 'polygon(0 0, 100% 0, 95% 100%, 0% 100%)' }}
>
Start Trading <ArrowRight className="w-6 h-6" />
@@ -57,15 +57,15 @@ export default function BrandHero() {
href={OFFICIAL_LINKS.github}
target="_blank"
rel="noreferrer"
className="border-2 border-white/20 text-white text-lg font-bold px-8 py-4 uppercase tracking-wider hover:bg-white/10 hover:border-white transition-all flex items-center gap-2"
className="border-2 border-[rgba(26,24,19,0.2)] text-nofx-text text-lg font-bold px-8 py-4 uppercase tracking-wider hover:bg-nofx-text/5 hover:border-nofx-text transition-all flex items-center gap-2"
>
<Github className="w-5 h-5" /> Source
</a>
</div>
<div className="mt-12 flex items-center gap-8 text-zinc-500 font-mono text-xs md:text-sm">
<div className="mt-12 flex items-center gap-8 text-nofx-text-muted font-mono text-xs md:text-sm">
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
<div className="w-2 h-2 bg-nofx-success rounded-full animate-pulse" />
SYSTEM ONLINE
</div>
<div className="flex items-center gap-2">
@@ -79,14 +79,14 @@ export default function BrandHero() {
{/* Right Visual - Trader Terminal */}
<div className="flex-1 relative overflow-visible flex items-center justify-center py-8 lg:py-0 min-h-[600px]">
{/* Background gradient orbs */}
<div className="absolute top-1/2 right-[15%] -translate-y-1/2 w-[450px] h-[450px] rounded-full bg-gradient-to-br from-nofx-gold/20 via-nofx-gold/5 to-transparent blur-[80px]" />
<div className="absolute top-[25%] right-[35%] w-[250px] h-[250px] rounded-full bg-nofx-accent/10 blur-[60px]" />
<div className="absolute top-1/2 right-[15%] -translate-y-1/2 w-[450px] h-[450px] rounded-full bg-nofx-gold/10 blur-[80px]" />
<div className="absolute top-[25%] right-[35%] w-[250px] h-[250px] rounded-full bg-nofx-accent/8 blur-[60px]" />
{/* Subtle dot grid */}
<div
className="absolute inset-0 opacity-[0.04]"
className="absolute inset-0 opacity-[0.05]"
style={{
backgroundImage: 'radial-gradient(circle at 1px 1px, rgba(255,255,255,0.4) 1px, transparent 0)',
backgroundImage: 'radial-gradient(circle at 1px 1px, rgba(26,24,19,0.4) 1px, transparent 0)',
backgroundSize: '32px 32px'
}}
/>

View File

@@ -27,19 +27,19 @@ export default function BrandStats() {
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
transition={{ delay: i * 0.1 }}
className="relative overflow-hidden group bg-black/40 backdrop-blur-md border border-white/10 p-6 rounded-lg md:bg-transparent md:border-0 md:p-0 md:backdrop-blur-none"
className="relative overflow-hidden group bg-white/10 backdrop-blur-md border border-white/20 p-6 rounded-lg md:bg-transparent md:border-0 md:p-0 md:backdrop-blur-none"
>
{/* Mobile Neon Corners */}
<div className="absolute top-0 right-0 w-3 h-3 border-t-2 border-r-2 border-nofx-gold md:hidden opacity-80 shadow-[0_0_10px_rgba(234,179,8,0.5)]"></div>
<div className="absolute bottom-0 left-0 w-3 h-3 border-b-2 border-l-2 border-nofx-gold md:hidden opacity-80 shadow-[0_0_10px_rgba(234,179,8,0.5)]"></div>
{/* Mobile Corners */}
<div className="absolute top-0 right-0 w-3 h-3 border-t-2 border-r-2 border-white md:hidden opacity-80"></div>
<div className="absolute bottom-0 left-0 w-3 h-3 border-b-2 border-l-2 border-white md:hidden opacity-80"></div>
{/* Mobile Inner Glow */}
<div className="absolute inset-0 bg-nofx-gold/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none md:hidden"></div>
<div className="absolute inset-0 bg-white/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none md:hidden"></div>
<div className="text-3xl md:text-6xl font-black text-white tracking-tighter mb-2 group-hover:scale-110 transition-transform duration-300 origin-left relative z-10">
{stat.value}
</div>
<div className="text-[10px] md:text-base font-bold text-zinc-400 md:text-black/60 uppercase tracking-widest bg-white/5 md:bg-white/20 inline-block px-2 py-1 rounded relative z-10">
<div className="text-[10px] md:text-base font-bold text-white/80 md:text-white/80 uppercase tracking-widest bg-white/10 md:bg-white/20 inline-block px-2 py-1 rounded relative z-10">
{stat.label}
</div>
</motion.div>

View File

@@ -15,7 +15,7 @@ const traderPresets = [
risk: 'HIGH',
color: 'text-nofx-gold',
border: 'border-nofx-gold/50',
bg_glow: 'shadow-[0_0_30px_rgba(240,185,11,0.1)]',
bg_glow: 'shadow-sm',
icon: Zap,
},
{
@@ -25,9 +25,9 @@ const traderPresets = [
apy: '89%',
winRate: '55%',
risk: 'MED',
color: 'text-blue-400',
border: 'border-blue-400/30',
bg_glow: 'shadow-[0_0_30px_rgba(96,165,250,0.1)]',
color: 'text-nofx-accent',
border: 'border-nofx-accent/30',
bg_glow: 'shadow-sm',
icon: TrendingUp,
},
{
@@ -37,9 +37,9 @@ const traderPresets = [
apy: '24%',
winRate: '99%',
risk: 'LOW',
color: 'text-purple-400',
border: 'border-purple-400/30',
bg_glow: 'shadow-[0_0_30px_rgba(192,132,252,0.1)]',
color: 'text-nofx-text',
border: 'border-nofx-gold/20',
bg_glow: 'shadow-sm',
icon: Layers,
},
]
@@ -62,8 +62,8 @@ export default function AgentGrid() {
className="py-16 md:py-24 bg-nofx-bg relative overflow-hidden"
>
{/* Background Details */}
<div className="absolute top-0 right-0 p-10 opacity-20 pointer-events-none">
<Hexagon className="w-64 h-64 text-zinc-800" strokeWidth={0.5} />
<div className="absolute top-0 right-0 p-10 opacity-10 pointer-events-none">
<Hexagon className="w-64 h-64 text-nofx-text-muted" strokeWidth={0.5} />
</div>
<div className="max-w-7xl mx-auto px-6 relative z-10">
@@ -72,14 +72,14 @@ export default function AgentGrid() {
<div className="flex items-center gap-2 text-nofx-gold font-mono text-xs mb-2 tracking-widest uppercase">
<Crosshair className="w-4 h-4" /> ASSET CLASS SELECT
</div>
<h2 className="text-4xl md:text-5xl font-black text-white uppercase tracking-tighter">
<h2 className="text-4xl md:text-5xl font-black text-nofx-text uppercase tracking-tighter">
PROFESSIONAL{' '}
<span className="text-transparent bg-clip-text bg-gradient-to-r from-nofx-gold to-white">
<span className="text-nofx-gold">
TRADERS
</span>
</h2>
</div>
<div className="font-mono text-right text-xs text-zinc-500 max-w-xs">
<div className="font-mono text-right text-xs text-nofx-text-muted max-w-xs">
CREATE TRADERS FOR US STOCKS, COMMODITIES, FX AND PRE-IPO MARKETS.
DESCRIBE THE STRATEGY IN ONE SENTENCE.
</div>
@@ -96,19 +96,19 @@ export default function AgentGrid() {
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.1 }}
className={`group relative bg-black/40 backdrop-blur-xl border ${preset.border} overflow-hidden transition-all duration-300 min-w-[85vw] md:min-w-0 snap-center shrink-0 rounded-xl md:rounded-none`}
className={`group relative bg-nofx-bg-lighter backdrop-blur-xl border ${preset.border} overflow-hidden transition-all duration-300 min-w-[85vw] md:min-w-0 snap-center shrink-0 rounded-xl md:rounded-none`}
>
{/* Top "Hinge" decoration */}
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-white/10 to-transparent"></div>
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-nofx-text/10 to-transparent"></div>
<div className="p-8 relative z-10">
{/* Header */}
<div className="flex justify-between items-start mb-6">
<div className="p-3 bg-zinc-900/80 rounded border border-zinc-700">
<div className="p-3 bg-nofx-bg-deeper rounded border border-[rgba(26,24,19,0.14)]">
<Icon className={`w-8 h-8 ${preset.color}`} />
</div>
<div className="text-right">
<div className="text-[10px] font-mono text-zinc-500 uppercase">
<div className="text-[10px] font-mono text-nofx-text-muted uppercase">
Class
</div>
<div
@@ -120,33 +120,33 @@ export default function AgentGrid() {
</div>
{/* Name & Desc */}
<h3 className="text-3xl font-bold text-white mb-2 tracking-tight group-hover:text-nofx-accent transition-colors">
<h3 className="text-3xl font-bold text-nofx-text mb-2 tracking-tight group-hover:text-nofx-accent transition-colors">
{preset.name}
</h3>
<p className="text-zinc-500 text-sm mb-8 leading-relaxed h-10">
<p className="text-nofx-text-muted text-sm mb-8 leading-relaxed h-10">
{preset.desc}
</p>
{/* Stats Grid */}
<div className="grid grid-cols-3 gap-px bg-zinc-800/50 border border-zinc-800 rounded overflow-hidden mb-8">
<div className="bg-black/60 p-3 text-center group-hover:bg-zinc-900/60 transition-colors">
<div className="text-[10px] text-zinc-500 uppercase font-mono mb-1">
<div className="grid grid-cols-3 gap-px bg-[rgba(26,24,19,0.14)] border border-[rgba(26,24,19,0.14)] rounded overflow-hidden mb-8">
<div className="bg-nofx-bg-deeper p-3 text-center group-hover:bg-nofx-bg transition-colors">
<div className="text-[10px] text-nofx-text-muted uppercase font-mono mb-1">
APY
</div>
<div className="text-green-400 font-bold">
<div className="text-nofx-success font-bold">
{preset.apy}
</div>
</div>
<div className="bg-black/60 p-3 text-center group-hover:bg-zinc-900/60 transition-colors">
<div className="text-[10px] text-zinc-500 uppercase font-mono mb-1">
<div className="bg-nofx-bg-deeper p-3 text-center group-hover:bg-nofx-bg transition-colors">
<div className="text-[10px] text-nofx-text-muted uppercase font-mono mb-1">
Win %
</div>
<div className="text-white font-bold">
<div className="text-nofx-text font-bold">
{preset.winRate}
</div>
</div>
<div className="bg-black/60 p-3 text-center group-hover:bg-zinc-900/60 transition-colors">
<div className="text-[10px] text-zinc-500 uppercase font-mono mb-1">
<div className="bg-nofx-bg-deeper p-3 text-center group-hover:bg-nofx-bg transition-colors">
<div className="text-[10px] text-nofx-text-muted uppercase font-mono mb-1">
Risk
</div>
<div className={`${preset.color} font-bold`}>
@@ -158,7 +158,7 @@ export default function AgentGrid() {
{/* Action Btn */}
<button
onClick={handleInitialize}
className={`w-full py-4 text-xs font-bold font-mono uppercase tracking-[0.2em] border border-zinc-700 hover:border-${preset.color === 'text-nofx-gold' ? 'nofx-gold' : 'white'} hover:bg-white/5 transition-all flex items-center justify-center gap-2 group-hover:text-white cursor-pointer`}
className={`w-full py-4 text-xs font-bold font-mono uppercase tracking-[0.2em] border border-[rgba(26,24,19,0.14)] hover:border-${preset.color === 'text-nofx-gold' ? 'nofx-gold' : 'nofx-text'} hover:bg-nofx-text/5 transition-all flex items-center justify-center gap-2 group-hover:text-nofx-text cursor-pointer text-nofx-text`}
>
<span className={preset.color}>[</span> INITIALIZE{' '}
<span className={preset.color}>]</span>
@@ -166,8 +166,7 @@ export default function AgentGrid() {
</div>
{/* Decorative Background Elements */}
<div className="absolute -right-10 -bottom-10 w-40 h-40 bg-gradient-to-br from-white/5 to-transparent rounded-full blur-2xl group-hover:opacity-50 transition-opacity opacity-20"></div>
<div className="absolute inset-0 bg-scanlines opacity-20 pointer-events-none"></div>
<div className="absolute -right-10 -bottom-10 w-40 h-40 bg-gradient-to-br from-nofx-text/5 to-transparent rounded-full blur-2xl group-hover:opacity-50 transition-opacity opacity-20"></div>
</motion.div>
)
})}

View File

@@ -13,9 +13,9 @@ export default function DeploymentHub() {
}
return (
<section className="py-24 bg-black relative overflow-hidden border-t border-zinc-800">
<section className="py-24 bg-nofx-bg relative overflow-hidden border-t border-[rgba(26,24,19,0.14)]">
{/* Background Grids */}
<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]"></div>
<div className="absolute inset-0 bg-[linear-gradient(to_right,#1a181310_1px,transparent_1px),linear-gradient(to_bottom,#1a181310_1px,transparent_1px)] bg-[size:24px_24px]"></div>
<div className="max-w-7xl mx-auto px-6 relative z-10">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
@@ -26,11 +26,11 @@ export default function DeploymentHub() {
<Server className="w-4 h-4" /> System Deployment
</div>
<h2 className="text-4xl md:text-6xl font-black text-white leading-tight">
DEPLOY <span className="text-transparent bg-clip-text bg-gradient-to-r from-nofx-gold to-white">INSTANTLY</span>
<h2 className="text-4xl md:text-6xl font-black text-nofx-text leading-tight">
DEPLOY <span className="text-nofx-gold">INSTANTLY</span>
</h2>
<p className="text-zinc-400 text-lg leading-relaxed font-light">
<p className="text-nofx-text-muted text-lg leading-relaxed font-light">
Initialize your own high-frequency trading node in seconds.
Our optimized installer handles all dependencies, bringing the trading system online with a single command.
</p>
@@ -40,13 +40,13 @@ export default function DeploymentHub() {
{ icon: Command, label: "One-Line Install", desc: "No configuration needed" },
{ icon: Shield, label: "Secure Core", desc: "Sandboxed execution env" }
].map((item, i) => (
<div key={i} className="flex gap-4 items-start p-4 rounded bg-zinc-900/50 border border-zinc-800 hover:border-nofx-gold/30 transition-colors group">
<div className="p-2 rounded bg-black border border-zinc-800 text-nofx-gold group-hover:bg-nofx-gold/10 transition-colors">
<div key={i} className="flex gap-4 items-start p-4 rounded bg-nofx-bg-lighter border border-[rgba(26,24,19,0.14)] hover:border-nofx-gold/30 transition-colors group">
<div className="p-2 rounded bg-nofx-bg-deeper border border-[rgba(26,24,19,0.14)] text-nofx-gold group-hover:bg-nofx-gold/10 transition-colors">
<item.icon className="w-5 h-5" />
</div>
<div>
<h4 className="text-white font-bold font-mono text-sm mb-1">{item.label}</h4>
<p className="text-zinc-500 text-xs">{item.desc}</p>
<h4 className="text-nofx-text font-bold font-mono text-sm mb-1">{item.label}</h4>
<p className="text-nofx-text-muted text-xs">{item.desc}</p>
</div>
</div>
))}
@@ -61,31 +61,31 @@ export default function DeploymentHub() {
className="relative"
>
{/* Glow effect */}
<div className="absolute -inset-1 bg-gradient-to-r from-nofx-gold/20 to-blue-500/20 rounded-xl blur-xl opacity-50"></div>
<div className="absolute -inset-1 bg-nofx-gold/10 rounded-xl blur-xl opacity-50"></div>
<div className="relative rounded-xl overflow-hidden bg-[#0a0a0a] border border-zinc-800 shadow-2xl">
<div className="relative rounded-xl overflow-hidden bg-nofx-bg-lighter border border-[rgba(26,24,19,0.14)] shadow-lg">
{/* Terminal Header */}
<div className="flex items-center justify-between px-4 py-3 bg-zinc-900/80 border-b border-zinc-800">
<div className="flex items-center justify-between px-4 py-3 bg-nofx-bg-deeper border-b border-[rgba(26,24,19,0.14)]">
<div className="flex gap-2">
<div className="w-3 h-3 rounded-full bg-red-500/80"></div>
<div className="w-3 h-3 rounded-full bg-yellow-500/80"></div>
<div className="w-3 h-3 rounded-full bg-green-500/80"></div>
<div className="w-3 h-3 rounded-full bg-nofx-danger/80"></div>
<div className="w-3 h-3 rounded-full bg-nofx-gold/80"></div>
<div className="w-3 h-3 rounded-full bg-nofx-success/80"></div>
</div>
<div className="text-[10px] font-mono text-zinc-500 flex items-center gap-1.5">
<div className="text-[10px] font-mono text-nofx-text-muted flex items-center gap-1.5">
<Terminal className="w-3 h-3" />
root@nofx-os:~
</div>
</div>
{/* Terminal Content */}
<div className="p-8 font-mono text-sm md:text-base bg-black/50 backdrop-blur-sm min-h-[200px] flex flex-col justify-center">
<div className="mb-2 text-zinc-500 text-xs tracking-wide"># Initialize NoFX Core Protocol</div>
<div className="p-8 font-mono text-sm md:text-base bg-nofx-bg-lighter min-h-[200px] flex flex-col justify-center">
<div className="mb-2 text-nofx-text-muted text-xs tracking-wide"># Initialize NoFX Core Protocol</div>
<div
className="group relative flex items-start gap-3 p-4 rounded-lg bg-zinc-900/50 border border-zinc-800 hover:border-nofx-gold/50 cursor-pointer transition-all hover:bg-zinc-900/80"
className="group relative flex items-start gap-3 p-4 rounded-lg bg-nofx-bg-deeper border border-[rgba(26,24,19,0.14)] hover:border-nofx-gold/50 cursor-pointer transition-all hover:bg-nofx-bg"
onClick={handleCopy}
>
<span className="text-nofx-gold mt-1"><ChevronRight className="w-4 h-4" /></span>
<code className="text-zinc-100 flex-1 break-all">
<code className="text-nofx-text flex-1 break-all">
{installCmd}
</code>
@@ -96,12 +96,12 @@ export default function DeploymentHub() {
initial={{ scale: 0.5, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.5, opacity: 0 }}
className="flex items-center gap-1 text-green-400 bg-green-400/10 px-2 py-1 rounded text-xs font-bold"
className="flex items-center gap-1 text-nofx-success bg-nofx-success/10 px-2 py-1 rounded text-xs font-bold"
>
<Check className="w-3 h-3" />
</motion.div>
) : (
<div className="text-zinc-400 bg-zinc-800 p-1.5 rounded hover:text-white hover:bg-zinc-700">
<div className="text-nofx-text-muted bg-nofx-bg-deeper p-1.5 rounded hover:text-nofx-text hover:bg-nofx-bg">
<Copy className="w-4 h-4" />
</div>
)}

View File

@@ -21,7 +21,7 @@ const generateLog = (id: number): LogEntry => {
switch (type) {
case 'EXEC':
msg = `AGENT-${Math.floor(Math.random() * 99)} ${actions[Math.floor(Math.random() * 4)]} ${pairs[Math.floor(Math.random() * pairs.length)]} @ ${Math.floor(Math.random() * 600)}`
color = 'text-green-500'
color = 'text-nofx-success'
break;
case 'SIGNAL':
msg = `US equities momentum signal confirmed (${(Math.random()).toFixed(3)} z-score)`
@@ -29,15 +29,15 @@ const generateLog = (id: number): LogEntry => {
break;
case 'RISK':
msg = `Risk check passed: ${pairs[Math.floor(Math.random() * pairs.length)]} exposure within limits`
color = 'text-red-500'
color = 'text-nofx-danger'
break;
case 'MACRO':
msg = `Macro feed latency < ${Math.floor(Math.random() * 10)}ms`
color = 'text-zinc-500'
color = 'text-nofx-text-muted'
break;
default:
msg = `System optimization cycle complete. Allocating resources.`
color = 'text-blue-400'
color = 'text-nofx-accent'
}
return { id, time: new Date().toLocaleTimeString('en-US', { hour12: false }) + '.' + Math.floor(Math.random() * 999), type, msg, color }
@@ -62,16 +62,15 @@ export default function LiveFeed() {
}, [])
return (
<section className="w-full bg-[#020304] border-y border-zinc-800 py-1 overflow-hidden relative">
<div className="absolute inset-0 bg-scanlines opacity-10 pointer-events-none"></div>
<section className="w-full bg-nofx-bg-lighter border-y border-[rgba(26,24,19,0.14)] py-1 overflow-hidden relative">
<div className="max-w-[1920px] mx-auto px-4 flex flex-col md:flex-row gap-0 md:gap-8 items-stretch h-[240px] md:h-12 text-xs font-mono">
{/* Left Status Bar (Static) */}
<div className="hidden md:flex items-center gap-6 text-zinc-600 border-r border-zinc-900 pr-6 shrink-0">
<div className="hidden md:flex items-center gap-6 text-nofx-text-muted border-r border-[rgba(26,24,19,0.14)] pr-6 shrink-0">
<div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse"></div>
<span className="font-bold text-zinc-400">WS_CONN: STABLE</span>
<div className="w-1.5 h-1.5 bg-nofx-success rounded-full animate-pulse"></div>
<span className="font-bold text-nofx-text">WS_CONN: STABLE</span>
</div>
<div className="flex items-center gap-2">
<span className="text-nofx-gold">TPS: 48,291</span>
@@ -90,10 +89,10 @@ export default function LiveFeed() {
animate={{ opacity: 1, x: 0 }}
className="absolute inset-0 flex items-center gap-4"
>
<span className="text-zinc-600">[{log.time}]</span>
<span className={`font-bold w-10 ${log.type === 'RISK' ? 'text-blue-400 bg-blue-500/10 px-1 rounded' :
<span className="text-nofx-text-muted">[{log.time}]</span>
<span className={`font-bold w-10 ${log.type === 'RISK' ? 'text-nofx-danger bg-nofx-danger/10 px-1 rounded' :
log.type === 'SIGNAL' ? 'text-nofx-gold bg-nofx-gold/10 px-1 rounded' :
log.type === 'EXEC' ? 'text-green-500' : 'text-zinc-500'
log.type === 'EXEC' ? 'text-nofx-success' : 'text-nofx-text-muted'
}`}>{log.type}</span>
<span className={`${log.color}`}>{log.msg}</span>
</motion.div>
@@ -103,11 +102,11 @@ export default function LiveFeed() {
{/* Mobile View: Vertical Stack */}
<div className="md:hidden flex flex-col gap-2 w-full p-4 h-full overflow-hidden">
{logs.map((log) => (
<div key={log.id} className="flex gap-2 w-full truncate border-b border-zinc-900/50 pb-1 last:border-0">
<span className="text-zinc-700 w-16 shrink-0">{log.time.split('.')[0]}</span>
<span className={`font-bold w-8 shrink-0 ${log.type === 'RISK' ? 'text-blue-400' :
<div key={log.id} className="flex gap-2 w-full truncate border-b border-[rgba(26,24,19,0.10)] pb-1 last:border-0">
<span className="text-nofx-text-muted w-16 shrink-0">{log.time.split('.')[0]}</span>
<span className={`font-bold w-8 shrink-0 ${log.type === 'RISK' ? 'text-nofx-danger' :
log.type === 'SIGNAL' ? 'text-nofx-gold' :
'text-zinc-500'
'text-nofx-text-muted'
}`}>{log.type}</span>
<span className={`${log.color} truncate`}>{log.msg}</span>
</div>

View File

@@ -83,8 +83,8 @@ export default function TerminalHero() {
{/* BACKGROUND LAYERS */}
{/* 1. Grid */}
<div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-20 mix-blend-soft-light pointer-events-none"></div>
<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%)] pointer-events-none md:hidden" style={{ transform: 'perspective(500px) rotateX(60deg) translateY(100px) scale(2)' }}></div>
<div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-10 mix-blend-multiply pointer-events-none"></div>
<div className="absolute inset-x-0 bottom-0 h-[50vh] bg-[linear-gradient(to_right,#1a181310_1px,transparent_1px),linear-gradient(to_bottom,#1a181310_1px,transparent_1px)] bg-[size:40px_40px] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,#000_70%,transparent_100%)] pointer-events-none md:hidden" style={{ transform: 'perspective(500px) rotateX(60deg) translateY(100px) scale(2)' }}></div>
<div className="absolute inset-0 bg-grid-pattern opacity-[0.03] pointer-events-none"></div>
{/* 2. World Map / Data Viz Background (Abstract) */}
@@ -94,8 +94,8 @@ export default function TerminalHero() {
</div>
{/* 3. Gradient Spots - Intensified for Mobile */}
<div className="absolute top-[-10%] left-[-10%] w-[40vw] h-[40vw] bg-nofx-gold/20 rounded-full blur-[120px] pointer-events-none mix-blend-screen"></div>
<div className="absolute bottom-[-10%] right-[-10%] w-[40vw] h-[40vw] bg-nofx-accent/10 rounded-full blur-[120px] pointer-events-none mix-blend-screen"></div>
<div className="absolute top-[-10%] left-[-10%] w-[40vw] h-[40vw] bg-nofx-gold/10 rounded-full blur-[120px] pointer-events-none"></div>
<div className="absolute bottom-[-10%] right-[-10%] w-[40vw] h-[40vw] bg-nofx-accent/10 rounded-full blur-[120px] pointer-events-none"></div>
{/* Mobile Bottom Fade */}
<div className="absolute bottom-0 left-0 w-full h-32 bg-gradient-to-t from-nofx-bg to-transparent z-20 pointer-events-none md:hidden" />
@@ -111,20 +111,20 @@ export default function TerminalHero() {
<div className="relative z-10 flex-1 grid grid-cols-1 lg:grid-cols-12 gap-0 lg:gap-8 max-w-[1800px] mx-auto w-full px-6 h-full pb-20 pt-10 pointer-events-none">
{/* LEFT COLUMN: TELEMETRY & STATUS */}
<div className="hidden lg:flex col-span-3 flex-col justify-between h-full border-r border-white/5 pr-8 py-10 pointer-events-auto">
<div className="hidden lg:flex col-span-3 flex-col justify-between h-full border-r border-[rgba(26,24,19,0.14)] pr-8 py-10 pointer-events-auto">
{/* Top: System Health */}
<div className="space-y-6">
<div className="tech-border p-4 bg-black/40 backdrop-blur-sm">
<div className="border border-[rgba(26,24,19,0.14)] rounded p-4 bg-nofx-bg-lighter">
<h3 className="text-xs font-mono text-nofx-gold mb-4 flex items-center gap-2">
<Activity className="w-3 h-3" /> SYSTEM_DIAGNOSTICS
</h3>
<div className="space-y-3 font-mono text-[10px] text-zinc-400">
<div className="space-y-3 font-mono text-[10px] text-nofx-text-muted">
<div className="flex justify-between items-center">
<span>KERNEL_LATENCY</span>
<span className="text-nofx-accent">12ms</span>
</div>
<div className="w-full h-1 bg-zinc-800 rounded-full overflow-hidden">
<div className="w-full h-1 bg-nofx-bg-deeper rounded-full overflow-hidden">
<div className="w-[90%] h-full bg-nofx-accent/50"></div>
</div>
@@ -132,19 +132,19 @@ export default function TerminalHero() {
<span>MEMORY_INTEGRITY</span>
<span className="text-nofx-success">100%</span>
</div>
<div className="w-full h-1 bg-zinc-800 rounded-full overflow-hidden">
<div className="w-full h-1 bg-nofx-bg-deeper rounded-full overflow-hidden">
<div className="w-full h-full bg-nofx-success/50"></div>
</div>
<div className="flex justify-between items-center">
<span>UPTIME</span>
<span className="text-white">99.999%</span>
<span className="text-nofx-text">99.999%</span>
</div>
</div>
</div>
<div className="p-4 border border-zinc-800/50 rounded bg-zinc-900/20">
<div className="flex items-center gap-3 text-zinc-500 mb-2">
<div className="p-4 border border-[rgba(26,24,19,0.14)] rounded bg-nofx-bg-lighter">
<div className="flex items-center gap-3 text-nofx-text-muted mb-2">
<Shield className="w-4 h-4" />
<span className="text-[10px] font-mono tracking-widest">SECURITY PROTOCOLS</span>
</div>
@@ -152,14 +152,14 @@ export default function TerminalHero() {
<div className="h-1 flex-1 bg-nofx-gold"></div>
<div className="h-1 flex-1 bg-nofx-gold"></div>
<div className="h-1 flex-1 bg-nofx-gold"></div>
<div className="h-1 flex-1 bg-zinc-800"></div>
<div className="h-1 flex-1 bg-nofx-bg-deeper"></div>
</div>
<div className="mt-2 text-right text-[10px] text-nofx-gold/80 font-mono">LEVEL 3 ACTIVATE</div>
</div>
</div>
{/* Bottom: Network Log */}
<div className="font-mono text-[10px] text-zinc-600 space-y-1 opacity-70">
<div className="font-mono text-[10px] text-nofx-text-muted space-y-1 opacity-70">
<div>&gt; CONNECTING TO MARKET DATA... OK</div>
<div>&gt; SYNCING VENUES (424/424)... OK</div>
<div>&gt; LOADING MULTI-ASSET UNIVERSE... DONE</div>
@@ -185,13 +185,13 @@ export default function TerminalHero() {
{/* Main Title - Massive & Impactful */}
{/* Main Title - Massive & Impactful */}
<div className="relative z-20 mix-blend-hard-light md:mix-blend-normal">
<h1 className="text-5xl sm:text-6xl md:text-8xl lg:text-9xl font-black tracking-tighter leading-[0.9] md:leading-[0.8] mb-6 select-none bg-clip-text text-transparent bg-gradient-to-b from-white via-white to-zinc-600 drop-shadow-2xl">
<div className="relative z-20">
<h1 className="text-5xl sm:text-6xl md:text-8xl lg:text-9xl font-black tracking-tighter leading-[0.9] md:leading-[0.8] mb-6 select-none text-nofx-text">
AGENTIC<br />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-nofx-gold via-white to-nofx-gold animate-shimmer bg-[length:200%_auto] tracking-tight filter drop-shadow-[0_0_15px_rgba(234,179,8,0.3)]">TRADING</span>
<span className="text-nofx-gold animate-shimmer tracking-tight">TRADING</span>
</h1>
<p className="max-w-xl text-zinc-200 md:text-zinc-400 text-lg mb-6 font-light leading-relaxed drop-shadow-md">
<p className="max-w-xl text-nofx-text-muted text-lg mb-6 font-light leading-relaxed">
Professional AI trading agents for US stocks, commodities, FX and Pre-IPO synthetic markets.
Build institutional-grade strategies by chatting in plain English.
</p>
@@ -210,10 +210,10 @@ export default function TerminalHero() {
<div className="flex flex-wrap gap-4 font-mono">
{['US STOCKS', 'COMMODITIES', 'FOREX', 'PRE-IPO'].map((market) => (
<div key={market} className="relative group cursor-default">
<div className="absolute -inset-0.5 bg-gradient-to-r from-nofx-gold/20 to-blue-600/20 rounded-lg blur opacity-0 group-hover:opacity-100 transition duration-500"></div>
<div className="relative flex items-center gap-3 px-6 py-3 rounded-lg bg-zinc-900/80 border border-zinc-700 hover:border-nofx-gold/50 transition-all duration-300 backdrop-blur-sm">
<div className="w-1.5 h-1.5 rounded-full bg-nofx-success shadow-[0_0_8px_rgba(74,222,128,0.6)] animate-pulse"></div>
<span className="text-lg md:text-xl font-bold text-white tracking-wider group-hover:text-nofx-gold transition-colors">{market}</span>
<div className="absolute -inset-0.5 bg-nofx-gold/15 rounded-lg blur opacity-0 group-hover:opacity-100 transition duration-500"></div>
<div className="relative flex items-center gap-3 px-6 py-3 rounded-lg bg-nofx-bg-lighter border border-[rgba(26,24,19,0.14)] hover:border-nofx-gold/50 transition-all duration-300">
<div className="w-1.5 h-1.5 rounded-full bg-nofx-success animate-pulse"></div>
<span className="text-lg md:text-xl font-bold text-nofx-text tracking-wider group-hover:text-nofx-gold transition-colors">{market}</span>
</div>
</div>
))}
@@ -221,10 +221,10 @@ export default function TerminalHero() {
</div>
{/* Command Line Input Simulation */}
<div className="w-full max-w-lg h-12 bg-black/50 border border-zinc-800 rounded flex items-center px-4 mb-10 font-mono text-sm shadow-2xl backdrop-blur-sm group hover:border-nofx-gold/50 transition-colors cursor-text" onClick={() => document.getElementById('market-scanner')?.scrollIntoView({ behavior: 'smooth' })}>
<div className="w-full max-w-lg h-12 bg-nofx-bg-lighter border border-[rgba(26,24,19,0.14)] rounded flex items-center px-4 mb-10 font-mono text-sm shadow-sm group hover:border-nofx-gold/50 transition-colors cursor-text" onClick={() => document.getElementById('market-scanner')?.scrollIntoView({ behavior: 'smooth' })}>
<span className="text-nofx-success mr-2"></span>
<span className="text-nofx-accent mr-2">~</span>
<span className="text-zinc-500">create US stock trader --idea="breakouts"</span>
<span className="text-nofx-text-muted">create US stock trader --idea="breakouts"</span>
<span className="w-2 h-4 bg-nofx-gold ml-1 animate-pulse"></span>
</div>
@@ -232,13 +232,13 @@ export default function TerminalHero() {
<div className="flex flex-col sm:flex-row gap-4 w-full justify-center">
<button
onClick={() => document.getElementById('market-scanner')?.scrollIntoView({ behavior: 'smooth' })}
className="group relative overflow-hidden bg-nofx-gold text-black px-8 py-4 font-bold font-mono tracking-wider hover:scale-105 transition-transform duration-200"
className="group relative overflow-hidden bg-nofx-gold text-nofx-bg-lighter px-8 py-4 font-bold font-mono tracking-wider hover:scale-105 transition-transform duration-200"
style={{ clipPath: 'polygon(10% 0, 100% 0, 100% 70%, 90% 100%, 0 100%, 0 30%)' }}
>
<span className="relative z-10 flex items-center gap-2">
CREATE STOCK TRADER <ArrowRight className="w-4 h-4" />
</span>
<div className="absolute inset-0 bg-white/20 translate-y-full group-hover:translate-y-0 transition-transform duration-300"></div>
<div className="absolute inset-0 bg-nofx-text/10 translate-y-full group-hover:translate-y-0 transition-transform duration-300"></div>
</button>
</div>
@@ -251,13 +251,13 @@ export default function TerminalHero() {
{/* RIGHT COLUMN: Trader Terminal - Desktop Only */}
<div className="absolute top-0 right-0 h-full w-[50vw] hidden lg:flex flex-col items-end justify-end pr-8 pb-20 z-10">
{/* Subtle gradient orb */}
<div className="absolute top-1/2 right-[10%] -translate-y-1/2 w-[400px] h-[400px] rounded-full bg-gradient-to-br from-nofx-gold/10 via-nofx-gold/5 to-transparent blur-[100px] pointer-events-none"></div>
<div className="absolute top-1/2 right-[10%] -translate-y-1/2 w-[400px] h-[400px] rounded-full bg-nofx-gold/8 blur-[100px] pointer-events-none"></div>
{/* Subtle grid fade */}
<div
className="absolute inset-0 opacity-[0.03] pointer-events-none"
style={{
backgroundImage: 'radial-gradient(circle at 1px 1px, rgba(255,255,255,0.3) 1px, transparent 0)',
backgroundImage: 'radial-gradient(circle at 1px 1px, rgba(26,24,19,0.3) 1px, transparent 0)',
backgroundSize: '40px 40px',
maskImage: 'radial-gradient(ellipse 80% 80% at 70% 50%, black 20%, transparent 70%)',
WebkitMaskImage: 'radial-gradient(ellipse 80% 80% at 70% 50%, black 20%, transparent 70%)'
@@ -271,11 +271,11 @@ export default function TerminalHero() {
</div>
{/* FLOATING TICKER FOOTER */}
<div className="absolute bottom-0 w-full bg-black/80 border-t border-zinc-800/50 backdrop-blur-md z-30 overflow-hidden py-2 flex items-center">
<div className="flex animate-marquee whitespace-nowrap gap-12 text-xs font-mono text-zinc-500 px-4">
<span className="flex items-center gap-2"><Globe className="w-3 h-3 text-zinc-600" /> GLOBAL MARKET ACCESS</span>
<div className="absolute bottom-0 w-full bg-nofx-bg-lighter border-t border-[rgba(26,24,19,0.14)] backdrop-blur-md z-30 overflow-hidden py-2 flex items-center">
<div className="flex animate-marquee whitespace-nowrap gap-12 text-xs font-mono text-nofx-text-muted px-4">
<span className="flex items-center gap-2"><Globe className="w-3 h-3 text-nofx-text-muted" /> GLOBAL MARKET ACCESS</span>
<span className="flex items-center gap-2 text-nofx-gold"><Zap className="w-3 h-3" /> MULTI-ASSET ROUTING ENABLED</span>
<span className="flex items-center gap-2"><Wifi className="w-3 h-3 text-green-500" /> LOW LATENCY LINK: 12ms</span>
<span className="flex items-center gap-2"><Wifi className="w-3 h-3 text-nofx-success" /> LOW LATENCY LINK: 12ms</span>
{/* Dynamic Coins */}
{Object.entries(prices).map(([symbol, price]) => (
@@ -295,8 +295,6 @@ export default function TerminalHero() {
</div>
</div>
{/* CRT OVERLAY (Global) */}
<div className="absolute inset-0 crt-overlay pointer-events-none z-50 opacity-40"></div>
</section >
)
}
@@ -311,28 +309,28 @@ function CommunityStats() {
label: 'GITHUB STARS',
value: isLoading ? '...' : (error ? '10,500+' : stars.toLocaleString()),
icon: Star,
color: 'text-yellow-400',
color: 'text-nofx-gold',
href: OFFICIAL_LINKS.github
},
{
label: 'FORKS',
value: isLoading ? '...' : (error ? '2,800+' : forks.toLocaleString()),
icon: GitFork,
color: 'text-blue-400',
color: 'text-nofx-accent',
href: `${OFFICIAL_LINKS.github}/fork`
},
{
label: 'CONTRIBUTORS',
value: isLoading ? '...' : (contributors > 0 ? contributors : '50+'),
icon: Users,
color: 'text-green-400',
color: 'text-nofx-success',
href: `${OFFICIAL_LINKS.github}/graphs/contributors`
},
{
label: 'DEV COMMUNITY',
value: '6,600+',
icon: MessageCircle,
color: 'text-blue-500',
color: 'text-nofx-accent',
href: OFFICIAL_LINKS.telegram
}
]
@@ -345,13 +343,13 @@ function CommunityStats() {
href={stat.href}
target="_blank"
rel="noopener noreferrer"
className="flex flex-col items-center justify-center p-3 rounded bg-black/40 border border-zinc-800/50 backdrop-blur-sm group hover:border-nofx-gold/30 transition-all cursor-pointer hover:bg-white/5"
className="flex flex-col items-center justify-center p-3 rounded bg-nofx-bg-lighter border border-[rgba(26,24,19,0.14)] group hover:border-nofx-gold/30 transition-all cursor-pointer hover:bg-nofx-bg-deeper"
>
<div className="flex items-center gap-2 mb-1">
<stat.icon className={`w-4 h-4 ${stat.color}`} />
<span className="text-[10px] font-mono text-zinc-500 tracking-wider">{stat.label}</span>
<span className="text-[10px] font-mono text-nofx-text-muted tracking-wider">{stat.label}</span>
</div>
<span className="text-xl font-bold font-mono text-white group-hover:text-nofx-gold transition-colors">{stat.value}</span>
<span className="text-xl font-bold font-mono text-nofx-text group-hover:text-nofx-gold transition-colors">{stat.value}</span>
</a>
))}
</div>

View File

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

View File

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

View File

@@ -50,29 +50,29 @@ export function GridRiskPanel({
const getRegimeColor = (regime: string) => {
switch (regime) {
case 'narrow': return '#0ECB81'
case 'standard': return '#F0B90B'
case 'wide': return '#F7931A'
case 'volatile': return '#F6465D'
case 'trending': return '#8B5CF6'
default: return '#848E9C'
case 'narrow': return '#2E8B57'
case 'standard': return '#E0483B'
case 'wide': return '#E0483B'
case 'volatile': return '#D6433A'
case 'trending': return '#E0483B'
default: return '#8A8478'
}
}
const getBreakoutColor = (level: string) => {
switch (level) {
case 'none': return '#0ECB81'
case 'short': return '#F0B90B'
case 'mid': return '#F7931A'
case 'long': return '#F6465D'
default: return '#848E9C'
case 'none': return '#2E8B57'
case 'short': return '#E0483B'
case 'mid': return '#E0483B'
case 'long': return '#D6433A'
default: return '#8A8478'
}
}
const getPositionColor = (percent: number) => {
if (percent < 50) return '#0ECB81'
if (percent < 80) return '#F0B90B'
return '#F6465D'
if (percent < 50) return '#2E8B57'
if (percent < 80) return '#E0483B'
return '#D6433A'
}
const formatPrice = (price: number) => {
@@ -87,13 +87,13 @@ export function GridRiskPanel({
}
const cardStyle = {
background: '#0B0E11',
border: '1px solid #2B3139',
background: '#F7F4EC',
border: '1px solid rgba(26,24,19,0.14)',
}
if (loading) {
return (
<div className="p-3 text-center text-xs" style={{ color: '#848E9C' }}>
<div className="p-3 text-center text-xs" style={{ color: '#8A8478' }}>
{ts(gridRisk.loading, language)}
</div>
)
@@ -101,7 +101,7 @@ export function GridRiskPanel({
if (error) {
return (
<div className="p-3 text-center text-xs" style={{ color: '#F6465D' }}>
<div className="p-3 text-center text-xs" style={{ color: '#D6433A' }}>
{ts(gridRisk.error, language)}: {error}
</div>
)
@@ -109,7 +109,7 @@ export function GridRiskPanel({
if (!riskInfo) {
return (
<div className="p-3 text-center text-xs" style={{ color: '#848E9C' }}>
<div className="p-3 text-center text-xs" style={{ color: '#8A8478' }}>
{ts(gridRisk.noData, language)}
</div>
)
@@ -119,12 +119,12 @@ export function GridRiskPanel({
<div className="rounded-lg" style={cardStyle}>
{/* Collapsible Header */}
<div
className="flex items-center justify-between p-3 cursor-pointer hover:bg-[#1E2329] transition-colors"
className="flex items-center justify-between p-3 cursor-pointer hover:bg-[#E8E2D5] transition-colors"
onClick={() => setExpanded(!expanded)}
>
<div className="flex items-center gap-2">
<Shield className="w-4 h-4" style={{ color: '#F0B90B' }} />
<span className="font-medium text-sm" style={{ color: '#EAECEF' }}>
<Shield className="w-4 h-4" style={{ color: '#E0483B' }} />
<span className="font-medium text-sm" style={{ color: '#1A1813' }}>
{ts(gridRisk.gridRisk, language)}
</span>
</div>
@@ -137,7 +137,7 @@ export function GridRiskPanel({
>
{ts(gridRisk[(riskInfo.regime_level || 'standard') as keyof typeof gridRisk], language)}
</span>
<span className="font-mono" style={{ color: '#EAECEF' }}>
<span className="font-mono" style={{ color: '#1A1813' }}>
{riskInfo.effective_leverage.toFixed(1)}x
</span>
<span
@@ -148,9 +148,9 @@ export function GridRiskPanel({
</span>
</div>
{expanded ? (
<ChevronUp className="w-4 h-4" style={{ color: '#848E9C' }} />
<ChevronUp className="w-4 h-4" style={{ color: '#8A8478' }} />
) : (
<ChevronDown className="w-4 h-4" style={{ color: '#848E9C' }} />
<ChevronDown className="w-4 h-4" style={{ color: '#8A8478' }} />
)}
</div>
</div>
@@ -161,25 +161,25 @@ export function GridRiskPanel({
{/* Row 1: Leverage & Position */}
<div className="grid grid-cols-2 gap-3">
{/* Leverage */}
<div className="p-2 rounded" style={{ background: '#1E2329' }}>
<div className="p-2 rounded" style={{ background: '#E8E2D5' }}>
<div className="flex items-center gap-1 mb-2">
<TrendingUp className="w-3 h-3" style={{ color: '#F0B90B' }} />
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{ts(gridRisk.leverageInfo, language)}</span>
<TrendingUp className="w-3 h-3" style={{ color: '#E0483B' }} />
<span className="text-xs font-medium" style={{ color: '#8A8478' }}>{ts(gridRisk.leverageInfo, language)}</span>
</div>
<div className="grid grid-cols-3 gap-1 text-xs">
<div>
<div style={{ color: '#5E6673' }}>{ts(gridRisk.currentLeverage, language)}</div>
<div className="font-mono" style={{ color: '#EAECEF' }}>{riskInfo.current_leverage}x</div>
<div style={{ color: '#8A8478' }}>{ts(gridRisk.currentLeverage, language)}</div>
<div className="font-mono" style={{ color: '#1A1813' }}>{riskInfo.current_leverage}x</div>
</div>
<div>
<div style={{ color: '#5E6673' }}>{ts(gridRisk.effectiveLeverage, language)}</div>
<div className="font-mono" style={{ color: '#F0B90B' }}>{riskInfo.effective_leverage.toFixed(2)}x</div>
<div style={{ color: '#8A8478' }}>{ts(gridRisk.effectiveLeverage, language)}</div>
<div className="font-mono" style={{ color: '#E0483B' }}>{riskInfo.effective_leverage.toFixed(2)}x</div>
</div>
<div>
<div style={{ color: '#5E6673' }}>{ts(gridRisk.recommendedLeverage, language)}</div>
<div style={{ color: '#8A8478' }}>{ts(gridRisk.recommendedLeverage, language)}</div>
<div
className="font-mono"
style={{ color: riskInfo.current_leverage > riskInfo.recommended_leverage ? '#F6465D' : '#0ECB81' }}
style={{ color: riskInfo.current_leverage > riskInfo.recommended_leverage ? '#D6433A' : '#2E8B57' }}
>
{riskInfo.recommended_leverage}x
</div>
@@ -188,29 +188,29 @@ export function GridRiskPanel({
</div>
{/* Position */}
<div className="p-2 rounded" style={{ background: '#1E2329' }}>
<div className="p-2 rounded" style={{ background: '#E8E2D5' }}>
<div className="flex items-center gap-1 mb-2">
<Activity className="w-3 h-3" style={{ color: '#F0B90B' }} />
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{ts(gridRisk.positionInfo, language)}</span>
<Activity className="w-3 h-3" style={{ color: '#E0483B' }} />
<span className="text-xs font-medium" style={{ color: '#8A8478' }}>{ts(gridRisk.positionInfo, language)}</span>
</div>
<div className="grid grid-cols-3 gap-1 text-xs">
<div>
<div style={{ color: '#5E6673' }}>{ts(gridRisk.currentPosition, language)}</div>
<div className="font-mono" style={{ color: '#EAECEF' }}>{formatUSD(riskInfo.current_position)}</div>
<div style={{ color: '#8A8478' }}>{ts(gridRisk.currentPosition, language)}</div>
<div className="font-mono" style={{ color: '#1A1813' }}>{formatUSD(riskInfo.current_position)}</div>
</div>
<div>
<div style={{ color: '#5E6673' }}>{ts(gridRisk.maxPosition, language)}</div>
<div className="font-mono" style={{ color: '#EAECEF' }}>{formatUSD(riskInfo.max_position)}</div>
<div style={{ color: '#8A8478' }}>{ts(gridRisk.maxPosition, language)}</div>
<div className="font-mono" style={{ color: '#1A1813' }}>{formatUSD(riskInfo.max_position)}</div>
</div>
<div>
<div style={{ color: '#5E6673' }}>{ts(gridRisk.positionPercent, language)}</div>
<div style={{ color: '#8A8478' }}>{ts(gridRisk.positionPercent, language)}</div>
<div className="font-mono" style={{ color: getPositionColor(riskInfo.position_percent) }}>
{riskInfo.position_percent.toFixed(1)}%
</div>
</div>
</div>
{/* Mini progress bar */}
<div className="h-1 mt-2 rounded-full overflow-hidden" style={{ background: '#2B3139' }}>
<div className="h-1 mt-2 rounded-full overflow-hidden" style={{ background: '#E8E2D5' }}>
<div
className="h-full rounded-full"
style={{ width: `${Math.min(riskInfo.position_percent, 100)}%`, background: getPositionColor(riskInfo.position_percent) }}
@@ -222,33 +222,33 @@ export function GridRiskPanel({
{/* Row 2: Market State & Liquidation */}
<div className="grid grid-cols-2 gap-3">
{/* Market State */}
<div className="p-2 rounded" style={{ background: '#1E2329' }}>
<div className="p-2 rounded" style={{ background: '#E8E2D5' }}>
<div className="flex items-center gap-1 mb-2">
<Shield className="w-3 h-3" style={{ color: '#F0B90B' }} />
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{ts(gridRisk.marketState, language)}</span>
<Shield className="w-3 h-3" style={{ color: '#E0483B' }} />
<span className="text-xs font-medium" style={{ color: '#8A8478' }}>{ts(gridRisk.marketState, language)}</span>
</div>
<div className="grid grid-cols-2 gap-2 text-xs">
<div>
<div style={{ color: '#5E6673' }}>{ts(gridRisk.regimeLevel, language)}</div>
<div style={{ color: '#8A8478' }}>{ts(gridRisk.regimeLevel, language)}</div>
<div className="font-medium" style={{ color: getRegimeColor(riskInfo.regime_level) }}>
{ts(gridRisk[(riskInfo.regime_level || 'standard') as keyof typeof gridRisk], language)}
</div>
</div>
<div>
<div style={{ color: '#5E6673' }}>{ts(gridRisk.currentPrice, language)}</div>
<div className="font-mono" style={{ color: '#EAECEF' }}>{formatPrice(riskInfo.current_price)}</div>
<div style={{ color: '#8A8478' }}>{ts(gridRisk.currentPrice, language)}</div>
<div className="font-mono" style={{ color: '#1A1813' }}>{formatPrice(riskInfo.current_price)}</div>
</div>
<div>
<div style={{ color: '#5E6673' }}>{ts(gridRisk.breakoutLevel, language)}</div>
<div style={{ color: '#8A8478' }}>{ts(gridRisk.breakoutLevel, language)}</div>
<div className="font-medium" style={{ color: getBreakoutColor(riskInfo.breakout_level) }}>
{ts(gridRisk[(riskInfo.breakout_level || 'none') as keyof typeof gridRisk], language)}
</div>
</div>
<div>
<div style={{ color: '#5E6673' }}>{ts(gridRisk.breakoutDirection, language)}</div>
<div style={{ color: '#8A8478' }}>{ts(gridRisk.breakoutDirection, language)}</div>
<div
className="font-medium"
style={{ color: riskInfo.breakout_direction === 'up' ? '#0ECB81' : riskInfo.breakout_direction === 'down' ? '#F6465D' : '#848E9C' }}
style={{ color: riskInfo.breakout_direction === 'up' ? '#2E8B57' : riskInfo.breakout_direction === 'down' ? '#D6433A' : '#8A8478' }}
>
{riskInfo.breakout_direction ? ts(gridRisk[riskInfo.breakout_direction as keyof typeof gridRisk], language) : '-'}
</div>
@@ -257,21 +257,21 @@ export function GridRiskPanel({
</div>
{/* Liquidation */}
<div className="p-2 rounded" style={{ background: '#1E2329' }}>
<div className="p-2 rounded" style={{ background: '#E8E2D5' }}>
<div className="flex items-center gap-1 mb-2">
<AlertTriangle className="w-3 h-3" style={{ color: '#F6465D' }} />
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{ts(gridRisk.liquidationInfo, language)}</span>
<AlertTriangle className="w-3 h-3" style={{ color: '#D6433A' }} />
<span className="text-xs font-medium" style={{ color: '#8A8478' }}>{ts(gridRisk.liquidationInfo, language)}</span>
</div>
<div className="grid grid-cols-2 gap-2 text-xs">
<div>
<div style={{ color: '#5E6673' }}>{ts(gridRisk.liquidationPrice, language)}</div>
<div className="font-mono" style={{ color: '#F6465D' }}>
<div style={{ color: '#8A8478' }}>{ts(gridRisk.liquidationPrice, language)}</div>
<div className="font-mono" style={{ color: '#D6433A' }}>
{riskInfo.liquidation_price > 0 ? formatPrice(riskInfo.liquidation_price) : '-'}
</div>
</div>
<div>
<div style={{ color: '#5E6673' }}>{ts(gridRisk.liquidationDistance, language)}</div>
<div className="font-mono" style={{ color: '#F6465D' }}>
<div style={{ color: '#8A8478' }}>{ts(gridRisk.liquidationDistance, language)}</div>
<div className="font-mono" style={{ color: '#D6433A' }}>
{riskInfo.liquidation_distance > 0 ? `${riskInfo.liquidation_distance.toFixed(1)}%` : '-'}
</div>
</div>
@@ -280,27 +280,27 @@ export function GridRiskPanel({
</div>
{/* Row 3: Box State */}
<div className="p-2 rounded" style={{ background: '#1E2329' }}>
<div className="p-2 rounded" style={{ background: '#E8E2D5' }}>
<div className="flex items-center gap-1 mb-2">
<Box className="w-3 h-3" style={{ color: '#F0B90B' }} />
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{ts(gridRisk.boxState, language)}</span>
<Box className="w-3 h-3" style={{ color: '#E0483B' }} />
<span className="text-xs font-medium" style={{ color: '#8A8478' }}>{ts(gridRisk.boxState, language)}</span>
</div>
<div className="grid grid-cols-3 gap-2 text-xs">
<div className="flex justify-between">
<span style={{ color: '#5E6673' }}>{ts(gridRisk.shortBox, language)}</span>
<span className="font-mono" style={{ color: '#EAECEF' }}>
<span style={{ color: '#8A8478' }}>{ts(gridRisk.shortBox, language)}</span>
<span className="font-mono" style={{ color: '#1A1813' }}>
{formatPrice(riskInfo.short_box_lower)} - {formatPrice(riskInfo.short_box_upper)}
</span>
</div>
<div className="flex justify-between">
<span style={{ color: '#5E6673' }}>{ts(gridRisk.midBox, language)}</span>
<span className="font-mono" style={{ color: '#EAECEF' }}>
<span style={{ color: '#8A8478' }}>{ts(gridRisk.midBox, language)}</span>
<span className="font-mono" style={{ color: '#1A1813' }}>
{formatPrice(riskInfo.mid_box_lower)} - {formatPrice(riskInfo.mid_box_upper)}
</span>
</div>
<div className="flex justify-between">
<span style={{ color: '#5E6673' }}>{ts(gridRisk.longBox, language)}</span>
<span className="font-mono" style={{ color: '#EAECEF' }}>
<span style={{ color: '#8A8478' }}>{ts(gridRisk.longBox, language)}</span>
<span className="font-mono" style={{ color: '#1A1813' }}>
{formatPrice(riskInfo.long_box_lower)} - {formatPrice(riskInfo.long_box_upper)}
</span>
</div>

View File

@@ -0,0 +1,75 @@
import { useMemo } from 'react'
import type { Kline } from '../../lib/api/data'
interface CandlesProps {
data: Kline[]
width?: number
height?: number
/** stretch to fill the parent's height (parent must have a definite height) */
fill?: boolean
}
/**
* Candles renders a compact OHLC candlestick chart from real kline data
* (GET /api/klines). Up candles use the terminal's profit green, down candles
* the loss red. Purely presentational — the parent fetches the real series.
*/
export function Candles({ data, width = 640, height = 150, fill = false }: CandlesProps) {
const candles = useMemo(() => {
if (!data || data.length === 0) return []
const slice = data.slice(-40)
const highs = slice.map((k) => k.high)
const lows = slice.map((k) => k.low)
const max = Math.max(...highs)
const min = Math.min(...lows)
const span = max - min || 1
const pad = 6
const gap = (width - pad * 2) / slice.length
const bodyW = Math.max(2, gap * 0.6)
const y = (v: number) => pad + (1 - (v - min) / span) * (height - pad * 2)
return slice.map((k, i) => {
const cx = pad + gap * i + gap / 2
const up = k.close >= k.open
return {
cx,
up,
wickTop: y(k.high),
wickBot: y(k.low),
bodyTop: y(Math.max(k.open, k.close)),
bodyBot: y(Math.min(k.open, k.close)),
bodyW,
}
})
}, [data, width, height])
if (candles.length === 0) return null
return (
<svg
width="100%"
height={fill ? '100%' : undefined}
viewBox={`0 0 ${width} ${height}`}
preserveAspectRatio={fill ? 'none' : 'xMidYMid meet'}
role="img"
aria-label="Candlestick chart"
style={{ display: 'block', ...(fill ? { height: '100%' } : {}) }}
>
{candles.map((c, i) => {
const color = c.up ? 'var(--tm-up)' : 'var(--tm-dn)'
return (
<g key={i} stroke={color} fill={color}>
<line x1={c.cx} y1={c.wickTop} x2={c.cx} y2={c.wickBot} strokeWidth={1} />
<rect
x={c.cx - c.bodyW / 2}
y={c.bodyTop}
width={c.bodyW}
height={Math.max(1, c.bodyBot - c.bodyTop)}
/>
</g>
)
})}
</svg>
)
}
export default Candles

View File

@@ -0,0 +1,300 @@
import { useMemo } from 'react'
import type { DecisionRecord } from '../../types'
/**
* ExecutionLog renders the AI trading agent's real decisions and order results
* as a high-density, newest-first terminal stream — Bloomberg-style log on the
* cream paper theme. Each cycle is a clearly-delimited block: a header bar
* (cycle no · time · action count), one badge line per AI action, and one
* indented sub-line per execution-log entry, color-coded by
* success / throttle / risk.
*
* Real data only — renders verbatim what's present on each DecisionRecord;
* verbose throttle strings are tidied for the gist but never invented.
*/
const C_AMBER = '#c8860b' // throttle / block warnings
// Strip dex prefix + quote suffix to the bare base ticker.
function baseSymbol(raw: string): string {
return raw
.toUpperCase()
.replace(/^XYZ:/, '')
.replace(/(USDT|USDC|USD)$/, '')
}
// HH:MM:SS from an ISO timestamp; guards against invalid input.
function fmtTime(iso: string): string {
const d = new Date(iso)
if (Number.isNaN(d.getTime())) return '--:--:--'
const pad = (n: number) => String(n).padStart(2, '0')
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
}
type Side = 'long' | 'short' | 'flat'
// Trade side of an action token — drives badge color.
function actionSide(action: string): Side {
const a = action.toLowerCase()
if (a === 'open_long' || a === 'close_short') return 'long'
if (a === 'open_short' || a === 'close_long') return 'short'
return 'flat'
}
function sideColor(side: Side): string {
if (side === 'long') return 'var(--tm-up)'
if (side === 'short') return 'var(--tm-dn)'
return 'var(--tm-muted)'
}
type LogTone = 'ok' | 'warn' | 'risk' | 'info'
// Classify an execution-log line into a tone for color-coding.
function logTone(line: string): LogTone {
const s = line.toLowerCase()
if (s.includes('succeed') || s.includes('success') || line.includes('✓')) return 'ok'
if (s.includes('throttle') || s.includes('re-entry') || s.includes('cooldown') || s.includes('blocked'))
return 'warn'
if (
s.includes('risk') ||
s.includes('fail') ||
s.includes('reject') ||
s.includes('error') ||
s.includes('denied') ||
line.includes('✗') ||
line.includes('❌')
)
return 'risk'
return 'info'
}
const TONE_COLOR: Record<LogTone, string> = {
ok: 'var(--tm-up)',
warn: C_AMBER,
risk: 'var(--tm-dn)',
info: 'var(--tm-ink-2)',
}
const TONE_GLYPH: Record<LogTone, string> = { ok: '✓', warn: '⚠', risk: '❌', info: '·' }
/**
* Tidy a verbose execution-log string to its gist without fabricating data.
* Strips a leading "└"/glyph the backend may already include (we render our
* own), the symbol (already shown), and collapses the wordy throttle phrasing
* to "throttle · closed 28m ago, wait 2m". Falls back to the original line.
*/
function cleanLog(raw: string): string {
let s = raw.replace(/^[\s└>•·]*[✓✗⚠❌]?\s*/, '').trim()
const throttle = s.match(/closed\s+([0-9smhd.]+)\s+ago;\s*wait\s+([0-9smhd.]+)/i)
if (throttle) {
const ago = throttle[1].replace(/(\d)0s$/, '$1').replace(/0s$/, '')
const wait = throttle[2].replace(/(\d)0s$/, '$1').replace(/0s$/, '')
return `throttle · closed ${ago} ago, wait ${wait}`
}
// drop a redundant leading "SYMBOL action" prefix when present
s = s.replace(/^[A-Z0-9:_-]{2,12}\s+(open_long|open_short|close_long|close_short)\s+/i, '')
return s
}
interface ExecutionLogProps {
decisions?: DecisionRecord[]
height?: number
}
export function ExecutionLog({ decisions, height = 440 }: ExecutionLogProps) {
// Newest cycle first.
const cycles = useMemo(() => {
const list = decisions ?? []
return [...list].sort((a, b) => b.cycle_number - a.cycle_number)
}, [decisions])
return (
<div style={{ fontFamily: 'var(--tm-mono)' }}>
{/* header */}
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 2 }}>
<span className="tm-px" style={{ fontSize: 11 }}>Execution log</span>
<span
className="tm-sc"
style={{ marginLeft: 'auto', color: cycles.length ? 'var(--tm-up)' : 'var(--tm-muted)' }}
>
{cycles.length ? `${cycles.length} cyc` : '—'}
</span>
</div>
<div className="tm-sc" style={{ fontSize: 9, marginBottom: 5 }}>
Execution log · AI decisions & fills per cycle
</div>
{/* legend */}
<div
className="tm-sc"
style={{ display: 'flex', flexWrap: 'wrap', gap: 12, marginBottom: 6, fontSize: 9 }}
>
<Legend glyph="✓" c="var(--tm-up)" label="ok" />
<Legend glyph="⚠" c={C_AMBER} label="throttle" />
<Legend glyph="❌" c="var(--tm-dn)" label="risk" />
</div>
<div className="tm-hair" style={{ marginBottom: 0 }} />
{!cycles.length ? (
<div className="tm-sc" style={{ padding: '16px 0' }}>No execution events yet.</div>
) : (
<div
style={{
height,
overflowY: 'auto',
fontSize: 10,
lineHeight: 1.5,
fontVariantNumeric: 'tabular-nums',
}}
>
{cycles.map((c) => (
<Cycle key={`${c.cycle_number}-${c.timestamp}`} record={c} />
))}
</div>
)}
</div>
)
}
function Legend({ glyph, c, label }: { glyph: string; c: string; label: string }) {
return (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
<span style={{ color: c, fontSize: 10 }}>{glyph}</span>
<span>{label}</span>
</span>
)
}
interface CycleProps {
record: DecisionRecord
}
function Cycle({ record }: CycleProps) {
const time = fmtTime(record.timestamp)
const actions = record.decisions ?? []
const logs = record.execution_log ?? []
const count = actions.length
return (
<div style={{ borderBottom: '1px solid var(--tm-hair)', padding: '5px 0' }}>
{/* cycle header bar */}
<div
className="tm-sc"
style={{
display: 'flex',
alignItems: 'center',
gap: 7,
fontSize: 9,
padding: '2px 6px',
marginBottom: actions.length || logs.length || record.error_message ? 4 : 0,
background: 'rgba(26,24,19,0.045)',
borderLeft: `2px solid ${record.success ? 'var(--tm-hair)' : 'var(--tm-dn)'}`,
color: 'var(--tm-ink-2)',
}}
>
<span style={{ color: 'var(--tm-ink)', fontWeight: 700 }}>CYCLE {record.cycle_number}</span>
<span style={{ color: 'var(--tm-muted)' }}>·</span>
<span style={{ color: 'var(--tm-muted)' }}>{time}</span>
<span style={{ marginLeft: 'auto', color: 'var(--tm-muted)' }}>
{count === 0 ? 'no action' : `${count} action${count > 1 ? 's' : ''}`}
</span>
{!record.success ? (
<span style={{ color: 'var(--tm-dn)', fontWeight: 700 }}>FAULT</span>
) : null}
</div>
{/* AI action lines */}
{actions.map((a, i) => {
const side = actionSide(a.action)
const aTime = fmtTime(a.timestamp)
return (
<div
key={`act-${i}`}
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '1px 6px',
color: 'var(--tm-ink-2)',
}}
>
<span style={{ color: 'var(--tm-muted)', flex: '0 0 auto', minWidth: 52 }}>
{aTime !== '--:--:--' ? aTime : time}
</span>
<ActionBadge action={a.action} side={side} />
<span style={{ color: 'var(--tm-ink)', fontWeight: 600, flex: '0 0 auto', minWidth: 48 }}>
{baseSymbol(a.symbol)}
</span>
{a.confidence != null ? (
<span style={{ color: 'var(--tm-muted)', flex: '0 0 auto' }}>
conf<span style={{ color: 'var(--tm-ink-2)' }}>{Math.round(a.confidence)}</span>
</span>
) : null}
</div>
)
})}
{/* execution-log result sub-lines */}
{logs.map((line, i) => (
<SubLine key={`log-${i}`} tone={logTone(line)} text={cleanLog(line)} />
))}
{/* fault message */}
{record.error_message ? (
<SubLine tone="risk" text={cleanLog(record.error_message)} />
) : null}
</div>
)
}
function ActionBadge({ action, side }: { action: string; side: Side }) {
const c = sideColor(side)
return (
<span
style={{
display: 'inline-flex',
alignItems: 'center',
flex: '0 0 auto',
padding: '0 5px',
height: 14,
fontSize: 9,
letterSpacing: '0.04em',
color: c,
border: `1px solid ${c}`,
background:
side === 'long'
? 'rgba(46,139,87,0.08)'
: side === 'short'
? 'rgba(214,67,58,0.08)'
: 'transparent',
borderRadius: 2,
whiteSpace: 'nowrap',
}}
>
{action.toLowerCase()}
</span>
)
}
function SubLine({ tone, text }: { tone: LogTone; text: string }) {
const color = TONE_COLOR[tone]
const glyph = TONE_GLYPH[tone]
return (
<div
style={{
display: 'flex',
gap: 6,
padding: '0 6px 0 10px',
marginLeft: 6,
borderLeft: '1px solid var(--tm-hair)',
color,
}}
>
<span style={{ flex: '0 0 auto', width: 10, textAlign: 'center' }}>{glyph}</span>
<span style={{ wordBreak: 'break-word' }}>{text}</span>
</div>
)
}
export default ExecutionLog

View File

@@ -0,0 +1,150 @@
import { useMemo } from 'react'
import type { FlowMarketItem } from '../../lib/api/data'
interface FlowMarketsProps {
items?: FlowMarketItem[]
window?: string
}
function baseLabel(raw: string): string {
return raw.toUpperCase().replace(/^XYZ:/, '').replace(/[-_]/g, '').replace(/(USDT|USDC|USD)$/, '')
}
function num(s: string): number {
const n = parseFloat(s)
return Number.isFinite(n) ? n : 0
}
function compact(n: number): string {
const a = Math.abs(n)
const sign = n < 0 ? '-' : '+'
if (a >= 1e9) return `${sign}$${(a / 1e9).toFixed(2)}B`
if (a >= 1e6) return `${sign}$${(a / 1e6).toFixed(2)}M`
if (a >= 1e3) return `${sign}$${(a / 1e3).toFixed(1)}K`
return `${sign}$${a.toFixed(0)}`
}
// Shared 5-column grid so the header, every row, and the legend line up exactly.
// symbol | net inflow | buy/sell split bar | trades | last price
const GRID = '64px 96px minmax(120px, 1fr) 80px 96px'
/**
* FlowMarkets renders the Vergex net-flow ranking (real data from
* GET /api/vergex/flow-markets via the trader's claw402 wallet). Each row shows
* a market's net inflow over the window, a buy/sell split bar, trade count, and
* latest price. Sorted by net inflow descending (the upstream ordering).
*/
export function FlowMarkets({ items, window = '1h' }: FlowMarketsProps) {
const win = window.toUpperCase()
const rows = useMemo(() => {
if (!items || items.length === 0) return []
const max = items.reduce((m, it) => Math.max(m, Math.abs(num(it.netFlow))), 1)
return items.slice(0, 10).map((it) => {
const buy = num(it.buyNotional)
const sell = num(it.sellNotional)
const tot = buy + sell || 1
const net = num(it.netFlow)
return {
key: it.key || it.symbol,
label: baseLabel(it.symbol),
net,
netStr: compact(net),
buyPct: (buy / tot) * 100,
widthPct: (Math.abs(net) / max) * 100,
trades: it.trades,
price: num(it.latestPrice),
}
})
}, [items])
if (rows.length === 0) {
return <div className="tm-sc" style={{ padding: '12px 0' }}>No net-flow data (claw402 payment required).</div>
}
return (
<div className="tm-mono" style={{ fontSize: 11 }}>
{/* column header — every number below is labeled by this row */}
<div
className="tm-sc"
style={{
display: 'grid',
gridTemplateColumns: GRID,
alignItems: 'end',
gap: 12,
paddingBottom: 4,
borderBottom: '1px solid var(--tm-hair)',
fontSize: 9,
}}
>
<span>SYMBOL</span>
<span style={{ textAlign: 'right' }}>{win} NET</span>
<span>BUY/SELL</span>
<span style={{ textAlign: 'right' }}>TRADES</span>
<span style={{ textAlign: 'right' }}>PRICE</span>
</div>
{/* rows */}
{rows.map((r) => (
<div
key={r.key}
style={{
display: 'grid',
gridTemplateColumns: GRID,
alignItems: 'center',
gap: 12,
height: 26,
borderBottom: '1px solid var(--tm-hair)',
}}
>
{/* symbol */}
<span style={{ fontWeight: 600, color: 'var(--tm-ink)' }}>{r.label}</span>
{/* net inflow figure (green = net buying / red = net selling) */}
<span className={r.net >= 0 ? 'tm-up' : 'tm-dn'} style={{ textAlign: 'right', fontWeight: 600 }}>
{r.netStr}
</span>
{/* buy/sell split bar — width encodes net-inflow magnitude (vs. the top
market), the green/red split inside encodes buy vs. sell share.
Green grows from the LEFT (buy), red fills the REST (sell). */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<div
title={`buy ${r.buyPct.toFixed(0)}% / sell ${(100 - r.buyPct).toFixed(0)}%`}
style={{
position: 'relative',
height: 8,
flex: 1,
minWidth: 40,
background: 'var(--tm-hair)',
}}
>
<div style={{ position: 'absolute', inset: 0, width: `${Math.max(4, r.widthPct)}%`, display: 'flex' }}>
<div style={{ width: `${r.buyPct}%`, background: 'var(--tm-up)' }} />
<div style={{ width: `${100 - r.buyPct}%`, background: 'var(--tm-dn)' }} />
</div>
</div>
<span className="tm-sc" style={{ fontSize: 9, minWidth: 30, textAlign: 'right' }}>
{r.buyPct.toFixed(0)}%
</span>
</div>
{/* trade count */}
<span style={{ textAlign: 'right', color: 'var(--tm-ink-2)' }}>
{r.trades.toLocaleString('en-US')}
</span>
{/* last price */}
<span style={{ textAlign: 'right', color: 'var(--tm-ink-2)' }}>
${r.price.toLocaleString('en-US', { maximumFractionDigits: 4 })}
</span>
</div>
))}
{/* legend — explains every column */}
<div className="tm-sc" style={{ marginTop: 8, fontSize: 9, lineHeight: 1.6 }}>
net inflow = {win} net buying · <span className="tm-up">green</span>/<span className="tm-dn">red</span> = buy/sell split
{' · '}trades = count · last price = last traded price
</div>
</div>
)
}
export default FlowMarkets

View File

@@ -0,0 +1,166 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import useSWR from 'swr'
import { api } from '../../lib/api'
import type { Kline } from '../../lib/api/data'
import { Candles } from './Candles'
/**
* KlineChart shows a live candlestick chart. History is seeded from the backend
* kline endpoint, then the latest bar streams in real time from Hyperliquid's
* public `candle` WebSocket (the forming candle ticks live and rolls over each
* interval). Resolves crypto majors to the main dex and synthetic markets to
* the `xyz:` builder dex, matching the order book.
*
* Real OHLC only — no synthetic data.
*/
const HL_INFO = 'https://api.hyperliquid.xyz/info'
const HL_WS = 'wss://api.hyperliquid.xyz/ws'
const INTERVAL = '1m'
const MAX_BARS = 90
function baseSymbol(raw: string): string {
return raw.toUpperCase().replace(/^XYZ:/, '').replace(/(USDT|USDC|USD)$/, '')
}
interface KlineChartProps {
symbol: string
/** target chart height in px (ignored when fill) */
height?: number
/** stretch the chart to fill the parent's remaining height */
fill?: boolean
}
export function KlineChart({ symbol, height = 360, fill = false }: KlineChartProps) {
const base = baseSymbol(symbol || '')
// history seed (resynced occasionally; the WS carries the live bar)
const { data: seed, isLoading } = useSWR(
base ? ['kline', base, INTERVAL] : null,
() => api.getKlines(base, INTERVAL, 'hyperliquid', MAX_BARS, true),
{ refreshInterval: 60000, revalidateOnFocus: false, shouldRetryOnError: false, keepPreviousData: true },
)
// resolve the Hyperliquid coin id (xyz: dex membership)
const [xyzSet, setXyzSet] = useState<Set<string>>(new Set())
useEffect(() => {
let alive = true
fetch(HL_INFO, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'allMids', dex: 'xyz' }) })
.then((r) => r.json())
.then((mids: Record<string, string>) => {
if (!alive) return
const set = new Set<string>()
for (const k of Object.keys(mids || {})) set.add(k.replace(/^xyz:/, '').toUpperCase())
setXyzSet(set)
})
.catch(() => {})
return () => {
alive = false
}
}, [])
const coin = useMemo(() => (base ? (xyzSet.has(base) ? `xyz:${base}` : base) : ''), [base, xyzSet])
// live bar from the candle WS
const [liveBar, setLiveBar] = useState<Kline | null>(null)
const [wsLive, setWsLive] = useState(false)
const pending = useRef<Kline | null>(null)
useEffect(() => {
if (!coin) return
setLiveBar(null)
let ws: WebSocket | null = null
let raf: number | null = null
let retry: ReturnType<typeof setTimeout> | null = null
let closed = false
const connect = () => {
ws = new WebSocket(HL_WS)
ws.onopen = () => ws?.send(JSON.stringify({ method: 'subscribe', subscription: { type: 'candle', coin, interval: INTERVAL } }))
ws.onmessage = (ev) => {
try {
const msg = JSON.parse(ev.data)
if (msg.channel !== 'candle' || !msg.data) return
const d = msg.data
pending.current = { openTime: d.t, closeTime: d.T, open: +d.o, high: +d.h, low: +d.l, close: +d.c, volume: +d.v }
setWsLive(true)
} catch {
/* ignore */
}
}
ws.onclose = () => {
if (closed) return
setWsLive(false)
retry = setTimeout(connect, 1500)
}
ws.onerror = () => ws?.close()
}
connect()
const loop = () => {
if (pending.current) {
setLiveBar(pending.current)
pending.current = null
}
raf = requestAnimationFrame(loop)
}
raf = requestAnimationFrame(loop)
return () => {
closed = true
if (raf) cancelAnimationFrame(raf)
if (retry) clearTimeout(retry)
try {
ws?.send(JSON.stringify({ method: 'unsubscribe', subscription: { type: 'candle', coin, interval: INTERVAL } }))
} catch {
/* socket gone */
}
ws?.close()
}
}, [coin])
// merge the live bar into the seeded history
const candles = useMemo(() => {
const hist = seed ?? []
if (!liveBar) return hist
const arr = [...hist]
const last = arr[arr.length - 1]
if (last && liveBar.openTime === last.openTime) arr[arr.length - 1] = liveBar
else if (!last || liveBar.openTime > last.openTime) arr.push(liveBar)
return arr.slice(-MAX_BARS)
}, [seed, liveBar])
const last = candles.length ? candles[candles.length - 1].close : 0
const first = candles.length ? candles[0].open : 0
const chg = first ? ((last - first) / first) * 100 : 0
const live = wsLive && candles.length > 0
return (
<div style={{ fontFamily: 'var(--tm-mono)', ...(fill ? { display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0 } : {}) }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 6 }}>
<span className="tm-px" style={{ fontSize: 11 }}>{base || 'MARKET'}</span>
<span className="tm-sc">{INTERVAL} · Live candles</span>
<span className="tm-sc" style={{ marginLeft: 'auto', color: live ? 'var(--tm-up)' : 'var(--tm-muted)' }}>
{live ? '● live' : isLoading || candles.length ? '○ sync' : '○ —'}
</span>
</div>
{last > 0 && (
<div className="tm-mono" style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 4, fontSize: 12 }}>
<span style={{ fontWeight: 600 }}>${last.toLocaleString('en-US', { maximumFractionDigits: 4 })}</span>
<span className={chg >= 0 ? 'tm-up' : 'tm-dn'} style={{ fontSize: 11 }}>{chg >= 0 ? '+' : ''}{chg.toFixed(2)}%</span>
<span className="tm-sc" style={{ marginLeft: 'auto' }}>{candles.length} bars · {INTERVAL}</span>
</div>
)}
{candles.length > 0 ? (
fill ? (
<div style={{ flex: 1, minHeight: 0 }}>
<Candles data={candles} width={380} height={height} fill />
</div>
) : (
<Candles data={candles} width={380} height={height} />
)
) : (
<div className="tm-sc" style={{ padding: '20px 0' }}>Loading market</div>
)}
</div>
)
}
export default KlineChart

View File

@@ -0,0 +1,236 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import useSWR from 'swr'
import { api } from '../../lib/api'
import type { VergexHeatmapBin } from '../../lib/api/data'
/**
* LiquidationMap renders the vergex (claw402) cost / liquidation heatmap as a
* vertical price ladder — position-cost concentration plus liquidation fuel by
* price level. Long metrics diverge right, short metrics diverge left from the
* mark price. Cream-themed adaptation of a Bloomberg-style liquidation map.
*
* Real paid data only (hip3_perp synthetic markets). Polled at 5 min to spare
* the claw402 wallet.
*/
const C_LONG_COST = 'var(--tm-up)' // forest green
const C_SHORT_COST = 'var(--tm-dn)' // crimson
const C_LONG_LIQ = '#c8860b' // amber — long-liquidation fuel (price falls)
const C_SHORT_LIQ = '#2c7a9e' // teal — short-liquidation fuel (price rises)
function fmtUsd(n: number): string {
const a = Math.abs(n)
if (a >= 1e9) return `$${(n / 1e9).toFixed(2)}B`
if (a >= 1e6) return `$${(n / 1e6).toFixed(2)}M`
if (a >= 1e3) return `$${(n / 1e3).toFixed(1)}K`
return `$${n.toFixed(0)}`
}
function fmtPx(n: number): string {
if (n >= 1000) return n.toLocaleString('en-US', { maximumFractionDigits: 0 })
if (n >= 1) return n.toLocaleString('en-US', { maximumFractionDigits: 2 })
return n.toLocaleString('en-US', { maximumFractionDigits: 4 })
}
interface Row extends VergexHeatmapBin {
px: number
longCost: number
shortCost: number
longLiq: number
shortLiq: number
}
interface LiquidationMapProps {
symbol: string
marketType?: string
/** fixed height of the scrollable ladder (px); auto-centres on the mark */
height?: number
}
export function LiquidationMap({ symbol, marketType = 'hip3_perp', height = 460 }: LiquidationMapProps) {
// Synthetic markets live under marketType "hip3_perp"; crypto majors under
// "perp". We try the caller's guess first and fall back to the other so the
// heatmap resolves for ANY symbol that has one.
const fetcher = (mt: string) =>
api.getVergexCostLiquidationHeatmap({ marketType: mt, symbol, chain: 'mainnet', liqBand: '15' })
const opts = { refreshInterval: 300000, revalidateOnFocus: false, keepPreviousData: true }
const primary = useSWR(symbol ? ['heatmap', marketType, symbol] : null, () => fetcher(marketType), opts)
const primaryHasBins = !!primary.data?.data?.bins?.length
const altMt = marketType === 'perp' ? 'hip3_perp' : 'perp'
const needAlt = !primaryHasBins && !primary.isLoading && primary.data !== undefined
const alt = useSWR(needAlt && symbol ? ['heatmap', altMt, symbol] : null, () => fetcher(altMt), opts)
const data = primaryHasBins ? primary.data : alt.data
const isLoading = primary.isLoading || (needAlt && alt.isLoading)
const error = primaryHasBins ? undefined : alt.error || primary.error
const [hover, setHover] = useState<number | null>(null)
const scrollRef = useRef<HTMLDivElement>(null)
const markRef = useRef<HTMLDivElement>(null)
const view = useMemo(() => {
const d = data?.data
// keepPreviousData can leave a stale heatmap on screen when the selected
// symbol has no hip3 market (e.g. a crypto major) — detect the mismatch and
// treat it as no-data so the panel honestly reflects the requested symbol.
const requested = (symbol || '').toUpperCase().replace(/^XYZ:/, '')
const loaded = (d?.market?.symbol || '').toUpperCase().replace(/^XYZ:/, '')
const stale = !!loaded && loaded !== requested
const raw = stale ? [] : d?.bins ?? []
const rows: Row[] = raw
.map((b) => ({
px: b.px ?? ((b.bucketStartPrice ?? 0) + (b.bucketEndPrice ?? 0)) / 2,
longCost: b.longCost ?? 0,
shortCost: b.shortCost ?? 0,
longLiq: b.longLiq ?? 0,
shortLiq: b.shortLiq ?? 0,
...b,
}))
.filter((r) => r.px > 0 && (r.longCost || r.shortCost || r.longLiq || r.shortLiq))
.sort((a, b) => b.px - a.px)
const maxSide = rows.reduce(
(m, r) => Math.max(m, r.longCost + r.longLiq, r.shortCost + r.shortLiq),
1,
)
const totals = rows.reduce(
(t, r) => ({
lc: t.lc + r.longCost,
sc: t.sc + r.shortCost,
ll: t.ll + r.longLiq,
sl: t.sl + r.shortLiq,
}),
{ lc: 0, sc: 0, ll: 0, sl: 0 },
)
return { rows, maxSide, mark: stale ? 0 : d?.markPrice ?? 0, costAddrs: stale ? 0 : d?.costAddrs ?? 0, liqAddrs: stale ? 0 : d?.liqAddrs ?? 0, totals, dispSymbol: stale ? symbol : d?.market?.symbol || symbol }
}, [data, symbol])
const markRowIdx = useMemo(() => {
if (!view.mark || !view.rows.length) return -1
let best = 0
let bd = Infinity
view.rows.forEach((r, i) => {
const dd = Math.abs(r.px - view.mark)
if (dd < bd) {
bd = dd
best = i
}
})
return best
}, [view])
// centre the scroll ladder on the mark (current) price once data arrives.
// Uses bounding-rect math (not offsetTop, which depends on the offsetParent)
// and re-applies on the next frame so it lands after layout settles.
useEffect(() => {
const sc = scrollRef.current
const mk = markRef.current
if (!sc || !mk) return
const apply = () => {
const rel = mk.getBoundingClientRect().top - sc.getBoundingClientRect().top + sc.scrollTop
sc.scrollTop = rel - sc.clientHeight / 2 + mk.offsetHeight / 2
}
apply()
const id = requestAnimationFrame(apply)
return () => cancelAnimationFrame(id)
}, [markRowIdx, view.rows.length, height, view.dispSymbol])
const rowH = view.rows.length > 44 ? 8 : view.rows.length > 28 ? 11 : 15
const hv = hover != null ? view.rows[hover] : null
return (
<div style={{ fontFamily: 'var(--tm-mono)' }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 3 }}>
<span className="tm-px" style={{ fontSize: 11 }}>Cost / Liq map</span>
<span className="tm-sc">{view.dispSymbol}</span>
<span className="tm-sc" style={{ marginLeft: 'auto', color: view.rows.length ? 'var(--tm-up)' : 'var(--tm-muted)' }}>
{view.rows.length ? '● live' : isLoading ? '○ sync' : '○ —'}
</span>
</div>
{/* legend */}
<div className="tm-sc" style={{ display: 'flex', flexWrap: 'wrap', gap: 10, marginBottom: 4, fontSize: 9 }}>
<Swatch c={C_LONG_COST} label="Long cost" />
<Swatch c={C_SHORT_COST} label="Short cost" />
<Swatch c={C_LONG_LIQ} label="Long liq" />
<Swatch c={C_SHORT_LIQ} label="Short liq" />
</div>
{/* hover readout / mark line */}
<div className="tm-mono" style={{ fontSize: 10, color: 'var(--tm-ink-2)', minHeight: 14, marginBottom: 2 }}>
{hv ? (
<span>
<b>{fmtPx(hv.px)}</b> · Cost line <span style={{ color: C_LONG_COST }}>{fmtUsd(hv.longCost)}</span>/<span style={{ color: C_SHORT_COST }}>{fmtUsd(hv.shortCost)}</span>
{' · '}liq <span style={{ color: C_LONG_LIQ }}>{fmtUsd(hv.longLiq)}</span>/<span style={{ color: C_SHORT_LIQ }}>{fmtUsd(hv.shortLiq)}</span>
</span>
) : (
<span className="tm-sc">mark <b style={{ color: 'var(--tm-red)' }}>{view.mark ? fmtPx(view.mark) : '—'}</b> · {view.costAddrs.toLocaleString()} positions / {view.liqAddrs.toLocaleString()} liq levels</span>
)}
</div>
{error && !view.rows.length ? (
<div className="tm-sc" style={{ padding: '16px 0' }}>No cost/liq heatmap for {view.dispSymbol} (crypto / main-dex markets have none).</div>
) : !view.rows.length ? (
<div className="tm-sc" style={{ padding: '16px 0' }}>Loading cost/liquidation map</div>
) : (
<div>
<div ref={scrollRef} style={{ maxHeight: height, overflowY: 'auto' }}>
{view.rows.map((r, i) => {
const isMark = i === markRowIdx
const lcW = (r.longCost / view.maxSide) * 100
const llW = (r.longLiq / view.maxSide) * 100
const scW = (r.shortCost / view.maxSide) * 100
const slW = (r.shortLiq / view.maxSide) * 100
const showLabel = i % 4 === 0 || isMark
return (
<div
key={i}
ref={isMark ? markRef : undefined}
onMouseEnter={() => setHover(i)}
onMouseLeave={() => setHover(null)}
style={{
display: 'grid',
gridTemplateColumns: '52px 1fr 1fr',
alignItems: 'center',
height: rowH,
background: hover === i ? 'rgba(26,24,19,0.05)' : 'transparent',
borderTop: isMark ? '1px solid var(--tm-red)' : 'none',
}}
>
<span style={{ fontSize: 9, textAlign: 'right', paddingRight: 6, color: isMark ? 'var(--tm-red)' : 'var(--tm-muted)', fontWeight: isMark ? 700 : 400 }}>
{showLabel ? fmtPx(r.px) : ''}
</span>
{/* short side — bars anchored at center, extend left (cost nearest center) */}
<div style={{ position: 'relative', height: '100%', display: 'flex', justifyContent: 'flex-end' }}>
<div style={{ width: `${slW}%`, background: C_SHORT_LIQ, opacity: 0.85 }} />
<div style={{ width: `${scW}%`, background: C_SHORT_COST }} />
</div>
{/* long side — bars from center, extend right (cost nearest center) */}
<div style={{ position: 'relative', height: '100%', display: 'flex', justifyContent: 'flex-start' }}>
<div style={{ width: `${lcW}%`, background: C_LONG_COST }} />
<div style={{ width: `${llW}%`, background: C_LONG_LIQ, opacity: 0.85 }} />
</div>
</div>
)
})}
</div>
{/* totals footer */}
<div className="tm-sc" style={{ display: 'flex', gap: 10, marginTop: 4, fontSize: 9, flexWrap: 'wrap' }}>
<span>Cost line <span style={{ color: C_LONG_COST }}>{fmtUsd(view.totals.lc)}</span>/<span style={{ color: C_SHORT_COST }}>{fmtUsd(view.totals.sc)}</span></span>
<span>liq <span style={{ color: C_LONG_LIQ }}>{fmtUsd(view.totals.ll)}</span>/<span style={{ color: C_SHORT_LIQ }}>{fmtUsd(view.totals.sl)}</span></span>
</div>
</div>
)}
</div>
)
}
function Swatch({ c, label }: { c: string; label: string }) {
return (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 3 }}>
<span style={{ width: 8, height: 8, background: c, display: 'inline-block' }} />
{label}
</span>
)
}
export default LiquidationMap

View File

@@ -0,0 +1,213 @@
import { useMemo } from 'react'
/**
* OrchestrationTopology renders the decision funnel as a chain of tightly-packed
* dot matrices — one wide grid per layer (flow → signal → decision → execute →
* hold), columns sitting close together. Every grid is always full: real symbols
* are SOLID (green long / red short) and SCATTERED across the grid; empty cells
* are HOLLOW placeholders. Matching symbols connect forward; the engine fans
* beams to the candidates that progress.
*/
export interface FunnelItem {
symbol: string
dir: 'long' | 'short'
}
export interface FunnelLayer {
key: string
title: string
zh: string
items: FunnelItem[]
}
interface OrchestrationTopologyProps {
layers: FunnelLayer[]
className?: string
}
const VB_W = 800
const ENGINE_X = 40
const HEADER_Y = 30
const COLS = 8
const LONG_ROWS = 5
const SHORT_ROWS = 5
const CAP_LONG = COLS * LONG_ROWS // 40
const CAP_SHORT = COLS * SHORT_ROWS // 40
const DX = 11
const DY = 11
const ZONE_GAP = 8
const LAYER_START = 100
const LAYER_STEP = 128
const LONG = 'var(--tm-up)'
const SHORT = 'var(--tm-dn)'
function baseSymbol(raw: string): string {
return raw.toUpperCase().replace(/^XYZ:/, '').replace(/[-_]/g, '').replace(/(USDT|USDC|USD)$/, '')
}
// evenly spread `count` solid items across `capacity` grid cells
function scatter(count: number, capacity: number): Map<number, number> {
const m = new Map<number, number>()
if (count <= 0) return m
for (let i = 0; i < count; i++) {
const cell = Math.min(capacity - 1, Math.floor(((i + 0.5) * capacity) / count))
m.set(cell, i)
}
return m
}
interface Cell {
base?: string
x: number
y: number
dir: 'long' | 'short'
solid: boolean
}
function splitDedup(items: FunnelItem[]) {
const seen = new Set<string>()
const longs: string[] = []
const shorts: string[] = []
for (const it of items) {
const b = baseSymbol(it.symbol)
if (!b || seen.has(b)) continue
seen.add(b)
;(it.dir === 'short' ? shorts : longs).push(b)
}
return { longs: longs.slice(0, CAP_LONG), shorts: shorts.slice(0, CAP_SHORT) }
}
export function OrchestrationTopology({ layers, className }: OrchestrationTopologyProps) {
const { prepared, cellsByLayer, realByLayer, height, cy, colX } = useMemo(() => {
const prep = layers.map((l) => ({ ...l, ...splitDedup(l.items) }))
const xs = prep.map((_, i) => LAYER_START + i * LAYER_STEP)
const gridH = (LONG_ROWS + SHORT_ROWS) * DY + ZONE_GAP
const h = HEADER_Y + gridH + 14
const centerY = HEADER_Y + gridH / 2
const longTop = centerY - gridH / 2
const shortTop = longTop + LONG_ROWS * DY + ZONE_GAP
const cells: Cell[][] = []
const realMaps: Map<string, { x: number; y: number; dir: 'long' | 'short' }>[] = []
prep.forEach((d, li) => {
const layerCells: Cell[] = []
const real = new Map<string, { x: number; y: number; dir: 'long' | 'short' }>()
const longScatter = scatter(d.longs.length, CAP_LONG)
const shortScatter = scatter(d.shorts.length, CAP_SHORT)
for (let idx = 0; idx < CAP_LONG; idx++) {
const c = idx % COLS
const r = Math.floor(idx / COLS)
const x = xs[li] + c * DX
const y = longTop + r * DY
const itemIdx = longScatter.get(idx)
const base = itemIdx !== undefined ? d.longs[itemIdx] : undefined
layerCells.push({ base, x, y, dir: 'long', solid: !!base })
if (base) real.set(base, { x, y, dir: 'long' })
}
for (let idx = 0; idx < CAP_SHORT; idx++) {
const c = idx % COLS
const r = Math.floor(idx / COLS)
const x = xs[li] + c * DX
const y = shortTop + r * DY
const itemIdx = shortScatter.get(idx)
const base = itemIdx !== undefined ? d.shorts[itemIdx] : undefined
layerCells.push({ base, x, y, dir: 'short', solid: !!base })
if (base) real.set(base, { x, y, dir: 'short' })
}
cells.push(layerCells)
realMaps.push(real)
})
return { prepared: prep, cellsByLayer: cells, realByLayer: realMaps, height: h, cy: centerY, colX: xs }
}, [layers])
const edges = useMemo(() => {
const out: { x1: number; y1: number; x2: number; y2: number; dir: string; key: string }[] = []
for (let l = 0; l < realByLayer.length - 1; l++) {
const right = realByLayer[l + 1]
if (right.size === 0) continue
realByLayer[l].forEach((a, base) => {
const b = right.get(base)
if (b) out.push({ x1: a.x, y1: a.y, x2: b.x, y2: b.y, dir: a.dir, key: `${l}-${base}` })
})
}
return out
}, [realByLayer])
// engine fans to real nodes in the first non-empty layer — balanced across
// long (top) and short (bottom) so both halves get dispatch lines/beams
const engineTargets = useMemo(() => {
const idx = realByLayer.findIndex((m) => m.size > 0)
if (idx < 0) return [] as { x: number; y: number; dir: 'long' | 'short' }[]
const all = [...realByLayer[idx].values()]
const longs = all.filter((n) => n.dir === 'long').slice(0, 24)
const shorts = all.filter((n) => n.dir === 'short').slice(0, 24)
return [...longs, ...shorts]
}, [realByLayer])
return (
<svg width="100%" viewBox={`0 0 ${VB_W} ${height}`} role="img"
aria-label="Decision funnel matrix: flow, signal, decision, execute, hold"
className={className} style={{ display: 'block' }}>
{prepared.map((d, li) => (
<g key={d.key} fontFamily="var(--tm-mono)">
<text x={colX[li] - 2} y={12} fontSize={8} fill="var(--tm-muted)" style={{ letterSpacing: '0.08em' }}>{d.title}</text>
<text x={colX[li] - 2} y={23} fontSize={8}>
<tspan fill="var(--tm-up)">L{d.longs.length}</tspan>
<tspan fill="var(--tm-muted)"> </tspan>
<tspan fill="var(--tm-dn)">S{d.shorts.length}</tspan>
</text>
</g>
))}
<g strokeWidth={0.6} strokeDasharray="2 3" opacity={0.4}>
{engineTargets.map((n, i) => (
<line key={i} x1={ENGINE_X} y1={cy} x2={n.x} y2={n.y} stroke={n.dir === 'short' ? SHORT : LONG} />
))}
</g>
<g strokeWidth={0.8} strokeDasharray="2 3" opacity={0.5}>
{edges.map((e) => (
<line key={e.key} x1={e.x1} y1={e.y1} x2={e.x2} y2={e.y2} stroke={e.dir === 'short' ? SHORT : LONG} />
))}
</g>
{engineTargets.map((n, i) => (
<circle key={`b0-${i}`} r={1.8} fill={n.dir === 'short' ? SHORT : LONG}>
<animate attributeName="cx" values={`${ENGINE_X};${n.x}`} dur={`${0.5 + (i % 5) * 0.08}s`} begin={`${(i % 8) * 0.07}s`} repeatCount="indefinite" />
<animate attributeName="cy" values={`${cy};${n.y}`} dur={`${0.5 + (i % 5) * 0.08}s`} begin={`${(i % 8) * 0.07}s`} repeatCount="indefinite" />
<animate attributeName="opacity" values="0.9;0.9;0" dur={`${0.5 + (i % 5) * 0.08}s`} begin={`${(i % 8) * 0.07}s`} repeatCount="indefinite" />
</circle>
))}
{edges.map((e, i) => (
<circle key={`be-${e.key}`} r={2.1} fill={e.dir === 'short' ? SHORT : LONG}>
<animate attributeName="cx" values={`${e.x1};${e.x2}`} dur={`${0.45 + (i % 4) * 0.08}s`} begin={`${(i % 6) * 0.06}s`} repeatCount="indefinite" />
<animate attributeName="cy" values={`${e.y1};${e.y2}`} dur={`${0.45 + (i % 4) * 0.08}s`} begin={`${(i % 6) * 0.06}s`} repeatCount="indefinite" />
<animate attributeName="opacity" values="1;1;0" dur={`${0.45 + (i % 4) * 0.08}s`} begin={`${(i % 6) * 0.06}s`} repeatCount="indefinite" />
</circle>
))}
{cellsByLayer.map((layer, li) =>
layer.map((cell, ci) => {
const color = cell.dir === 'short' ? SHORT : LONG
if (cell.solid) {
return <circle key={`${li}-${ci}`} cx={cell.x} cy={cell.y} r={2.7} fill={color} stroke={color} strokeWidth={0.5} />
}
return <circle key={`${li}-${ci}`} cx={cell.x} cy={cell.y} r={1.8} fill="none" stroke={color} strokeWidth={0.6} opacity={0.2} />
})
)}
<circle cx={ENGINE_X} cy={cy} r={18} fill="none" stroke="var(--tm-red)" strokeWidth={1.5}>
<animate attributeName="r" values="18;34" dur="2.2s" repeatCount="indefinite" />
<animate attributeName="opacity" values="0.6;0" dur="2.2s" repeatCount="indefinite" />
</circle>
<circle cx={ENGINE_X} cy={cy} r={18} fill="var(--tm-red)" />
<text x={ENGINE_X} y={cy + 3} textAnchor="middle" fontFamily="var(--tm-px)" fontSize={7} fill="var(--tm-paper)">NOFX</text>
</svg>
)
}
export default OrchestrationTopology

View File

@@ -0,0 +1,311 @@
import { useEffect, useMemo, useRef, useState } from 'react'
/**
* OrderBook renders a live L2 depth ladder for a single instrument, streamed
* directly from Hyperliquid's public WebSocket (`l2Book`). The app trades a
* Hyperliquid builder-deployed perp DEX named "xyz" for synthetic / equity
* markets (xyz:SP500, xyz:SKHX, …) and the main dex for crypto majors
* (BTC, ETH, …). We resolve which one a symbol belongs to from the xyz dex's
* `allMids` coin set, then subscribe to the matching `l2Book` feed.
*
* Real data only — no synthetic depth.
*/
const HL_INFO = 'https://api.hyperliquid.xyz/info'
const HL_WS = 'wss://api.hyperliquid.xyz/ws'
const DEPTH = 11 // levels per side
interface Level {
px: number
sz: number
}
interface BookState {
coin: string
bids: Level[]
asks: Level[]
}
function baseSymbol(raw: string): string {
return raw
.toUpperCase()
.replace(/^XYZ:/, '')
.replace(/(USDT|USDC|USD)$/, '')
}
// Resolve a base symbol to the Hyperliquid coin id. Members of the xyz dex get
// the "xyz:" prefix; everything else is treated as a main-dex coin.
function resolveCoin(base: string, xyzSet: Set<string>): string {
if (!base) return ''
return xyzSet.has(base) ? `xyz:${base}` : base
}
function fmtPx(px: number): string {
if (px >= 1000) return px.toLocaleString('en-US', { maximumFractionDigits: 1 })
if (px >= 1) return px.toLocaleString('en-US', { maximumFractionDigits: 3 })
return px.toLocaleString('en-US', { maximumFractionDigits: 5 })
}
function fmtSz(sz: number): string {
if (sz >= 1000) return `${(sz / 1000).toFixed(1)}k`
if (sz >= 1) return sz.toFixed(2)
return sz.toFixed(3)
}
interface OrderBookProps {
/** raw business symbol (e.g. position symbol or candidate coin) */
symbol: string
/** optional entry price to mark the user's position level on the ladder */
markPrice?: number
}
export function OrderBook({ symbol, markPrice }: OrderBookProps) {
const base = useMemo(() => baseSymbol(symbol || ''), [symbol])
const [xyzSet, setXyzSet] = useState<Set<string>>(new Set())
const [book, setBook] = useState<BookState | null>(null)
const [status, setStatus] = useState<'connecting' | 'live' | 'down'>('connecting')
// one-time: fetch the xyz dex coin universe so we can resolve symbols
useEffect(() => {
let alive = true
fetch(HL_INFO, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'allMids', dex: 'xyz' }),
})
.then((r) => r.json())
.then((mids: Record<string, string>) => {
if (!alive) return
const set = new Set<string>()
for (const k of Object.keys(mids || {})) set.add(k.replace(/^xyz:/, '').toUpperCase())
setXyzSet(set)
})
.catch(() => {
/* CORS / offline — fall back to main-dex resolution (empty set) */
})
return () => {
alive = false
}
}, [])
const coin = useMemo(() => resolveCoin(base, xyzSet), [base, xyzSet])
// live L2 stream
const pending = useRef<BookState | null>(null)
useEffect(() => {
if (!coin) return
let ws: WebSocket | null = null
let raf: number | null = null
let retry: ReturnType<typeof setTimeout> | null = null
let closed = false
const connect = () => {
setStatus('connecting')
ws = new WebSocket(HL_WS)
ws.onopen = () => {
ws?.send(JSON.stringify({ method: 'subscribe', subscription: { type: 'l2Book', coin } }))
}
ws.onmessage = (ev) => {
try {
const msg = JSON.parse(ev.data)
if (msg.channel !== 'l2Book' || !msg.data) return
const lv = msg.data.levels
if (!Array.isArray(lv) || lv.length < 2) return
const toLevels = (arr: { px: string; sz: string }[]): Level[] =>
arr.slice(0, DEPTH).map((l) => ({ px: parseFloat(l.px), sz: parseFloat(l.sz) }))
pending.current = { coin: msg.data.coin, bids: toLevels(lv[0]), asks: toLevels(lv[1]) }
setStatus('live')
} catch {
/* ignore malformed frame */
}
}
ws.onclose = () => {
if (closed) return
setStatus('down')
retry = setTimeout(connect, 1500)
}
ws.onerror = () => ws?.close()
}
connect()
// flush on every animation frame (~16ms) so individual rows tick the instant
// the venue pushes a change — millisecond-level, not a batched interval
const loop = () => {
if (pending.current) {
setBook(pending.current)
pending.current = null
}
raf = requestAnimationFrame(loop)
}
raf = requestAnimationFrame(loop)
return () => {
closed = true
if (raf) cancelAnimationFrame(raf)
if (retry) clearTimeout(retry)
try {
ws?.send(JSON.stringify({ method: 'unsubscribe', subscription: { type: 'l2Book', coin } }))
} catch {
/* socket already gone */
}
ws?.close()
}
}, [coin])
const view = useMemo(() => {
if (!book) return null
const asks = book.asks.slice(0, DEPTH)
const bids = book.bids.slice(0, DEPTH)
// cumulative depth for background bars
let ca = 0
const askRows = asks.map((l) => ({ ...l, cum: (ca += l.sz) }))
let cb = 0
const bidRows = bids.map((l) => ({ ...l, cum: (cb += l.sz) }))
const maxCum = Math.max(ca, cb, 1)
const bestAsk = asks[0]?.px ?? 0
const bestBid = bids[0]?.px ?? 0
const mid = bestAsk && bestBid ? (bestAsk + bestBid) / 2 : 0
const spread = bestAsk && bestBid ? bestAsk - bestBid : 0
const spreadBps = mid ? (spread / mid) * 10000 : 0
// buy/sell pressure across the visible book (by notional)
const bidVol = bidRows.reduce((s, l) => s + l.sz * l.px, 0)
const askVol = askRows.reduce((s, l) => s + l.sz * l.px, 0)
const bidPct = bidVol + askVol > 0 ? (bidVol / (bidVol + askVol)) * 100 : 50
// the single visible level nearest the user's entry — marked with ▸
let markLevel: number | undefined
if (markPrice) {
let bd = Infinity
for (const l of [...asks, ...bids]) {
const d = Math.abs(l.px - markPrice)
if (d < bd) {
bd = d
markLevel = l.px
}
}
}
return { askRows: askRows.reverse(), bidRows, maxCum, mid, spread, spreadBps, bidPct, markLevel }
}, [book, markPrice])
const rowH = 16
return (
<div style={{ fontFamily: 'var(--tm-mono)' }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 6 }}>
<span className="tm-px" style={{ fontSize: 11 }}>Order book</span>
<span className="tm-sc">L2 · {coin || base || '—'}</span>
<span
className="tm-sc"
style={{ marginLeft: 'auto', color: status === 'live' ? 'var(--tm-up)' : 'var(--tm-muted)' }}
>
{status === 'live' ? '● live' : status === 'connecting' ? '○ sync' : '○ down'}
</span>
</div>
{!view ? (
<div className="tm-sc" style={{ padding: '16px 0' }}>Connecting to Hyperliquid</div>
) : (
<div style={{ fontSize: 11 }}>
{/* column header */}
<div className="tm-sc" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 4, marginBottom: 2 }}>
<span>price</span>
<span style={{ textAlign: 'right' }}>size</span>
<span style={{ textAlign: 'right' }}>cum $</span>
</div>
{/* asks (red), best ask nearest the mid — keyed by PRICE so each level
keeps its identity and flashes independently when its size changes */}
{view.askRows.map((l) => (
<Row key={`a-${l.px}`} px={l.px} sz={l.sz} cum={l.cum} maxCum={view.maxCum} side="ask" h={rowH} mark={view.markLevel} />
))}
{/* mid / spread */}
<div
className="tm-mono"
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '3px 0',
margin: '2px 0',
borderTop: '1px solid var(--tm-hair)',
borderBottom: '1px solid var(--tm-hair)',
}}
>
<span className="tm-px" style={{ fontSize: 12, color: 'var(--tm-red)' }}>{fmtPx(view.mid)}</span>
<span className="tm-sc" style={{ marginLeft: 'auto' }}>spread {fmtPx(view.spread)} · {view.spreadBps.toFixed(1)}bps</span>
</div>
{/* bids (green) — keyed by price, same independent-flash behavior */}
{view.bidRows.map((l) => (
<Row key={`b-${l.px}`} px={l.px} sz={l.sz} cum={l.cum} maxCum={view.maxCum} side="bid" h={rowH} mark={view.markLevel} />
))}
{/* buy/sell pressure across the visible book */}
<div style={{ marginTop: 7 }}>
<div style={{ display: 'flex', height: 6 }}>
<div style={{ width: `${view.bidPct}%`, background: 'var(--tm-up)', transition: 'width 0.2s ease-out' }} />
<div style={{ flex: 1, background: 'var(--tm-dn)' }} />
</div>
<div className="tm-sc" style={{ display: 'flex', fontSize: 9, marginTop: 2 }}>
<span className="tm-up">B {view.bidPct.toFixed(1)}%</span>
<span style={{ marginLeft: 'auto' }} className="tm-dn">{(100 - view.bidPct).toFixed(1)}% S</span>
</div>
</div>
</div>
)}
</div>
)
}
interface RowProps {
px: number
sz: number
cum: number
maxCum: number
side: 'ask' | 'bid'
h: number
mark?: number
}
function fmtNotional(n: number): string {
if (n >= 1e9) return `$${(n / 1e9).toFixed(2)}B`
if (n >= 1e6) return `$${(n / 1e6).toFixed(2)}M`
if (n >= 1e3) return `$${(n / 1e3).toFixed(1)}K`
return `$${n.toFixed(0)}`
}
function Row({ px, sz, cum, maxCum, side, h, mark }: RowProps) {
const pct = Math.min(100, (cum / maxCum) * 100)
const color = side === 'ask' ? 'var(--tm-dn)' : 'var(--tm-up)'
// bold cumulative-depth bar, saturated toward the edge, that animates its
// width as the book updates (the live "growing ladder" effect)
const bar = side === 'ask'
? 'linear-gradient(to left, rgba(214,67,58,0.36), rgba(214,67,58,0.05))'
: 'linear-gradient(to left, rgba(46,139,87,0.36), rgba(46,139,87,0.05))'
const isMark = mark != null && px === mark
// this Row instance is keyed by price, so these refs persist across updates —
// we flash green/red only when THIS level's size actually changes, and keep
// the direction class fixed until the next change so the animation isn't cut
// short by the 60fps re-renders.
const prevSz = useRef(sz)
const dirRef = useRef('')
if (sz !== prevSz.current) {
dirRef.current = sz > prevSz.current ? 'ob-up' : 'ob-dn'
prevSz.current = sz
}
return (
<div style={{ position: 'relative', height: h, display: 'flex', alignItems: 'center' }}>
{/* per-row flash overlay — keyed by size so it remounts (replays the
animation) exactly when this level's size changes */}
<div key={sz} className={dirRef.current} style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }} />
<div style={{ position: 'absolute', right: 0, top: 0, bottom: 0, width: `${pct}%`, background: bar, transition: 'width 0.16s ease-out' }} />
<div style={{ position: 'relative', display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 4, width: '100%', alignItems: 'center' }}>
<span style={{ color, fontWeight: isMark ? 700 : 500 }}>
{isMark ? '▸ ' : ''}{fmtPx(px)}
</span>
<span style={{ textAlign: 'right', color: 'var(--tm-ink)' }}>{fmtSz(sz)}</span>
<span style={{ textAlign: 'right', color: 'var(--tm-muted)' }}>{fmtNotional(cum * px)}</span>
</div>
</div>
)
}
export default OrderBook

View File

@@ -0,0 +1,383 @@
import { useMemo } from 'react'
import type { Position } from '../../types'
/**
* RiskRadar renders derived risk telemetry for the live trading book — long /
* short exposure split, leverage usage vs config cap, margin utilization,
* single-name concentration, max drawdown and position count — as a dense stack
* of self-explanatory gauge rows. Each row reads as: bilingual label · value +
* unit · a thin gauge bar · and a one-glance verdict tag. Cream-themed
* Bloomberg/terminal cockpit.
*
* Every value is DERIVED from real props. No synthetic or random data; missing
* inputs collapse to 0 and divides are guarded.
*/
const C_AMBER = '#c8860b' // escalation tint between green and red
function fmtUsd(n: number): string {
const a = Math.abs(n)
const sign = n < 0 ? '-' : ''
if (a >= 1e6) return `${sign}$${(a / 1e6).toFixed(2)}M`
if (a >= 1e3) return `${sign}$${(a / 1e3).toFixed(1)}K`
return `${sign}$${a.toFixed(0)}`
}
function pct(n: number): string {
return `${n.toFixed(1)}%`
}
function isLong(side: string): boolean {
return (side || '').toLowerCase() === 'long'
}
// margin utilization escalates green → amber → red as the book fills up
function utilColor(p: number): string {
if (p > 80) return 'var(--tm-dn)'
if (p >= 50) return C_AMBER
return 'var(--tm-up)'
}
interface RiskRadarProps {
positions?: Position[]
account?: { total_equity?: number; total_unrealized_profit?: number; margin_used_pct?: number } | null
config?: { btc_eth_leverage?: number; altcoin_leverage?: number; max_positions?: number } | null
fullStats?: { max_drawdown_pct?: number; profit_factor?: number; sharpe_ratio?: number; win_rate?: number } | null
}
export function RiskRadar({ positions, account, config, fullStats }: RiskRadarProps) {
const pos = positions ?? []
const m = useMemo(() => {
const equity = account?.total_equity ?? 0
let longNotional = 0
let shortNotional = 0
let levSum = 0
let levCount = 0
let maxLev = 0
let marginSum = 0
let topNotional = 0
for (const p of pos) {
const px = p.mark_price || p.entry_price || 0
const notional = Math.abs(p.quantity || 0) * px
if (isLong(p.side)) longNotional += notional
else shortNotional += notional
const lev = p.leverage || 0
if (lev > 0) {
levSum += lev
levCount += 1
if (lev > maxLev) maxLev = lev
}
marginSum += p.margin_used || 0
if (notional > topNotional) topNotional = notional
}
const totalNotional = longNotional + shortNotional
const netNotional = longNotional - shortNotional
const longShare = totalNotional > 0 ? (longNotional / totalNotional) * 100 : 0
const shortShare = totalNotional > 0 ? (shortNotional / totalNotional) * 100 : 0
const avgLev = levCount > 0 ? levSum / levCount : 0
const configMax = Math.max(config?.btc_eth_leverage ?? 0, config?.altcoin_leverage ?? 0)
const levUse = configMax > 0 ? Math.min(100, (avgLev / configMax) * 100) : 0
const marginPct =
account?.margin_used_pct != null
? account.margin_used_pct
: equity > 0
? (marginSum / equity) * 100
: 0
const concentration = totalNotional > 0 ? (topNotional / totalNotional) * 100 : 0
const ddFrac = fullStats?.max_drawdown_pct ?? 0
const drawdown = ddFrac * 100
const count = pos.length
const maxPositions = config?.max_positions ?? 0
const countUse = maxPositions > 0 ? Math.min(100, (count / maxPositions) * 100) : 0
const upnl = account?.total_unrealized_profit ?? 0
return {
longNotional,
shortNotional,
netNotional,
longShare,
shortShare,
totalNotional,
avgLev,
maxLev,
configMax,
levUse,
marginPct,
concentration,
drawdown,
count,
maxPositions,
countUse,
upnl,
}
}, [pos, account, config, fullStats])
const hasData = pos.length > 0 || account != null
if (!hasData) {
return <div className="tm-sc" style={{ padding: '16px 0' }}>No live risk data.</div>
}
// ── one-glance verdicts ──────────────────────────────────────────────
// Net exposure bias: Long-lean / Short-lean / Balanced by the long-share spread around 50%.
const biasSkew = m.longShare - m.shortShare
const exposureTag: Verdict =
m.totalNotional === 0
? { text: 'Flat', tone: 'muted' }
: biasSkew > 15
? { text: 'Long-lean', tone: 'up' }
: biasSkew < -15
? { text: 'Short-lean', tone: 'dn' }
: { text: 'Balanced', tone: 'ink' }
// Leverage: Safe / High / Risky by avg vs cap.
const levTag: Verdict =
m.configMax === 0 || m.avgLev === 0
? { text: '—', tone: 'muted' }
: m.levUse > 80
? { text: 'Risky', tone: 'dn' }
: m.levUse >= 50
? { text: 'High', tone: 'amber' }
: { text: 'Safe', tone: 'up' }
// Margin used: Ample / Tight / Risky.
const marginTag: Verdict =
m.marginPct > 80
? { text: 'Risky', tone: 'dn' }
: m.marginPct >= 50
? { text: 'Tight', tone: 'amber' }
: { text: 'Ample', tone: 'up' }
// Concentration: Spread / Concentrated.
const concTag: Verdict =
m.totalNotional === 0
? { text: '—', tone: 'muted' }
: m.concentration >= 35
? { text: 'Concentrated', tone: 'amber' }
: { text: 'Spread', tone: 'up' }
// Drawdown: Calm / Caution / Deep by depth.
const ddTag: Verdict =
m.drawdown <= 0
? { text: 'Calm', tone: 'up' }
: m.drawdown >= 20
? { text: 'Deep', tone: 'dn' }
: { text: 'Caution', tone: 'amber' }
// Positions: Room / Full.
const countTag: Verdict =
m.maxPositions === 0
? { text: `${m.count}`, tone: 'muted' }
: m.count >= m.maxPositions
? { text: 'Full', tone: 'amber' }
: { text: 'Room', tone: 'up' }
return (
<div style={{ fontFamily: 'var(--tm-mono)' }}>
{/* header */}
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 1 }}>
<span className="tm-px" style={{ fontSize: 11 }}>Risk radar</span>
<span
className="tm-sc"
style={{ marginLeft: 'auto', color: m.totalNotional > 0 ? 'var(--tm-up)' : 'var(--tm-muted)' }}
>
{m.totalNotional > 0 ? '● live' : '○ flat'}
</span>
</div>
<div className="tm-sc" style={{ fontSize: 9, marginBottom: 8 }}>
Risk radar · live position-risk check
</div>
{/* Net exposure — diverging long/short split, the visual centerpiece */}
<div style={{ marginBottom: 9, paddingBottom: 9, borderBottom: '1px solid var(--tm-hair)' }}>
<div style={{ display: 'flex', alignItems: 'baseline', marginBottom: 4 }}>
<Label zh="Net exposure" en="NET EXPOSURE" />
<Tag verdict={exposureTag} />
<span className="tm-mono" style={{ marginLeft: 'auto', fontSize: 11, color: 'var(--tm-ink)' }}>
long {pct(m.longShare)}
<span style={{ color: 'var(--tm-muted)' }}> / </span>
short {pct(m.shortShare)}
</span>
</div>
<div style={{ display: 'flex', height: 7, background: 'var(--tm-hair)', overflow: 'hidden' }}>
<div style={{ width: `${m.longShare}%`, background: 'var(--tm-up)' }} />
<div style={{ width: `${m.shortShare}%`, background: 'var(--tm-dn)' }} />
</div>
<div className="tm-mono" style={{ display: 'flex', justifyContent: 'space-between', fontSize: 9, marginTop: 3 }}>
<span style={{ color: 'var(--tm-up)' }}>long {fmtUsd(m.longNotional)}</span>
<span style={{ color: 'var(--tm-ink-2)' }}>
net <b style={{ color: m.netNotional >= 0 ? 'var(--tm-up)' : 'var(--tm-dn)' }}>{fmtUsd(m.netNotional)}</b>
</span>
<span style={{ color: 'var(--tm-dn)' }}>short {fmtUsd(m.shortNotional)}</span>
</div>
</div>
{/* gauge rows */}
<GaugeRow
zh="Leverage"
en="LEVERAGE"
value={`${m.avgLev.toFixed(1)}× avg`}
sub={`/ ${m.maxLev > 0 ? `${m.maxLev.toFixed(0)}×` : '—'} peak · ${m.configMax > 0 ? `${m.configMax}×` : '—'} cap`}
fill={m.levUse}
color={levTag.tone === 'dn' ? 'var(--tm-dn)' : levTag.tone === 'amber' ? C_AMBER : 'var(--tm-up)'}
verdict={levTag}
/>
<GaugeRow
zh="Margin used"
en="MARGIN USED"
value={pct(m.marginPct)}
sub="of equity"
fill={Math.min(100, Math.max(0, m.marginPct))}
color={utilColor(m.marginPct)}
verdict={marginTag}
/>
<GaugeRow
zh="Concentration"
en="CONCENTRATION"
value={pct(m.concentration)}
sub="top-position share"
fill={m.concentration}
color={concTag.tone === 'amber' ? C_AMBER : 'var(--tm-up)'}
verdict={concTag}
/>
<GaugeRow
zh="Drawdown"
en="MAX DRAWDOWN"
value={`-${pct(m.drawdown)}`}
sub="peak drawdown"
fill={Math.min(100, m.drawdown)}
color="var(--tm-red)"
verdict={ddTag}
valueColor="var(--tm-dn)"
/>
<GaugeRow
zh="Positions"
en="POSITIONS"
value={m.maxPositions > 0 ? `${m.count} / ${m.maxPositions}` : `${m.count}`}
sub="held / cap"
fill={m.maxPositions > 0 ? m.countUse : 0}
color={countTag.tone === 'amber' ? C_AMBER : 'var(--tm-up)'}
verdict={countTag}
/>
{/* unrealized PnL footer */}
<div
style={{
display: 'flex',
alignItems: 'baseline',
marginTop: 8,
paddingTop: 7,
borderTop: '1px solid var(--tm-hair)',
}}
>
<Label zh="Unrealized PnL" en="UNREALIZED PNL" />
<span
className="tm-mono"
style={{ marginLeft: 'auto', fontSize: 13, fontWeight: 700, color: m.upnl >= 0 ? 'var(--tm-up)' : 'var(--tm-dn)' }}
>
{m.upnl >= 0 ? '+' : ''}{fmtUsd(m.upnl)}
</span>
</div>
</div>
)
}
// ── verdict tag ────────────────────────────────────────────────────────
type Tone = 'up' | 'dn' | 'amber' | 'ink' | 'muted'
interface Verdict {
text: string
tone: Tone
}
function toneColor(tone: Tone): string {
switch (tone) {
case 'up':
return 'var(--tm-up)'
case 'dn':
return 'var(--tm-dn)'
case 'amber':
return C_AMBER
case 'ink':
return 'var(--tm-ink)'
default:
return 'var(--tm-muted)'
}
}
function Tag({ verdict }: { verdict: Verdict }) {
const c = toneColor(verdict.tone)
return (
<span
style={{
marginLeft: 6,
padding: '0 4px',
fontSize: 9,
lineHeight: '13px',
letterSpacing: '0.08em',
color: c,
border: `1px solid ${c}`,
borderRadius: 2,
}}
>
{verdict.text}
</span>
)
}
// ── bilingual label block ──────────────────────────────────────────────
function Label({ zh, en }: { zh: string; en: string }) {
return (
<span style={{ display: 'inline-flex', flexDirection: 'column', lineHeight: 1.2 }}>
<span style={{ fontSize: 11, color: 'var(--tm-ink)', fontWeight: 600 }}>{zh}</span>
<span className="tm-sc" style={{ fontSize: 8, letterSpacing: '0.12em' }}>{en}</span>
</span>
)
}
interface GaugeRowProps {
zh: string
en: string
value: string
sub?: string
fill: number
color: string
verdict: Verdict
valueColor?: string
}
function GaugeRow({ zh, en, value, sub, fill, color, verdict, valueColor }: GaugeRowProps) {
const w = Math.min(100, Math.max(0, fill))
return (
<div style={{ marginBottom: 9 }}>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 4 }}>
<Label zh={zh} en={en} />
<Tag verdict={verdict} />
<span
className="tm-mono"
style={{ marginLeft: 'auto', fontSize: 12, fontWeight: 600, color: valueColor ?? 'var(--tm-ink)' }}
>
{value}
</span>
</div>
<div style={{ height: 5, background: 'var(--tm-hair)', overflow: 'hidden' }}>
<div style={{ width: `${w}%`, height: '100%', background: color, transition: 'width 0.2s ease-out' }} />
</div>
{sub && (
<div className="tm-sc" style={{ fontSize: 8, marginTop: 2 }}>{sub}</div>
)}
</div>
)
}
export default RiskRadar

View File

@@ -0,0 +1,192 @@
import { useMemo } from 'react'
import type { SignalRankItem } from '../../lib/api/data'
/**
* SignalMatrix renders the vergex (claw402) signal ranking as a high-density
* heatmap grid — one cell per symbol, colored by directional bias (green =
* bullish, red = bearish, muted = neutral) with intensity scaled by the signal
* score. Rank 1 is the strongest signal. Cream-themed Bloomberg/terminal feel.
*
* Real ranked data only — no synthetic signals.
*/
function baseSymbol(raw: string): string {
return raw
.toUpperCase()
.replace(/^XYZ:/, '')
.replace(/(USDT|USDC|USD)$/, '')
}
type Bias = 'bullish' | 'bearish' | 'neutral'
function normBias(raw: string): Bias {
const b = (raw || '').toLowerCase()
if (b === 'bullish') return 'bullish'
if (b === 'bearish') return 'bearish'
return 'neutral'
}
// solid bias color for the left accent + swatches
const ACCENT: Record<Bias, string> = {
bullish: 'var(--tm-up)',
bearish: 'var(--tm-dn)',
neutral: 'var(--tm-muted)',
}
// background wash, intensity-scaled (alpha 0.10 → 0.42)
function washFor(bias: Bias, intensity: number): string {
const a = 0.1 + Math.max(0, Math.min(1, intensity)) * 0.32
if (bias === 'bullish') return `rgba(46,139,87,${a.toFixed(3)})`
if (bias === 'bearish') return `rgba(214,67,58,${a.toFixed(3)})`
return 'rgba(138,132,120,0.10)'
}
interface Cell {
rank: number
symbol: string
bias: Bias
score: number
intensity: number
}
interface SignalMatrixProps {
items?: SignalRankItem[]
max?: number
/** currently-selected base symbol (drives the liq map + order book) */
active?: string
/** click a cell to switch the active instrument */
onSelect?: (symbol: string) => void
}
export function SignalMatrix({ items, max = 36, active, onSelect }: SignalMatrixProps) {
const view = useMemo(() => {
const raw = items ?? []
const sorted = [...raw].sort((a, b) => a.rank - b.rank).slice(0, max)
if (!sorted.length) return { cells: [] as Cell[], bull: 0, bear: 0, neut: 0 }
const scores = sorted.map((s) => s.score)
const min = Math.min(...scores)
const span = Math.max(...scores) - min
const n = sorted.length
let bull = 0
let bear = 0
let neut = 0
const cells: Cell[] = sorted.map((s, i) => {
const bias = normBias(s.bias)
if (bias === 'bullish') bull += 1
else if (bias === 'bearish') bear += 1
else neut += 1
// intensity by normalized score; if scores are uniform, fall back to rank
// (top ranks brighter, descending across the slice).
const intensity = span > 0 ? (s.score - min) / span : n > 1 ? 1 - i / (n - 1) : 1
return { rank: s.rank, symbol: s.symbol, bias, score: s.score, intensity }
})
return { cells, bull, bear, neut }
}, [items, max])
if (!view.cells.length) {
return (
<div style={{ fontFamily: 'var(--tm-mono)' }}>
<Head />
<div className="tm-sc">No signal data (claw402).</div>
</div>
)
}
return (
<div style={{ fontFamily: 'var(--tm-mono)' }}>
<Head />
{/* legend */}
<div
className="tm-sc"
style={{ display: 'flex', flexWrap: 'wrap', gap: 10, marginBottom: 6, fontSize: 9 }}
>
<Swatch c="var(--tm-up)" label="Bullish" />
<Swatch c="var(--tm-dn)" label="Bearish" />
<Swatch c="var(--tm-muted)" label="Neutral" />
{onSelect && <span style={{ color: 'var(--tm-red)' }}>click to switch </span>}
<span style={{ marginLeft: 'auto' }}>{view.cells.length} signals</span>
</div>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(64px, 1fr))',
gap: 3,
}}
>
{view.cells.map((c) => {
const base = baseSymbol(c.symbol)
const isActive = !!active && base === active.toUpperCase()
return (
<div
key={`${c.rank}-${c.symbol}`}
title={`${base} · #${c.rank} · ${c.bias} · ${c.score} — click to switch`}
onClick={onSelect ? () => onSelect(base) : undefined}
style={{
padding: '4px 5px',
background: washFor(c.bias, c.intensity),
border: isActive ? '1px solid var(--tm-red)' : '1px solid var(--tm-hair)',
borderLeft: `2px solid ${ACCENT[c.bias]}`,
outline: isActive ? '1px solid var(--tm-red)' : 'none',
boxShadow: isActive ? 'inset 0 0 0 1px var(--tm-red)' : 'none',
lineHeight: 1.15,
overflow: 'hidden',
cursor: onSelect ? 'pointer' : 'default',
}}
>
<div
className="tm-mono"
style={{
fontSize: 10,
fontWeight: 700,
color: 'var(--tm-ink)',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{baseSymbol(c.symbol)}
</div>
<div
className="tm-sc"
style={{ fontSize: 8, letterSpacing: '0.08em', marginTop: 1 }}
>
#{c.rank} · {fmtScore(c.score)}
</div>
</div>
)
})}
</div>
</div>
)
}
function fmtScore(n: number): string {
if (!Number.isFinite(n)) return '—'
if (Math.abs(n) >= 100) return n.toFixed(0)
if (Math.abs(n) >= 10) return n.toFixed(1)
return n.toFixed(2)
}
function Head() {
return (
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 6 }}>
<span className="tm-px" style={{ fontSize: 11 }}>Signal matrix</span>
<span className="tm-sc">Signal matrix · vergex</span>
</div>
)
}
function Swatch({ c, label }: { c: string; label: string }) {
return (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 3 }}>
<span style={{ width: 8, height: 8, background: c, display: 'inline-block' }} />
{label}
</span>
)
}
export default SignalMatrix

View File

@@ -0,0 +1,566 @@
import { useEffect, useMemo, useState } from 'react'
import { createPortal } from 'react-dom'
import type { CSSProperties } from 'react'
import useSWR from 'swr'
import { api } from '../../lib/api'
import type {
SystemStatus,
AccountInfo,
Position,
DecisionRecord,
TraderInfo,
} from '../../types'
import { OrchestrationTopology } from './OrchestrationTopology'
import { OrderBook } from './OrderBook'
import { LiquidationMap } from './LiquidationMap'
import { KlineChart } from './KlineChart'
import { ExecutionLog } from './ExecutionLog'
import { SignalMatrix } from './SignalMatrix'
import { RiskRadar } from './RiskRadar'
// crypto majors trade on the Hyperliquid main dex (no hip3 cost/liq heatmap);
// everything else in the universe is an xyz-dex synthetic market that does.
const CRYPTO_MAJORS = new Set([
'BTC', 'ETH', 'SOL', 'HYPE', 'BNB', 'XRP', 'DOGE', 'AVAX', 'LINK', 'SUI', 'APT', 'ARB', 'OP',
'TON', 'ADA', 'TRX', 'LTC', 'BCH', 'NEAR', 'INJ', 'SEI', 'TIA', 'PEPE', 'WIF', 'BONK', 'AAVE',
'UNI', 'ENA', 'ONDO', 'JUP', 'PENDLE', 'KPEPE', 'ZEC', 'XPL', 'LIT',
])
// fixed height for the three row-1 panels so the row stays balanced at any width
const ROW1_H = 500
import { FlowMarkets } from './FlowMarkets'
import './terminal.css'
interface TerminalDashboardProps {
selectedTrader?: TraderInfo
traders?: TraderInfo[]
selectedTraderId?: string
onTraderSelect: (traderId: string) => void
status?: SystemStatus
account?: AccountInfo
positions?: Position[]
decisions?: DecisionRecord[]
}
function fmtUsd(n: number | undefined, signed = false): string {
if (n == null || Number.isNaN(n)) return '—'
const sign = signed && n > 0 ? '+' : n < 0 ? '-' : ''
return `${sign}$${Math.abs(n).toLocaleString('en-US', { maximumFractionDigits: 2 })}`
}
function fmtPct(n: number | undefined): string {
if (n == null || Number.isNaN(n)) return '—'
return `${n >= 0 ? '+' : ''}${n.toFixed(2)}%`
}
function baseLabel(raw?: string): string {
if (!raw) return ''
return raw.toUpperCase().replace(/^XYZ:/, '').replace(/[-_]/g, '').replace(/(USDT|USDC|USD)$/, '')
}
function parseScanMinutes(scan?: string): number {
if (!scan) return 15
const m = scan.match(/(\d+)\s*m/i)
if (m) return parseInt(m[1], 10)
const h = scan.match(/(\d+)\s*h/i)
if (h) return parseInt(h[1], 10) * 60
const n = parseInt(scan, 10)
return Number.isFinite(n) && n > 0 ? n : 15
}
function fmtTime(raw?: string | number): string {
if (raw == null || raw === '') return ''
let n = typeof raw === 'number' ? raw : Number(raw)
if (Number.isFinite(n)) {
if (n < 1e12) n *= 1000
return new Date(n).toLocaleString('en-GB', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false })
}
const d = new Date(raw as string)
return Number.isNaN(d.getTime()) ? '' : d.toLocaleString('en-GB', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false })
}
function useTick(ms = 1000) {
const [, set] = useState(0)
useEffect(() => {
const id = setInterval(() => set((n) => n + 1), ms)
return () => clearInterval(id)
}, [ms])
}
export function TerminalDashboard({
selectedTrader,
traders,
selectedTraderId,
onTraderSelect,
status,
account,
positions,
decisions,
}: TerminalDashboardProps) {
const traderId = selectedTrader?.trader_id || selectedTraderId
useTick(1000)
const clock = new Date().toLocaleTimeString('en-GB', { hour12: false })
const { data: fullStats } = useSWR(
traderId ? ['full-stats', traderId] : null,
() => api.getFullStats(traderId!, true),
{ refreshInterval: 30000, shouldRetryOnError: false }
)
const { data: equity } = useSWR(
traderId ? ['equity-history', traderId] : null,
() => api.getEquityHistory(traderId!, true),
{ refreshInterval: 30000, shouldRetryOnError: false }
)
const { data: history } = useSWR(
traderId ? ['pos-history', traderId] : null,
() => api.getPositionHistory(traderId!, 50, true),
{ refreshInterval: 60000, shouldRetryOnError: false }
)
const { data: config } = useSWR(
traderId ? ['trader-config', traderId] : null,
() => api.getTraderConfig(traderId!, true),
{ refreshInterval: 120000, shouldRetryOnError: false }
)
const latest = decisions && decisions.length > 0 ? decisions[0] : undefined
const candidateCoins = latest?.candidate_coins ?? []
const { data: flow } = useSWR(
traderId ? ['flow-markets', traderId] : null,
() => api.getFlowMarkets(selectedTrader?.ai_model, 'mainnet', '1h', 50, true),
// paid x402 endpoint — poll slowly (5m) to conserve claw402 funds; the
// topology beam animation is client-side and stays fast regardless
{ refreshInterval: 300000, shouldRetryOnError: false }
)
const flowItems = flow?.data?.inflow ?? []
const { data: signalRank } = useSWR(
traderId ? ['signal-rank', traderId] : null,
() => api.getSignalRanking(selectedTrader?.ai_model, 'mainnet', 'all', 30, true),
// paid x402 endpoint — poll slowly (5m) to conserve claw402 funds
{ refreshInterval: 300000, shouldRetryOnError: false }
)
// Both the cost/liq map and the order book follow this symbol so they stay in
// sync. The heatmap only covers hip3_perp synthetic markets, so we pick a
// synthetic (non-crypto) the bot trades — preferring the BUSIEST one (most
// 1h trades, per flow-markets) so the shared order book ticks as fast as
// possible. Falls back to any held synthetic, then SP500.
const heatmapSymbol = useMemo(() => {
const held = new Set(
[...(positions ?? []).map((p) => p.symbol), ...candidateCoins]
.map(baseLabel)
.filter((b) => b && !CRYPTO_MAJORS.has(b)),
)
const synthByActivity = flowItems
.map((i) => ({ b: baseLabel(i.symbol), trades: i.trades || 0 }))
.filter((x) => x.b && !CRYPTO_MAJORS.has(x.b))
.sort((a, b) => b.trades - a.trades)
const busiestHeld = synthByActivity.find((x) => held.has(x.b))
if (busiestHeld) return busiestHeld.b
if (held.size) return [...held][0]
if (synthByActivity.length) return synthByActivity[0].b
return 'SP500'
}, [positions, candidateCoins, flowItems])
// user can click a signal-matrix cell to drive both the cost/liq map and the
// order book. Default to the instrument the bot is ACTUALLY holding (first
// open position, else this cycle's first candidate) so the price panels match
// the real traded symbol; only fall back to the busiest synthetic if the bot
// holds nothing.
const [selectedSym, setSelectedSym] = useState<string | null>(null)
const defaultSym = useMemo(() => {
// the bot's actual first open position (else this cycle's first candidate);
// every market — synthetic or crypto — now has a cost/liq heatmap, so no
// need to prefer one type. Falls back to the busiest synthetic if flat.
const heldBases = [...(positions ?? []).map((p) => p.symbol), ...candidateCoins].map(baseLabel).filter(Boolean)
return heldBases[0] || heatmapSymbol || 'SP500'
}, [positions, candidateCoins, heatmapSymbol])
const activeSym = (selectedSym || defaultSym).toUpperCase()
const pnl = account?.total_pnl ?? 0
const pnlPct = account?.total_pnl_pct ?? 0
const up = pnl >= 0
const running = status?.is_running
// direction per symbol — priority: AI's actual decision > signal bias >
// net flow > prevailing market majority (never blindly default to long).
const dirFor = useMemo(() => {
const dec = new Map<string, 'long' | 'short'>()
;(latest?.decisions ?? []).forEach((d) => {
const b = baseLabel(d.symbol)
if (d.action === 'open_long' || d.action === 'close_short') dec.set(b, 'long')
else if (d.action === 'open_short' || d.action === 'close_long') dec.set(b, 'short')
})
const sig = new Map<string, 'long' | 'short'>()
let bull = 0
let bear = 0
;(signalRank?.items ?? []).forEach((s) => {
const b = baseLabel(s.symbol)
const bias = (s.bias || '').toLowerCase()
if (bias === 'bearish') { sig.set(b, 'short'); bear++ }
else if (bias === 'bullish') { sig.set(b, 'long'); bull++ }
})
const fl = new Map<string, 'long' | 'short'>()
;(flow?.data?.inflow ?? []).forEach((i) => fl.set(baseLabel(i.symbol), 'long'))
;(flow?.data?.outflow ?? []).forEach((i) => fl.set(baseLabel(i.symbol), 'short'))
const majority: 'long' | 'short' = bear > bull ? 'short' : 'long'
return (sym: string): 'long' | 'short' => {
const b = baseLabel(sym)
return dec.get(b) ?? sig.get(b) ?? fl.get(b) ?? majority
}
}, [latest, signalRank, flow])
const scanMin = config?.scan_interval_minutes || parseScanMinutes(status?.scan_interval)
const nextCycleMs = useMemo(() => {
if (!latest?.timestamp) return null
return new Date(latest.timestamp).getTime() + scanMin * 60_000
}, [latest?.timestamp, scanMin])
let countdown = '—'
if (nextCycleMs) {
const ms = nextCycleMs - Date.now()
if (ms <= 0) countdown = 'due now'
else {
const s = Math.floor(ms / 1000)
countdown = `${Math.floor(s / 60)}m ${s % 60}s`
}
}
const equityPath = useMemo(() => {
if (!equity || equity.length < 2) return null
const vals = equity.map((e: { total_equity: number }) => e.total_equity)
const min = Math.min(...vals)
const max = Math.max(...vals)
const span = max - min || 1
const W = 680
const H = 80
return vals
.map((v: number, i: number) => {
const x = (i / (vals.length - 1)) * W
const y = H - ((v - min) / span) * (H - 10) - 5
return `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`
})
.join(' ')
}, [equity])
const recentTrades = (history?.positions ?? []).slice(0, 8)
const symbolStats = useMemo(
() => (history?.symbol_stats ?? []).slice().sort((a, b) => b.total_trades - a.total_trades).slice(0, 6),
[history]
)
const maxSymTrades = symbolStats.reduce((m, s) => Math.max(m, s.total_trades), 1)
const sc: CSSProperties = { padding: '10px 14px' }
const cellBorder = '1px solid var(--tm-hair)'
// Portal the trader selector + run status into the global nav so the app has
// a single top bar (no separate dashboard titlebar).
const [navSlot, setNavSlot] = useState<HTMLElement | null>(null)
useEffect(() => {
setNavSlot(document.getElementById('dash-header-slot'))
}, [])
return (
<div className="nofx-terminal" style={{ minHeight: '100vh', padding: 0 }}>
{/* centered, capped content column — no border (keeps it from feeling
embedded) but bounded so the aspect-ratio SVGs don't balloon on wide screens */}
{navSlot &&
createPortal(
<span className="nofx-terminal" style={{ background: 'transparent', display: 'flex', alignItems: 'center', gap: 12, marginLeft: 16, paddingLeft: 16, borderLeft: '1px solid rgba(26,24,19,0.15)', fontSize: 11 }}>
<span className="tm-sc" style={{ color: 'var(--tm-muted)' }}>orchestration</span>
{traders && traders.length > 0 && (
<select value={traderId || ''} onChange={(e) => onTraderSelect(e.target.value)} className="tm-mono"
style={{ background: 'var(--tm-panel)', color: 'var(--tm-ink)', border: '1px solid var(--tm-hair)', borderRadius: 0, fontSize: 11, padding: '3px 6px' }}>
{traders.map((t) => (<option key={t.trader_id} value={t.trader_id} style={{ color: '#111' }}>{t.trader_name}</option>))}
</select>
)}
<span style={{ color: running ? 'var(--tm-up)' : 'var(--tm-muted)' }}>{running ? '● running' : '○ stopped'}</span>
<span className="tm-sc" style={{ color: 'var(--tm-muted)' }}>cycle</span><span className="tm-mono" style={{ color: 'var(--tm-ink)' }}>{status?.call_count ?? '—'}</span>
<span className="tm-px" style={{ fontSize: 12, color: 'var(--tm-ink)' }}>{clock}</span>
</span>,
navSlot,
)}
<div className="tm-box" style={{ maxWidth: 1280, margin: '0 auto', border: 'none' }}>
{/* config / identity strip — first row, flows directly under the global nav */}
<div className="tm-mono" style={{ display: 'flex', gap: 16, padding: '6px 14px', fontSize: 11, color: 'var(--tm-ink-2)', flexWrap: 'wrap' }}>
<span style={{ fontWeight: 500 }}>{selectedTrader?.trader_name ?? 'NOFX'}</span>
<span><span className="tm-sc">model </span>{(() => {
const raw = config?.ai_model || status?.ai_model || ''
if (!raw) return '—'
if (/claw402/i.test(raw)) return 'CLAW402'
return raw.length > 16 ? raw.slice(0, 16).toUpperCase() : raw.toUpperCase()
})()}</span>
<span><span className="tm-sc">strategy </span>{config?.strategy_name || selectedTrader?.strategy_name || '—'}</span>
<span><span className="tm-sc">lev </span>{config?.btc_eth_leverage ?? '—'}× / {config?.altcoin_leverage ?? '—'}×</span>
<span><span className="tm-sc">scan </span>{scanMin}m</span>
<span><span className="tm-sc">universe </span>{candidateCoins.length}</span>
<span><span className="tm-sc">positions </span>{positions?.length ?? 0}</span>
<span style={{ marginLeft: 'auto' }}><span className="tm-sc">next cycle </span>{countdown}</span>
</div>
<div className="tm-rule" />
{/* metric row */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)' }}>
{[
{ l: 'Equity', v: fmtUsd(account?.total_equity), c: 'var(--tm-ink)' },
{ l: 'Total P/L', v: `${fmtUsd(pnl, true)} (${fmtPct(pnlPct)})`, c: up ? 'var(--tm-up)' : 'var(--tm-dn)' },
{ l: 'Win rate', v: fullStats != null ? `${fullStats.win_rate.toFixed(1)}%` : '—', c: 'var(--tm-ink)' },
{ l: 'Profit factor', v: fullStats != null ? fullStats.profit_factor.toFixed(2) : '—', c: 'var(--tm-ink)' },
{ l: 'Max drawdown', v: fullStats != null ? `-${(fullStats.max_drawdown_pct * 100).toFixed(1)}%` : '—', c: 'var(--tm-dn)' },
].map((m, i) => (
<div key={m.l} style={{ padding: '12px 14px', borderRight: i < 4 ? cellBorder : 'none' }}>
<div className="tm-sc">{m.l}</div>
<div className="tm-mono" style={{ fontSize: 17, fontWeight: 500, color: m.c, marginTop: 3 }}>{m.v}</div>
</div>
))}
</div>
<div className="tm-rule" />
{/* trades summary */}
{fullStats != null && (
<>
<div className="tm-mono" style={{ display: 'flex', gap: 18, padding: '6px 14px', fontSize: 11, color: 'var(--tm-ink-2)', flexWrap: 'wrap' }}>
<span className="tm-sc">trades <b style={{ color: 'var(--tm-ink)' }}>{fullStats.total_trades}</b></span>
<span className="tm-sc tm-up">win {fullStats.win_trades}</span>
<span className="tm-sc tm-dn">loss {fullStats.loss_trades}</span>
<span className="tm-sc">sharpe <b style={{ color: 'var(--tm-ink)' }}>{fullStats.sharpe_ratio.toFixed(2)}</b></span>
<span className="tm-sc">avg win/loss <b style={{ color: 'var(--tm-ink)' }}>{fullStats.avg_win.toFixed(2)}/{fullStats.avg_loss.toFixed(2)}</b></span>
<span className="tm-sc">fees <b style={{ color: 'var(--tm-ink)' }}>{fmtUsd(fullStats.total_fee)}</b></span>
</div>
<div className="tm-rule" />
</>
)}
{/* ── row 1: cost/liq map · live L2 order book · signal matrix (instrument selector)
all three columns are locked to one fixed height so the row is always
balanced; the K-line flexes to fill any remaining space ── */}
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0,1.1fr) minmax(0,0.95fr) minmax(0,1.05fr)' }}>
<div style={{ ...sc, borderRight: cellBorder, height: ROW1_H, overflow: 'hidden' }}>
{/* cost/liq heatmap works for both synthetic (hip3_perp) and crypto
(perp) markets — pass the likely marketType; the component falls
back to the other one if the guess is wrong */}
<LiquidationMap
symbol={activeSym}
marketType={CRYPTO_MAJORS.has(activeSym) ? 'perp' : 'hip3_perp'}
height={ROW1_H - 130}
/>
</div>
<div style={{ ...sc, borderRight: cellBorder, height: ROW1_H, overflow: 'hidden' }}>
<OrderBook symbol={activeSym} markPrice={positions?.find((p) => baseLabel(p.symbol) === activeSym)?.entry_price} />
</div>
<div style={{ ...sc, height: ROW1_H, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
<SignalMatrix items={signalRank?.items} active={activeSym} onSelect={setSelectedSym} />
{/* the live K-line always sits under the selector and flexes to fill */}
<div className="tm-rule" style={{ margin: '10px 0 8px' }} />
<div style={{ flex: 1, minHeight: 0 }}>
<KlineChart symbol={activeSym} fill />
</div>
</div>
</div>
<div className="tm-rule" />
{/* orchestration topology — second row, full width (the agent workflow) */}
<div style={sc}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10, marginBottom: 4 }}>
<span className="tm-px" style={{ fontSize: 12 }}>Orchestration topology</span>
<span className="tm-sc">Orchestration topology · net inflow signal execute hold</span>
</div>
<OrchestrationTopology
layers={[
{
key: 'flow',
title: 'FLOW',
zh: 'flow',
items: [
...(flow?.data?.inflow ?? []).map((i) => ({ symbol: i.symbol, dir: 'long' as const })),
...(flow?.data?.outflow ?? []).map((i) => ({ symbol: i.symbol, dir: 'short' as const })),
],
},
{
key: 'signal',
title: 'SIGNAL',
zh: 'signal',
items: (signalRank?.items ?? []).map((s) => ({
symbol: s.symbol,
dir: (s.bias || '').toLowerCase() === 'bearish' ? ('short' as const) : ('long' as const),
})),
},
{
// every candidate the AI actually judged this cycle (its full decision set)
key: 'decision',
title: 'DECISION',
zh: 'decision',
items: candidateCoins.map((c) => ({ symbol: c, dir: dirFor(c) })),
},
{
// executed & live: every open position is an executed order, so
// EXECUTE mirrors the live book (this cycle's fills plus anything
// still open from prior cycles) and flows straight into HOLD
key: 'exec',
title: 'EXECUTE',
zh: 'execute',
items: (positions ?? []).map((p) => ({
symbol: p.symbol,
dir: (p.side || '').toLowerCase().includes('short') ? ('short' as const) : ('long' as const),
})),
},
{
key: 'hold',
title: 'HOLD',
zh: 'hold',
items: (positions ?? []).map((p) => ({
symbol: p.symbol,
dir: (p.side || '').toLowerCase().includes('short') ? ('short' as const) : ('long' as const),
})),
},
]}
/>
</div>
<div className="tm-rule" />
{/* ── row 3: execution log · risk radar · recent trades ── */}
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0,1.1fr) minmax(0,1fr) minmax(0,1fr)' }}>
<div style={{ ...sc, borderRight: cellBorder }}>
<ExecutionLog decisions={decisions} height={432} />
</div>
<div style={{ ...sc, borderRight: cellBorder }}>
<RiskRadar positions={positions} account={account} config={config} fullStats={fullStats} />
</div>
<div style={sc}>
{/* live open positions (the book right now) */}
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 6 }}>
<span className="tm-px" style={{ fontSize: 11 }}>Positions</span>
<span className="tm-sc">Current positions · live</span>
<span className="tm-sc" style={{ marginLeft: 'auto' }}>{positions?.length ?? 0} open</span>
</div>
{positions && positions.length > 0 ? (
<table className="tm-mono" style={{ width: '100%', borderCollapse: 'collapse', fontSize: 11 }}>
<thead>
<tr className="tm-sc" style={{ fontSize: 9 }}>
<td style={{ padding: '0 0 3px' }}>symbol</td>
<td style={{ padding: '0 0 3px' }}>side</td>
<td style={{ padding: '0 0 3px', textAlign: 'right' }}>lev</td>
<td style={{ padding: '0 0 3px', textAlign: 'right' }}>PnL</td>
<td style={{ padding: '0 0 3px', textAlign: 'right' }}>return%</td>
</tr>
</thead>
<tbody>
{positions.map((p, i) => {
const long = /long|buy/i.test(p.side)
const win = (p.unrealized_pnl ?? 0) >= 0
return (
<tr key={`${p.symbol}-${i}`} style={{ borderTop: '1px solid var(--tm-hair)' }}>
<td style={{ padding: '5px 0', fontWeight: 500 }}>{baseLabel(p.symbol)}</td>
<td style={{ padding: '5px 0' }} className={long ? 'tm-up' : 'tm-dn'}>{long ? 'long' : 'short'}</td>
<td style={{ padding: '5px 0', textAlign: 'right', color: 'var(--tm-muted)' }}>{p.leverage}×</td>
<td style={{ padding: '5px 0', textAlign: 'right' }} className={win ? 'tm-up' : 'tm-dn'}>{fmtUsd(p.unrealized_pnl, true)}</td>
<td style={{ padding: '5px 0', textAlign: 'right' }} className={win ? 'tm-up' : 'tm-dn'}>{(p.unrealized_pnl_pct ?? 0).toFixed(2)}%</td>
</tr>
)
})}
</tbody>
</table>
) : <div className="tm-sc" style={{ padding: '8px 0' }}>No open positions.</div>}
<div className="tm-rule" style={{ margin: '12px 0 10px' }} />
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 6 }}>
<span className="tm-px" style={{ fontSize: 11 }}>Recent trades</span>
<span className="tm-sc">Recent closes · symbol/side/time/pnl</span>
</div>
{recentTrades.length > 0 ? (
<table className="tm-mono" style={{ width: '100%', borderCollapse: 'collapse', fontSize: 11 }}>
<tbody>
{recentTrades.map((p) => {
const win = p.realized_pnl >= 0
return (
<tr key={p.id} style={{ borderTop: '1px solid var(--tm-hair)' }}>
<td style={{ padding: '5px 0', fontWeight: 500 }}>{baseLabel(p.symbol)}</td>
<td style={{ padding: '5px 0' }} className={p.side === 'long' || p.side === 'LONG' ? 'tm-up' : 'tm-dn'}>{p.side.toLowerCase()}</td>
<td style={{ padding: '5px 0', color: 'var(--tm-muted)' }}>{fmtTime(p.exit_time)}</td>
<td style={{ padding: '5px 0', textAlign: 'right' }} className={win ? 'tm-up' : 'tm-dn'}>{fmtUsd(p.realized_pnl, true)}</td>
</tr>
)
})}
</tbody>
</table>
) : <div className="tm-sc" style={{ padding: '8px 0' }}>No closed trades yet.</div>}
</div>
</div>
<div className="tm-rule" />
{/* equity curve */}
{equityPath && (
<>
<div style={{ ...sc, paddingBottom: 4, display: 'flex', alignItems: 'baseline', gap: 8 }}>
<span className="tm-px" style={{ fontSize: 11 }}>Equity curve</span>
<span className="tm-sc">Account equity curve · all {equity?.length ?? 0} pts</span>
</div>
<div style={{ padding: '0 14px 10px' }}>
<svg width="100%" height={88} viewBox="0 0 680 80" preserveAspectRatio="none" role="img" aria-label="Equity curve" style={{ display: 'block' }}>
<line x1="0" y1="79" x2="680" y2="79" stroke="var(--tm-hair)" vectorEffect="non-scaling-stroke" />
<path d={equityPath} fill="none" stroke="var(--tm-red)" strokeWidth={2} vectorEffect="non-scaling-stroke" />
</svg>
</div>
<div className="tm-rule" />
</>
)}
{/* market net inflow — real Vergex flow-markets via claw402 */}
<div style={sc}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10, marginBottom: 8 }}>
<span className="tm-px" style={{ fontSize: 12 }}>Market net inflow</span>
<span className="tm-sc">Market net inflow · {flow?.data?.window || '1h'} · Vergex</span>
<span className="tm-sc" style={{ marginLeft: 'auto' }}>{flowItems.length} markets</span>
</div>
<FlowMarkets items={flowItems} window={flow?.data?.window} />
</div>
<div className="tm-rule" />
{/* by-symbol stats | latest decision */}
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0,1fr) minmax(0,1.3fr)' }}>
<div style={{ ...sc, borderRight: cellBorder }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 8 }}>
<span className="tm-px" style={{ fontSize: 11 }}>By symbol</span>
<span className="tm-sc">By-symbol history · trades/win/pnl</span>
</div>
{symbolStats.length > 0 ? symbolStats.map((s) => (
<div key={s.symbol} style={{ marginBottom: 7 }}>
<div className="tm-mono" style={{ display: 'flex', fontSize: 11, marginBottom: 2 }}>
<span style={{ fontWeight: 500 }}>{baseLabel(s.symbol)}</span>
<span className="tm-sc" style={{ marginLeft: 8 }}>{s.total_trades} trades · {s.win_rate.toFixed(0)}% win</span>
<span className={s.total_pnl >= 0 ? 'tm-up' : 'tm-dn'} style={{ marginLeft: 'auto' }}>{fmtUsd(s.total_pnl, true)}</span>
</div>
<div style={{ height: 4, background: 'var(--tm-hair)' }}>
<div style={{ height: 4, width: `${(s.total_trades / maxSymTrades) * 100}%`, background: s.total_pnl >= 0 ? 'var(--tm-up)' : 'var(--tm-dn)' }} />
</div>
</div>
)) : <div className="tm-sc">No symbol history.</div>}
</div>
<div style={sc}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10, marginBottom: 6 }}>
<span className="tm-px" style={{ fontSize: 11 }}>Latest decision</span>
<span className="tm-sc">AI rationale</span>
{latest && <span className="tm-sc" style={{ marginLeft: 'auto' }}>{fmtTime(latest.timestamp)}{latest.decisions?.[0]?.confidence != null ? ` · conf ${latest.decisions[0].confidence}` : ''}</span>}
</div>
{latest ? (
<>
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 8 }}>
{(latest.decisions ?? []).slice(0, 5).map((d, i) => (
<span key={i} className="tm-mono" style={{ fontSize: 11, background: 'var(--tm-red-soft)', color: '#7a1f16', padding: '2px 9px' }}>{d.action} {baseLabel(d.symbol)}</span>
))}
</div>
<p style={{ fontSize: 12, lineHeight: 1.6, color: 'var(--tm-ink-2)', margin: '0 0 8px', borderLeft: '2px solid var(--tm-red)', paddingLeft: 12 }}>
{latest.decisions?.[0]?.reasoning || latest.cot_trace?.slice(0, 320) || 'No reasoning recorded.'}
</p>
{(latest.execution_log ?? []).length > 0 && (
<div className="tm-mono" style={{ fontSize: 10, color: 'var(--tm-muted)' }}>
{(latest.execution_log ?? []).slice(0, 3).map((l, i) => <div key={i}>{l}</div>)}
</div>
)}
</>
) : <div className="tm-sc">No decisions recorded yet.</div>}
</div>
</div>
</div>
</div>
)
}
export default TerminalDashboard

View File

@@ -0,0 +1,139 @@
/*
* NOFX orchestration terminal theme — cream paper + vermilion accent.
* Scoped under .nofx-terminal so it can be built and previewed without
* disturbing the global Neo-Gold theme. The global flip (task #6) promotes
* these tokens to :root once the look is approved.
*/
@import url('https://fonts.googleapis.com/css2?family=Silkscreen:wght@400;700&display=swap');
.nofx-terminal {
/* surfaces */
--tm-paper: #f1ece2;
--tm-panel: #f7f4ec;
--tm-ink: #1a1813;
--tm-ink-2: #3a372e;
--tm-muted: #8a8478;
--tm-hair: #d8d2c4;
--tm-rule: #1a1813;
--tm-head: #16140f;
/* brand + semantic */
--tm-red: #e0483b;
--tm-red-soft: #f6d9d2;
--tm-up: #2e8b57;
--tm-dn: #d6433a;
--tm-mono: 'IBM Plex Mono', 'Courier New', monospace;
--tm-px: 'Silkscreen', 'IBM Plex Mono', monospace;
background: var(--tm-paper);
color: var(--tm-ink);
font-family: var(--tm-mono);
font-variant-numeric: tabular-nums;
}
.nofx-terminal,
.nofx-terminal * {
box-sizing: border-box;
}
/* pixel display numbers (hero P/L, big counters) */
.tm-px {
font-family: var(--tm-px);
letter-spacing: 0.02em;
}
/* small-caps label, the workhorse meta text */
.tm-sc {
font-size: 10px;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--tm-muted);
}
.tm-mono {
font-family: var(--tm-mono);
font-variant-numeric: tabular-nums;
}
/* order-book per-row flash — each row is keyed by PRICE and the flash overlay
is keyed by SIZE, so only the levels whose size actually changed flash, and
they flash independently (green = size grew / red = size shrank). */
@keyframes ob-up {
0% {
background: rgba(46, 139, 87, 0.34);
}
100% {
background: transparent;
}
}
@keyframes ob-dn {
0% {
background: rgba(214, 67, 58, 0.34);
}
100% {
background: transparent;
}
}
.ob-up {
animation: ob-up 0.45s ease-out;
}
.ob-dn {
animation: ob-dn 0.45s ease-out;
}
.tm-red {
color: var(--tm-red);
}
.tm-up {
color: var(--tm-up);
}
.tm-dn {
color: var(--tm-dn);
}
.tm-rule {
height: 1px;
background: var(--tm-rule);
}
.tm-hair {
height: 1px;
background: var(--tm-hair);
}
/* bordered cell — the basic terminal box */
.tm-box {
border: 1px solid var(--tm-ink);
}
/* radar ping + beam motion for the topology */
@keyframes tm-ping {
0% {
r: 30;
opacity: 0.6;
}
100% {
r: 52;
opacity: 0;
}
}
@keyframes tm-pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.55;
}
}
.tm-blink {
animation: tm-pulse 1.6s ease-in-out infinite;
}
@media (max-width: 768px) {
.nofx-terminal {
font-size: 13px;
}
}

View File

@@ -679,23 +679,23 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
<DeepVoidBackground className="py-8" disableAnimation>
<div className="w-full px-4 md:px-8 space-y-8 animate-fade-in">
{/* Header - Terminal Style */}
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 border-b border-white/10 pb-6">
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 border-b border-nofx-gold/20 pb-6">
<div className="flex items-center gap-4">
<div className="relative group">
<div className="absolute -inset-1 bg-nofx-gold/20 rounded-xl blur opacity-0 group-hover:opacity-100 transition duration-500"></div>
<div className="w-12 h-12 md:w-14 md:h-14 rounded-xl flex items-center justify-center bg-black border border-nofx-gold/30 text-nofx-gold relative z-10 shadow-[0_0_15px_rgba(240,185,11,0.1)]">
<div className="absolute -inset-1 bg-transparent rounded-xl opacity-0 group-hover:opacity-100 transition duration-500"></div>
<div className="w-12 h-12 md:w-14 md:h-14 rounded-xl flex items-center justify-center bg-nofx-bg-lighter border border-nofx-gold/30 text-nofx-gold relative z-10">
<Bot className="w-6 h-6 md:w-7 md:h-7" />
</div>
</div>
<div>
<h1 className="text-2xl md:text-3xl font-bold font-mono tracking-tight text-white flex items-center gap-3 uppercase">
<h1 className="text-2xl md:text-3xl font-bold font-mono tracking-tight text-nofx-text flex items-center gap-3 uppercase">
{t('aiTraders', language)}
<span className="text-xs font-mono font-normal px-2 py-0.5 rounded bg-nofx-gold/10 text-nofx-gold border border-nofx-gold/20 tracking-wider">
{traders?.length || 0} ACTIVE_NODES
</span>
</h1>
<p className="text-xs font-mono text-zinc-500 uppercase tracking-widest mt-1 ml-1 flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-green-500 animate-pulse"></span>
<p className="text-xs font-mono text-nofx-text-muted uppercase tracking-widest mt-1 ml-1 flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-nofx-success animate-pulse"></span>
SYSTEM_READY
</p>
</div>
@@ -704,7 +704,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
<div className="flex gap-2 w-full md:w-auto overflow-x-auto pb-1 md:pb-0 hide-scrollbar">
<button
onClick={handleAddModel}
className="px-4 py-2 rounded text-xs font-mono uppercase tracking-wider transition-all border border-zinc-700 bg-black/20 text-zinc-400 hover:text-white hover:border-zinc-500 whitespace-nowrap backdrop-blur-sm"
className="px-4 py-2 rounded text-xs font-mono uppercase tracking-wider transition-all border border-nofx-gold/20 bg-nofx-bg-lighter text-nofx-text-muted hover:text-nofx-text hover:border-nofx-gold/40 whitespace-nowrap"
>
<div className="flex items-center gap-2">
<Plus className="w-3 h-3" />
@@ -714,7 +714,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
<button
onClick={handleAddExchange}
className="px-4 py-2 rounded text-xs font-mono uppercase tracking-wider transition-all border border-zinc-700 bg-black/20 text-zinc-400 hover:text-white hover:border-zinc-500 whitespace-nowrap backdrop-blur-sm"
className="px-4 py-2 rounded text-xs font-mono uppercase tracking-wider transition-all border border-nofx-gold/20 bg-nofx-bg-lighter text-nofx-text-muted hover:text-nofx-text hover:border-nofx-gold/40 whitespace-nowrap"
>
<div className="flex items-center gap-2">
<Plus className="w-3 h-3" />
@@ -724,7 +724,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
<button
onClick={() => setShowTelegramModal(true)}
className="px-4 py-2 rounded text-xs font-mono uppercase tracking-wider transition-all border border-sky-900/50 bg-black/20 text-sky-500 hover:text-sky-300 hover:border-sky-700 whitespace-nowrap backdrop-blur-sm"
className="px-4 py-2 rounded text-xs font-mono uppercase tracking-wider transition-all border border-nofx-accent/30 bg-nofx-bg-lighter text-nofx-accent hover:text-nofx-gold hover:border-nofx-accent/50 whitespace-nowrap"
>
<div className="flex items-center gap-2">
<MessageCircle className="w-3 h-3" />
@@ -738,7 +738,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
configuredModels.length === 0 ||
configuredExchanges.length === 0
}
className="group relative px-6 py-2 rounded text-xs font-bold font-mono uppercase tracking-wider transition-all disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap overflow-hidden bg-nofx-gold text-black hover:bg-yellow-400 shadow-[0_0_20px_rgba(240,185,11,0.2)] hover:shadow-[0_0_30px_rgba(240,185,11,0.4)]"
className="group relative px-6 py-2 rounded text-xs font-bold font-mono uppercase tracking-wider transition-all disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap overflow-hidden bg-nofx-gold text-white hover:bg-nofx-accent"
>
<span className="relative z-10 flex items-center gap-2">
<Plus className="w-4 h-4" />

View File

@@ -100,10 +100,10 @@ function BeginnerHyperliquidGuide({
]
return (
<div className="rounded-xl border border-nofx-gold/20 bg-[linear-gradient(180deg,rgba(240,185,11,0.1),rgba(0,0,0,0.16))] p-4">
<div className="rounded-xl border border-nofx-gold/20 bg-nofx-bg-lighter p-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div>
<div className="text-sm font-semibold text-white">
<div className="text-sm font-semibold text-nofx-text">
New to Hyperliquid?
</div>
<p className="mt-1 text-xs leading-5 text-nofx-text-muted">
@@ -114,7 +114,7 @@ function BeginnerHyperliquidGuide({
<div
className={`w-fit rounded-full px-2.5 py-1 text-[11px] font-semibold ${
hasInjectedWallet
? 'bg-emerald-500/10 text-emerald-300'
? 'bg-nofx-success/10 text-nofx-success'
: 'bg-nofx-gold/10 text-nofx-gold'
}`}
>
@@ -127,11 +127,11 @@ function BeginnerHyperliquidGuide({
const Icon = step.icon
return (
<div key={step.title} className="flex gap-3">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg border border-white/10 bg-black/25 text-nofx-gold">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg border border-nofx-gold/20 bg-nofx-bg-deeper text-nofx-gold">
<Icon className="h-3.5 w-3.5" />
</div>
<div>
<div className="text-sm font-semibold text-zinc-100">
<div className="text-sm font-semibold text-nofx-text">
{index + 1}. {step.title}
</div>
<p className="mt-0.5 text-xs leading-5 text-nofx-text-muted">
@@ -150,7 +150,7 @@ function BeginnerHyperliquidGuide({
href="https://rabby.io/"
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2 rounded-lg border border-white/10 bg-white/[0.04] px-3 py-2 text-xs font-semibold text-zinc-200 hover:border-white/20 hover:bg-white/[0.07]"
className="inline-flex items-center gap-2 rounded-lg border border-nofx-gold/20 bg-nofx-bg-deeper px-3 py-2 text-xs font-semibold text-nofx-text hover:border-nofx-gold/40 hover:bg-nofx-bg"
>
<Download className="h-3.5 w-3.5" />
Install Rabby
@@ -159,7 +159,7 @@ function BeginnerHyperliquidGuide({
href="https://metamask.io/download/"
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2 rounded-lg border border-white/10 bg-white/[0.04] px-3 py-2 text-xs font-semibold text-zinc-200 hover:border-white/20 hover:bg-white/[0.07]"
className="inline-flex items-center gap-2 rounded-lg border border-nofx-gold/20 bg-nofx-bg-deeper px-3 py-2 text-xs font-semibold text-nofx-text hover:border-nofx-gold/40 hover:bg-nofx-bg"
>
<ExternalLink className="h-3.5 w-3.5" />
MetaMask
@@ -170,7 +170,7 @@ function BeginnerHyperliquidGuide({
href="https://app.hyperliquid.xyz/"
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2 rounded-lg bg-nofx-gold px-3 py-2 text-xs font-bold text-black hover:bg-yellow-400"
className="inline-flex items-center gap-2 rounded-lg bg-nofx-gold px-3 py-2 text-xs font-bold text-white hover:bg-nofx-accent"
>
Open Hyperliquid
<ExternalLink className="h-3.5 w-3.5" />
@@ -331,7 +331,23 @@ export function AutopilotLaunchPanel({
if (!claw402Model || !hyperliquidExchange) return
setLaunching(true)
try {
// Re-fetch the live trader list before deciding. The `traders` prop can be
// stale (open tab serving an old snapshot), which would make us create a
// duplicate "NOFX Autopilot" — paying the ~35s first-create cost again and
// orphaning the dashboard onto a deleted id (the "API Not Found" 404). Match
// the same way the autopilotTrader memo does, falling back to it on failure.
let trader = autopilotTrader
try {
const fresh = await api.getTraders(true)
trader =
fresh.find((t) => t.trader_name === 'NOFX Autopilot') ||
fresh.find((t) =>
(t.strategy_name || '').toLowerCase().includes('claw402')
) ||
trader
} catch {
// network hiccup — keep the prop-derived trader rather than risk a dup
}
if (!trader) {
const strategyId = await ensureClaw402Strategy()
trader = await api.createTrader({
@@ -339,7 +355,7 @@ export function AutopilotLaunchPanel({
ai_model_id: claw402Model.id,
exchange_id: hyperliquidExchange.id,
strategy_id: strategyId,
scan_interval_minutes: 15,
scan_interval_minutes: 5,
is_cross_margin: true,
show_in_competition: true,
btc_eth_leverage: 10,
@@ -379,7 +395,7 @@ export function AutopilotLaunchPanel({
<button
type="button"
onClick={() => onOpenClaw402Config?.()}
className="inline-flex items-center gap-1.5 text-xs font-semibold text-nofx-gold hover:text-yellow-300"
className="inline-flex items-center gap-1.5 text-xs font-semibold text-nofx-gold hover:text-nofx-accent"
>
<CircleDollarSign className="h-3.5 w-3.5" />
Deposit
@@ -387,7 +403,7 @@ export function AutopilotLaunchPanel({
<button
type="button"
onClick={() => void copyText(feeWalletAddress, 'AI fee wallet')}
className="inline-flex items-center gap-1.5 text-xs font-semibold text-nofx-gold hover:text-yellow-300"
className="inline-flex items-center gap-1.5 text-xs font-semibold text-nofx-gold hover:text-nofx-accent"
>
<Copy className="h-3.5 w-3.5" />
Copy
@@ -397,7 +413,7 @@ export function AutopilotLaunchPanel({
<button
type="button"
onClick={() => onOpenClaw402Config?.()}
className="inline-flex items-center gap-1.5 text-xs font-semibold text-nofx-gold hover:text-yellow-300"
className="inline-flex items-center gap-1.5 text-xs font-semibold text-nofx-gold hover:text-nofx-accent"
>
<ArrowRight className="h-3.5 w-3.5" />
Open
@@ -416,7 +432,7 @@ export function AutopilotLaunchPanel({
<button
type="button"
onClick={() => onOpenHyperliquidConfig?.()}
className="inline-flex items-center gap-1.5 text-xs font-semibold text-nofx-gold hover:text-yellow-300"
className="inline-flex items-center gap-1.5 text-xs font-semibold text-nofx-gold hover:text-nofx-accent"
>
<Wallet className="h-3.5 w-3.5" />
Open
@@ -461,7 +477,7 @@ export function AutopilotLaunchPanel({
navigate(ROUTES.welcome)
}
}}
className="inline-flex items-center justify-center gap-2 rounded-lg bg-nofx-gold px-4 py-3 text-sm font-bold text-black hover:bg-yellow-400"
className="inline-flex items-center justify-center gap-2 rounded-lg bg-nofx-gold px-4 py-3 text-sm font-bold text-white hover:bg-nofx-accent"
>
Open Claw402 wallet
<ArrowRight className="h-4 w-4" />
@@ -482,7 +498,7 @@ export function AutopilotLaunchPanel({
?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}}
className="inline-flex items-center justify-center gap-2 rounded-lg bg-nofx-gold px-4 py-3 text-sm font-bold text-black hover:bg-yellow-400"
className="inline-flex items-center justify-center gap-2 rounded-lg bg-nofx-gold px-4 py-3 text-sm font-bold text-white hover:bg-nofx-accent"
>
Connect Hyperliquid
<ArrowRight className="h-4 w-4" />
@@ -496,7 +512,7 @@ export function AutopilotLaunchPanel({
href="https://app.hyperliquid.xyz/"
target="_blank"
rel="noreferrer"
className="inline-flex items-center justify-center gap-2 rounded-lg bg-nofx-gold px-4 py-3 text-sm font-bold text-black hover:bg-yellow-400"
className="inline-flex items-center justify-center gap-2 rounded-lg bg-nofx-gold px-4 py-3 text-sm font-bold text-white hover:bg-nofx-accent"
>
Deposit USDC on Hyperliquid
<ExternalLink className="h-4 w-4" />
@@ -511,7 +527,7 @@ export function AutopilotLaunchPanel({
onClick={() =>
navigate(buildDashboardPath(autopilotTrader.trader_id))
}
className="inline-flex items-center justify-center gap-2 rounded-lg bg-emerald-400 px-4 py-3 text-sm font-bold text-black hover:bg-emerald-300"
className="inline-flex items-center justify-center gap-2 rounded-lg bg-nofx-success px-4 py-3 text-sm font-bold text-white hover:bg-nofx-success/80"
>
Open dashboard
<ArrowRight className="h-4 w-4" />
@@ -524,7 +540,7 @@ export function AutopilotLaunchPanel({
type="button"
onClick={launchAutopilot}
disabled={launching || !allReady}
className="inline-flex items-center justify-center gap-2 rounded-lg bg-nofx-gold px-4 py-3 text-sm font-bold text-black hover:bg-yellow-400 disabled:cursor-not-allowed disabled:opacity-60"
className="inline-flex items-center justify-center gap-2 rounded-lg bg-nofx-gold px-4 py-3 text-sm font-bold text-white hover:bg-nofx-accent disabled:cursor-not-allowed disabled:opacity-60"
>
{launching ? (
<Loader2 className="h-4 w-4 animate-spin" />
@@ -537,7 +553,7 @@ export function AutopilotLaunchPanel({
}
return (
<section className="overflow-hidden rounded-xl border border-nofx-gold/20 bg-[linear-gradient(135deg,rgba(20,17,7,0.92),rgba(8,11,16,0.9)_42%,rgba(7,14,18,0.88))] shadow-[0_20px_80px_rgba(0,0,0,0.28)]">
<section className="overflow-hidden rounded-xl border border-nofx-gold/20 bg-nofx-bg-lighter">
<div className="grid gap-0 xl:grid-cols-[1.05fr_0.95fr]">
<div className="p-5 md:p-6">
<div className="mb-5 flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
@@ -546,7 +562,7 @@ export function AutopilotLaunchPanel({
<ShieldCheck className="h-3.5 w-3.5" />
Guided Launch
</div>
<h2 className="text-2xl font-bold tracking-tight text-white md:text-3xl">
<h2 className="text-2xl font-bold tracking-tight text-nofx-text md:text-3xl">
Start NOFX Autopilot in minutes
</h2>
<p className="mt-2 max-w-2xl text-sm leading-6 text-nofx-text-muted">
@@ -559,7 +575,7 @@ export function AutopilotLaunchPanel({
type="button"
onClick={() => void refreshEverything()}
disabled={refreshing || walletLoading}
className="inline-flex items-center justify-center gap-2 rounded-lg border border-white/10 bg-white/[0.04] px-3 py-2 text-xs font-semibold text-nofx-text-muted hover:text-white disabled:opacity-60"
className="inline-flex items-center justify-center gap-2 rounded-lg border border-nofx-gold/20 bg-nofx-bg-deeper px-3 py-2 text-xs font-semibold text-nofx-text-muted hover:text-nofx-text disabled:opacity-60"
>
<RefreshCw
className={`h-3.5 w-3.5 ${refreshing || walletLoading ? 'animate-spin' : ''}`}
@@ -574,16 +590,16 @@ export function AutopilotLaunchPanel({
{steps.map((step, index) => (
<div
key={step.title}
className="rounded-lg border border-white/10 bg-black/20 p-4"
className="rounded-lg border border-nofx-gold/20 bg-nofx-bg p-4"
>
<div className="flex items-start gap-3">
<div
className={`mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg border text-sm font-bold ${
step.status === 'ready'
? 'border-emerald-400/30 bg-emerald-500/15 text-emerald-300'
? 'border-nofx-success/30 bg-nofx-success/15 text-nofx-success'
: step.status === 'action'
? 'border-nofx-gold/30 bg-nofx-gold/15 text-nofx-gold'
: 'border-white/10 bg-white/[0.04] text-nofx-text-muted'
: 'border-nofx-gold/20 bg-nofx-bg-deeper text-nofx-text-muted'
}`}
>
{step.status === 'ready' ? (
@@ -596,7 +612,7 @@ export function AutopilotLaunchPanel({
</div>
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center justify-between gap-2">
<h3 className="font-semibold text-white">{step.title}</h3>
<h3 className="font-semibold text-nofx-text">{step.title}</h3>
{step.action}
</div>
<p className="mt-1 text-xs leading-5 text-nofx-text-muted">
@@ -612,21 +628,21 @@ export function AutopilotLaunchPanel({
</div>
</div>
<aside className="border-t border-white/10 bg-black/20 p-5 md:p-6 xl:border-l xl:border-t-0">
<div className="mb-4 flex items-center gap-2 text-sm font-semibold text-white">
<aside className="border-t border-nofx-gold/20 bg-nofx-bg p-5 md:p-6 xl:border-l xl:border-t-0">
<div className="mb-4 flex items-center gap-2 text-sm font-semibold text-nofx-text">
<Wallet className="h-4 w-4 text-nofx-gold" />
Hyperliquid setup
</div>
{hyperliquidConnected ? (
<div className="rounded-lg border border-emerald-400/25 bg-emerald-500/10 p-4">
<div className="flex items-center gap-2 text-sm font-semibold text-emerald-200">
<div className="rounded-lg border border-nofx-success/25 bg-nofx-success/10 p-4">
<div className="flex items-center gap-2 text-sm font-semibold text-nofx-success">
<CheckCircle2 className="h-4 w-4" />
Trading authorization is ready
</div>
<div className="mt-2 font-mono text-xs text-emerald-100/80">
<div className="mt-2 font-mono text-xs text-nofx-success/90">
{shortAddress(hyperliquidExchange?.hyperliquidWalletAddr)}
</div>
<p className="mt-3 text-xs leading-5 text-emerald-100/70">
<p className="mt-3 text-xs leading-5 text-nofx-text-muted">
Funds stay in your Hyperliquid account. NOFX only stores the
authorized Agent key required for automated execution.
</p>

View File

@@ -38,24 +38,24 @@ export function BeginnerGuideCards({
{
key: 'model',
icon: Brain,
title: isZh ? '1. 极速模型' : '1. Fast AI',
title: isZh ? '1. Fast AI' : '1. Fast AI',
desc: isZh
? '默认就是 Claw402 + DeepSeek。第一次不用挑模型,先跑起来。'
? 'Start with Claw402 + DeepSeek. No model picking needed for the first run.'
: 'Start with Claw402 + DeepSeek. No model picking needed for the first run.',
meta: walletAddress
? isZh
? `钱包 ${truncateAddress(walletAddress)}`
? `Wallet ${truncateAddress(walletAddress)}`
: `Wallet ${truncateAddress(walletAddress)}`
: isZh
? 'Base USDC 按次付费'
? 'Pay per call with Base USDC'
: 'Pay per call with Base USDC',
ready: claw402Ready,
actionLabel: claw402Ready
? isZh
? '已配置'
? 'Configured'
: 'Configured'
: isZh
? '一键配置'
? 'One-click setup'
: 'One-click setup',
onAction: onQuickSetupClaw402,
disabled: claw402Ready,
@@ -63,13 +63,13 @@ export function BeginnerGuideCards({
{
key: 'exchange',
icon: Landmark,
title: isZh ? '2. 连接交易所' : '2. Add Exchange',
title: isZh ? '2. Add Exchange' : '2. Add Exchange',
desc: isZh
? '交易所接好以后AI 才能真正下单。'
? 'Connect an exchange so the AI can actually place trades.'
: 'Connect an exchange so the AI can actually place trades.',
meta: exchangeReady
? isZh
? '已准备好'
? 'Ready'
: 'Ready'
: isZh
? 'Binance / OKX / Bybit / Hyperliquid'
@@ -77,10 +77,10 @@ export function BeginnerGuideCards({
ready: exchangeReady,
actionLabel: exchangeReady
? isZh
? '继续管理'
? 'Manage'
: 'Manage'
: isZh
? '去配置'
? 'Configure'
: 'Configure',
onAction: onOpenExchange,
disabled: false,
@@ -88,47 +88,47 @@ export function BeginnerGuideCards({
{
key: 'strategy',
icon: Sparkles,
title: isZh ? '3. 选择策略' : '3. Pick Strategy',
title: isZh ? '3. Pick Strategy' : '3. Pick Strategy',
desc: isZh
? '先用默认策略也可以,后面再慢慢细调。'
? 'You can start with a default strategy and fine-tune later.'
: 'You can start with a default strategy and fine-tune later.',
meta: strategyReady
? isZh
? '已有策略可用'
? 'Strategy ready'
: 'Strategy ready'
: isZh
? '可选,但建议提前看一眼'
? 'Optional, but worth a quick look'
: 'Optional, but worth a quick look',
ready: strategyReady,
actionLabel: isZh ? '打开策略页' : 'Open strategy',
actionLabel: isZh ? 'Open strategy' : 'Open strategy',
onAction: onOpenStrategy,
disabled: false,
},
{
key: 'trader',
icon: Rocket,
title: isZh ? '4. 创建 Trader' : '4. Create Trader',
title: isZh ? '4. Create Trader' : '4. Create Trader',
desc: isZh
? '最后一步,把模型和交易所绑在一起,就能开始运行。'
? 'Last step: bind your model and exchange, then start running.'
: 'Last step: bind your model and exchange, then start running.',
meta: traderReady
? isZh
? '已创建 Trader,可继续添加'
? 'Trader created, you can add more'
: 'Trader created, you can add more'
: canCreateTrader
? isZh
? '已经可以创建'
? 'Ready to create'
: 'Ready to create'
: isZh
? '先完成前三步'
? 'Finish the first three steps first'
: 'Finish the first three steps first',
ready: traderReady,
actionLabel: traderReady
? isZh
? '继续创建'
? 'Create another'
: 'Create another'
: isZh
? '立即创建'
? 'Create now'
: 'Create now',
onAction: onCreateTrader,
disabled: !canCreateTrader,
@@ -136,20 +136,20 @@ export function BeginnerGuideCards({
]
return (
<section className="space-y-4 rounded-[28px] border border-white/10 bg-zinc-950/60 p-5 backdrop-blur-xl">
<section className="space-y-4 rounded-[28px] border border-nofx-gold/20 bg-nofx-bg-lighter p-5">
<div className="flex items-center justify-between gap-4">
<div>
<div className="text-xs font-semibold uppercase tracking-[0.3em] text-nofx-gold/80">
{isZh ? '新手引导' : 'Quickstart'}
{isZh ? 'Quickstart' : 'Quickstart'}
</div>
<h2 className="mt-1 text-xl font-bold text-white">
<h2 className="mt-1 text-xl font-bold text-nofx-text">
{isZh
? '先按这 4 步走,最快上手'
? 'Follow these 4 steps to get started fast'
: 'Follow these 4 steps to get started fast'}
</h2>
</div>
{/* <div className="rounded-full border border-white/10 bg-white/5 px-3 py-1 text-xs text-zinc-400">
{isZh ? '老手模式不会看到这块' : 'Hidden in advanced mode'}
{/* <div className="rounded-full border border-nofx-gold/20 bg-nofx-bg-deeper px-3 py-1 text-xs text-nofx-text-muted">
{isZh ? 'Hidden in advanced mode' : 'Hidden in advanced mode'}
</div> */}
</div>
@@ -159,36 +159,36 @@ export function BeginnerGuideCards({
return (
<div
key={card.key}
className="rounded-[22px] border border-white/8 bg-black/25 p-4"
className="rounded-[22px] border border-nofx-gold/20 bg-nofx-bg-deeper p-4"
>
<div className="flex items-center justify-between gap-3">
<div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-white/6 text-nofx-gold">
<div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-nofx-gold/10 text-nofx-gold">
<Icon className="h-5 w-5" />
</div>
<span
className={`rounded-full px-2.5 py-1 text-[10px] font-bold uppercase tracking-[0.22em] ${
card.ready
? 'bg-emerald-500/15 text-emerald-300'
: 'bg-zinc-800 text-zinc-400'
? 'bg-nofx-success/15 text-nofx-success'
: 'bg-nofx-bg-deeper text-nofx-text-muted'
}`}
>
{card.ready
? isZh
? '已就绪'
? 'Ready'
: 'Ready'
: isZh
? '待完成'
? 'Pending'
: 'Pending'}
</span>
</div>
<h3 className="mt-4 text-base font-semibold text-white">
<h3 className="mt-4 text-base font-semibold text-nofx-text">
{card.title}
</h3>
<p className="mt-2 min-h-[72px] text-sm leading-6 text-zinc-400">
<p className="mt-2 min-h-[72px] text-sm leading-6 text-nofx-text-muted">
{card.desc}
</p>
<div className="mt-3 text-xs text-zinc-500">{card.meta}</div>
<div className="mt-3 text-xs text-nofx-text-muted">{card.meta}</div>
<button
type="button"
@@ -196,8 +196,8 @@ export function BeginnerGuideCards({
disabled={card.disabled}
className={`mt-5 w-full rounded-2xl px-4 py-3 text-sm font-semibold transition ${
card.disabled
? 'cursor-not-allowed bg-zinc-900 text-zinc-500'
: 'bg-nofx-gold text-black hover:bg-yellow-400'
? 'cursor-not-allowed bg-nofx-bg-deeper text-nofx-text-muted'
: 'bg-nofx-gold text-white hover:bg-nofx-gold/90'
}`}
>
{card.actionLabel}

View File

@@ -1,16 +1,16 @@
import { describe, it, expect } from 'vitest'
/**
* PR #678 測試: 修復 CompetitionPage 中 NaN 和缺失數據的顯示問題
* PR #678 test: fix display of NaN and missing data in CompetitionPage
*
* 問題:當 total_pnl_pct null/undefined/NaN 時,會顯示 "NaN%" "0.00%"
* 修復:檢查數據有效性,顯示 "—" 表示缺失數據
* Problem: when total_pnl_pct is null/undefined/NaN, it would show "NaN%" or "0.00%"
* Fix: check data validity and show "—" to indicate missing data
*/
describe('CompetitionPage - Data Validation Logic (PR #678)', () => {
/**
* 測試數據有效性檢查邏輯
* 這是 PR #678 引入的核心邏輯
* Test the data validity check logic
* This is the core logic introduced in PR #678
*/
describe('hasValidData check', () => {
it('should return true for valid numbers', () => {
@@ -106,8 +106,8 @@ describe('CompetitionPage - Data Validation Logic (PR #678)', () => {
})
/**
* 測試 gap 計算邏輯
* gap 應該只在數據有效時計算
* Test the gap calculation logic
* The gap should only be calculated when data is valid
*/
describe('gap calculation', () => {
it('should calculate gap correctly for valid data', () => {
@@ -164,8 +164,8 @@ describe('CompetitionPage - Data Validation Logic (PR #678)', () => {
})
/**
* 測試顯示邏輯
* 修復後應顯示「—」而非「NaN%」或「0.00%
* Test the display logic
* After the fix it should show "—" instead of "NaN%" or "0.00%"
*/
describe('display formatting', () => {
it('should format valid positive percentage correctly', () => {
@@ -236,8 +236,8 @@ describe('CompetitionPage - Data Validation Logic (PR #678)', () => {
})
/**
* 測試領先/落後訊息顯示邏輯
* 只有在數據有效時才顯示 "領先" 或 "落後" 訊息
* Test the leading/trailing message display logic
* The "leading" or "trailing" message should only show when data is valid
*/
describe('leading/trailing message display', () => {
it('should show leading message when winning with positive gap', () => {
@@ -290,7 +290,7 @@ describe('CompetitionPage - Data Validation Logic (PR #678)', () => {
})
/**
* 測試邊界情況
* Test edge cases
*/
describe('edge cases', () => {
it('should handle very small positive numbers', () => {
@@ -312,7 +312,7 @@ describe('CompetitionPage - Data Validation Logic (PR #678)', () => {
it('should handle Infinity as invalid (produces NaN in calculations)', () => {
const total_pnl_pct = Infinity
// Infinity 本身不是 NaN但在減法運算中可能導致問題
// Infinity itself is not NaN, but it can cause issues in subtraction
const hasValidData = total_pnl_pct != null && isFinite(total_pnl_pct)
expect(hasValidData).toBe(false)

View File

@@ -20,7 +20,7 @@ export function CompetitionPage() {
'competition',
api.getCompetition,
{
refreshInterval: 15000, // 15秒刷新竞赛数据不需要太频繁更新
refreshInterval: 15000, // Refresh every 15s (competition data does not need very frequent updates)
revalidateOnFocus: false,
dedupingInterval: 10000,
}
@@ -33,8 +33,8 @@ export function CompetitionPage() {
setIsModalOpen(true)
} catch (error) {
console.error('Failed to fetch trader config:', error)
// 对于未登录用户,不显示详细配置,这是正常行为
// 竞赛页面主要用于查看排行榜和基本信息
// For logged-out users we do not show detailed config; this is expected behavior
// The competition page is mainly for viewing the leaderboard and basic info
}
}
@@ -48,20 +48,20 @@ export function CompetitionPage() {
<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="animate-pulse bg-nofx-bg-lighter border border-nofx-gold/20 rounded-xl p-8">
<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 className="h-8 w-64 bg-nofx-bg-deeper rounded"></div>
<div className="h-4 w-48 bg-nofx-bg-deeper rounded"></div>
</div>
<div className="h-12 w-32 bg-white/5 rounded"></div>
<div className="h-12 w-32 bg-nofx-bg-deeper 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="bg-nofx-bg-lighter border border-nofx-gold/20 rounded-xl p-6">
<div className="h-6 w-40 mb-4 bg-nofx-bg-deeper 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 className="h-20 w-full bg-nofx-bg-deeper rounded"></div>
<div className="h-20 w-full bg-nofx-bg-deeper rounded"></div>
</div>
</div>
</div>
@@ -70,16 +70,16 @@ export function CompetitionPage() {
)
}
// 如果有数据返回但没有交易员,显示空状态
// If data returns but there are no traders, show the empty state
if (!competition.traders || competition.traders.length === 0) {
return (
<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 - 精简版 */}
{/* Competition Header - compact */}
<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)]"
className="w-10 h-10 md:w-12 md:h-12 rounded-xl flex items-center justify-center bg-nofx-bg-lighter border border-nofx-gold/30"
>
<Trophy
className="w-6 h-6 md:w-7 md:h-7 text-nofx-gold"
@@ -87,7 +87,7 @@ export function CompetitionPage() {
</div>
<div>
<h1
className="text-xl md:text-2xl font-bold flex items-center gap-2 text-white"
className="text-xl md:text-2xl font-bold flex items-center gap-2 text-nofx-text"
>
{t('aiCompetition', language)}
<span
@@ -96,7 +96,7 @@ export function CompetitionPage() {
0 {t('traders', language)}
</span>
</h1>
<p className="text-xs text-zinc-400">
<p className="text-xs text-nofx-text-muted">
{t('liveBattle', language)}
</p>
</div>
@@ -104,14 +104,14 @@ export function CompetitionPage() {
</div>
{/* Empty State */}
<div className="bg-black/40 border border-white/10 rounded-xl p-16 text-center backdrop-blur-md">
<div className="bg-nofx-bg-lighter border border-nofx-gold/20 rounded-xl p-16 text-center">
<Trophy
className="w-16 h-16 mx-auto mb-4 text-zinc-700"
className="w-16 h-16 mx-auto mb-4 text-nofx-text-muted"
/>
<h3 className="text-lg font-bold mb-2 text-white">
<h3 className="text-lg font-bold mb-2 text-nofx-text">
{t('noTraders', language)}
</h3>
<p className="text-sm text-zinc-400">
<p className="text-sm text-nofx-text-muted">
{t('createFirstTrader', language)}
</p>
</div>
@@ -120,22 +120,22 @@ export function CompetitionPage() {
)
}
// 按收益率排序
// Sort by return percentage
const sortedTraders = [...competition.traders].sort(
(a, b) => b.total_pnl_pct - a.total_pnl_pct
)
// 找出领先者
// Find the leader
const leader = sortedTraders[0]
return (
<DeepVoidBackground className="py-8" disableAnimation>
<div className="w-full px-4 md:px-8 space-y-8 animate-fade-in">
{/* Competition Header - 精简版 */}
{/* Competition Header - compact */}
<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)]"
className="w-10 h-10 md:w-12 md:h-12 rounded-xl flex items-center justify-center bg-nofx-bg-lighter border border-nofx-gold/30"
>
<Trophy
className="w-6 h-6 md:w-7 md:h-7 text-nofx-gold"
@@ -143,7 +143,7 @@ export function CompetitionPage() {
</div>
<div>
<h1
className="text-xl md:text-2xl font-bold flex items-center gap-2 text-white"
className="text-xl md:text-2xl font-bold flex items-center gap-2 text-nofx-text"
>
{t('aiCompetition', language)}
<span
@@ -152,13 +152,13 @@ export function CompetitionPage() {
{competition.count} {t('traders', language)}
</span>
</h1>
<p className="text-xs text-zinc-400">
<p className="text-xs text-nofx-text-muted">
{t('liveBattle', language)}
</p>
</div>
</div>
<div className="text-left md:text-right w-full md:w-auto">
<div className="text-xs mb-1 text-zinc-400">
<div className="text-xs mb-1 text-nofx-text-muted">
{t('leader', language)}
</div>
<div
@@ -169,7 +169,7 @@ export function CompetitionPage() {
<div
className="text-sm font-semibold"
style={{
color: (leader?.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D',
color: (leader?.total_pnl ?? 0) >= 0 ? '#2E8B57' : '#D6433A',
}}
>
{(leader?.total_pnl ?? 0) >= 0 ? '+' : ''}
@@ -182,16 +182,16 @@ export function CompetitionPage() {
<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"
className="bg-nofx-bg-lighter border border-nofx-gold/20 rounded-xl p-6 animate-slide-in hover:border-nofx-gold/40 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"
className="text-lg font-bold flex items-center gap-2 text-nofx-text"
>
{t('performanceComparison', language)}
</h2>
<div className="text-xs text-zinc-400">
<div className="text-xs text-nofx-text-muted">
{t('realTimePnL', language)}
</div>
</div>
@@ -200,17 +200,17 @@ export function CompetitionPage() {
{/* 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"
className="bg-nofx-bg-lighter border border-nofx-gold/20 rounded-xl p-6 animate-slide-in hover:border-nofx-gold/40 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"
className="text-lg font-bold flex items-center gap-2 text-nofx-text"
>
{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)]"
className="text-xs px-2 py-1 rounded bg-nofx-gold/10 text-nofx-gold border border-nofx-gold/20"
>
{t('live', language)}
</div>
@@ -230,12 +230,12 @@ export function CompetitionPage() {
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'}`,
? 'rgba(224, 72, 59, 0.08)'
: '#F7F4EC',
border: `1px solid ${isLeader ? 'rgba(224, 72, 59, 0.4)' : 'rgba(26,24,19,0.14)'}`,
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)',
? '0 0 0 1px rgba(224, 72, 59, 0.15)'
: 'none',
}}
>
<div className="flex items-center justify-between">
@@ -246,13 +246,13 @@ export function CompetitionPage() {
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%)'
? '#E0483B'
: index === 1
? 'linear-gradient(135deg, #C0C0C0 0%, #E8E8E8 100%)'
: index === 2
? 'linear-gradient(135deg, #CD7F32 0%, #E8A64C 100%)'
: '#2B3139',
color: index < 3 ? '#000' : '#848E9C',
: '#E8E2D5',
color: index === 0 ? '#fff' : index < 3 ? '#000' : '#8A8478',
}}
>
{index + 1}
@@ -266,7 +266,7 @@ export function CompetitionPage() {
<div>
<div
className="font-bold text-sm"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
{trader.trader_name}
</div>
@@ -284,12 +284,12 @@ export function CompetitionPage() {
<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' }}>
<div className="text-xs" style={{ color: '#8A8478' }}>
{t('equity', language)}
</div>
<div
className="text-xs md:text-sm font-bold mono"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
{trader.total_equity?.toFixed(2) || '0.00'}
</div>
@@ -297,7 +297,7 @@ export function CompetitionPage() {
{/* P&L */}
<div className="text-right min-w-[70px] md:min-w-[90px]">
<div className="text-xs" style={{ color: '#848E9C' }}>
<div className="text-xs" style={{ color: '#8A8478' }}>
{t('pnl', language)}
</div>
<div
@@ -305,8 +305,8 @@ export function CompetitionPage() {
style={{
color:
(trader.total_pnl ?? 0) >= 0
? '#0ECB81'
: '#F6465D',
? '#2E8B57'
: '#D6433A',
}}
>
{(trader.total_pnl ?? 0) >= 0 ? '+' : ''}
@@ -314,7 +314,7 @@ export function CompetitionPage() {
</div>
<div
className="text-xs mono"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
{(trader.total_pnl ?? 0) >= 0 ? '+' : ''}
{trader.total_pnl?.toFixed(2) || '0.00'}
@@ -323,16 +323,16 @@ export function CompetitionPage() {
{/* Positions */}
<div className="text-right">
<div className="text-xs" style={{ color: '#848E9C' }}>
<div className="text-xs" style={{ color: '#8A8478' }}>
{t('pos', language)}
</div>
<div
className="text-xs md:text-sm font-bold mono"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
{trader.position_count}
</div>
<div className="text-xs" style={{ color: '#848E9C' }}>
<div className="text-xs" style={{ color: '#8A8478' }}>
{trader.margin_used_pct.toFixed(1)}%
</div>
</div>
@@ -344,12 +344,12 @@ export function CompetitionPage() {
style={
trader.is_running
? {
background: 'rgba(14, 203, 129, 0.1)',
color: '#0ECB81',
background: 'rgba(46, 139, 87, 0.1)',
color: '#2E8B57',
}
: {
background: 'rgba(246, 70, 93, 0.1)',
color: '#F6465D',
background: 'rgba(214, 67, 58, 0.1)',
color: '#D6433A',
}
}
>
@@ -368,11 +368,11 @@ export function CompetitionPage() {
{/* 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"
className="bg-nofx-bg-lighter border border-nofx-gold/20 rounded-xl p-6 animate-slide-in"
style={{ animationDelay: '0.3s' }}
>
<h2
className="text-lg font-bold mb-6 flex items-center gap-2 text-white"
className="text-lg font-bold mb-6 flex items-center gap-2 text-nofx-text"
>
{t('headToHead', language)}
</h2>
@@ -399,15 +399,14 @@ export function CompetitionPage() {
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: 'rgba(46, 139, 87, 0.08)',
border: '2px solid rgba(46, 139, 87, 0.3)',
boxShadow: 'none',
}
: {
background: '#0B0E11',
border: '1px solid #2B3139',
boxShadow: '0 1px 4px rgba(0, 0, 0, 0.3)',
background: '#F7F4EC',
border: '1px solid rgba(26,24,19,0.14)',
boxShadow: 'none',
}
}
>
@@ -432,7 +431,7 @@ export function CompetitionPage() {
className="text-lg md:text-2xl font-bold mono mb-1"
style={{
color:
(trader.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D',
(trader.total_pnl ?? 0) >= 0 ? '#2E8B57' : '#D6433A',
}}
>
{trader.total_pnl_pct != null &&
@@ -443,7 +442,7 @@ export function CompetitionPage() {
{hasValidData && isWinning && gap > 0 && (
<div
className="text-xs font-semibold"
style={{ color: '#0ECB81' }}
style={{ color: '#2E8B57' }}
>
{t('leadingBy', language, { gap: gap.toFixed(2) })}
</div>
@@ -451,7 +450,7 @@ export function CompetitionPage() {
{hasValidData && !isWinning && gap < 0 && (
<div
className="text-xs font-semibold"
style={{ color: '#F6465D' }}
style={{ color: '#D6433A' }}
>
{t('behindBy', language, {
gap: Math.abs(gap).toFixed(2),
@@ -461,7 +460,7 @@ export function CompetitionPage() {
{!hasValidData && (
<div
className="text-xs font-semibold"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
</div>

View File

@@ -56,8 +56,8 @@ export function ConfigStatusGrid({
const getExchangeStateMeta = (state: ExchangeAccountState | undefined) => {
if (!state) {
return {
label: language === 'zh' ? '未检查' : 'NOT CHECKED',
className: 'text-zinc-400 border-zinc-700/80 bg-zinc-900/40',
label: language === 'zh' ? 'NOT CHECKED' : 'NOT CHECKED',
className: 'text-nofx-text-muted border-[rgba(26,24,19,0.14)] bg-nofx-bg-deeper',
}
}
@@ -65,32 +65,32 @@ export function ConfigStatusGrid({
case 'ok':
return {
label: state.display_balance || '0',
className: 'text-emerald-300 border-emerald-500/20 bg-emerald-500/10',
className: 'text-nofx-success border-nofx-success/20 bg-nofx-success/10',
}
case 'disabled':
return {
label: language === 'zh' ? '已禁用' : 'DISABLED',
className: 'text-zinc-400 border-zinc-700/80 bg-zinc-900/40',
label: language === 'zh' ? 'DISABLED' : 'DISABLED',
className: 'text-nofx-text-muted border-[rgba(26,24,19,0.14)] bg-nofx-bg-deeper',
}
case 'missing_credentials':
return {
label: language === 'zh' ? '配置不完整' : 'INCOMPLETE',
className: 'text-amber-300 border-amber-500/20 bg-amber-500/10',
label: language === 'zh' ? 'INCOMPLETE' : 'INCOMPLETE',
className: 'text-nofx-gold border-nofx-gold/20 bg-nofx-gold/10',
}
case 'invalid_credentials':
return {
label: language === 'zh' ? '密钥无效' : 'INVALID KEYS',
className: 'text-rose-300 border-rose-500/20 bg-rose-500/10',
label: language === 'zh' ? 'INVALID KEYS' : 'INVALID KEYS',
className: 'text-nofx-danger border-nofx-danger/20 bg-nofx-danger/10',
}
case 'permission_denied':
return {
label: language === 'zh' ? '无余额权限' : 'NO PERMISSION',
className: 'text-orange-300 border-orange-500/20 bg-orange-500/10',
label: language === 'zh' ? 'NO PERMISSION' : 'NO PERMISSION',
className: 'text-nofx-gold border-nofx-gold/20 bg-nofx-gold/10',
}
default:
return {
label: language === 'zh' ? '暂时无法获取' : 'UNAVAILABLE',
className: 'text-zinc-300 border-zinc-600/60 bg-zinc-800/50',
label: language === 'zh' ? 'UNAVAILABLE' : 'UNAVAILABLE',
className: 'text-nofx-text-muted border-[rgba(26,24,19,0.14)] bg-nofx-bg-deeper',
}
}
}
@@ -98,10 +98,10 @@ export function ConfigStatusGrid({
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* AI Models Card */}
<div className="nofx-glass rounded-lg border border-white/5 overflow-hidden">
<div className="px-4 py-3 border-b border-white/5 bg-black/20 flex items-center gap-2 backdrop-blur-sm">
<div className="bg-nofx-bg-lighter rounded-lg border border-[rgba(26,24,19,0.14)] overflow-hidden">
<div className="px-4 py-3 border-b border-[rgba(26,24,19,0.14)] bg-nofx-bg-deeper flex items-center gap-2">
<Brain className="w-4 h-4 text-nofx-gold" />
<h3 className="text-sm font-mono tracking-widest text-zinc-300 uppercase">
<h3 className="text-sm font-mono tracking-widest text-nofx-text uppercase">
{t('aiModels', language)}
</h3>
</div>
@@ -114,7 +114,7 @@ export function ConfigStatusGrid({
key={model.id}
role="button"
tabIndex={0}
className="group relative flex cursor-pointer items-center justify-between rounded-md border border-transparent bg-black/20 p-3 transition-all hover:border-white/10 hover:bg-white/5"
className="group relative flex cursor-pointer items-center justify-between rounded-md border border-transparent bg-nofx-bg-deeper p-3 transition-all hover:border-nofx-gold/20 hover:bg-nofx-bg"
onClick={() => onModelClick(model.id)}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
@@ -125,30 +125,29 @@ export function ConfigStatusGrid({
>
<div className="flex items-center gap-4">
<div className="relative">
<div className="absolute inset-0 bg-indigo-500/20 rounded-full blur-sm group-hover:bg-indigo-500/30 transition-all"></div>
<div className="w-10 h-10 rounded-full flex items-center justify-center bg-black border border-white/10 relative z-10">
<div className="w-10 h-10 rounded-full flex items-center justify-center bg-nofx-bg border border-[rgba(26,24,19,0.14)] relative z-10">
{getModelIcon(model.provider || model.id, { width: 20, height: 20 }) || (
<span className="text-xs font-bold text-indigo-400">{getShortName(model.name)[0]}</span>
<span className="text-xs font-bold text-nofx-accent">{getShortName(model.name)[0]}</span>
)}
</div>
</div>
<div className="min-w-0">
<div className="font-mono text-sm text-zinc-200 group-hover:text-nofx-gold transition-colors">
<div className="font-mono text-sm text-nofx-text group-hover:text-nofx-gold transition-colors">
{getShortName(model.name)}
</div>
<div className="text-[10px] text-zinc-500 font-mono flex items-center gap-2">
<div className="text-[10px] text-nofx-text-muted font-mono flex items-center gap-2">
{model.customModelName || AI_PROVIDER_CONFIG[model.provider]?.defaultModel || ''}
</div>
{model.provider === 'claw402' && (model.balanceUsdc || model.walletAddress) ? (
<div className="mt-1.5 flex flex-wrap items-center gap-2 text-[10px] font-mono">
{model.balanceUsdc ? (
<span className="rounded border border-emerald-500/20 bg-emerald-500/10 px-1.5 py-0.5 text-emerald-400">
<span className="rounded border border-nofx-success/20 bg-nofx-success/10 px-1.5 py-0.5 text-nofx-success">
{model.balanceUsdc} USDC
</span>
) : null}
{model.walletAddress ? (
<span className="rounded border border-sky-500/20 bg-sky-500/10 px-1.5 py-0.5 text-sky-400">
<span className="rounded border border-nofx-gold/20 bg-nofx-gold/10 px-1.5 py-0.5 text-nofx-gold">
{truncateAddress(model.walletAddress)}
</span>
) : null}
@@ -160,14 +159,14 @@ export function ConfigStatusGrid({
<div className="text-right">
{usageInfo.totalCount > 0 ? (
<span className={`text-[10px] font-mono px-2 py-1 rounded border ${usageInfo.runningCount > 0
? 'bg-green-500/10 border-green-500/30 text-green-400'
: 'bg-yellow-500/10 border-yellow-500/30 text-yellow-400'
? 'bg-nofx-success/10 border-nofx-success/30 text-nofx-success'
: 'bg-nofx-gold/10 border-nofx-gold/30 text-nofx-gold'
}`}>
{usageInfo.runningCount}/{usageInfo.totalCount} ACTIVE
</span>
) : (
<span className="text-[10px] font-mono text-zinc-600 uppercase tracking-wider">
{language === 'zh' ? '就绪' : 'STANDBY'}
<span className="text-[10px] font-mono text-nofx-text-muted uppercase tracking-wider">
{language === 'zh' ? 'STANDBY' : 'STANDBY'}
</span>
)}
</div>
@@ -176,19 +175,19 @@ export function ConfigStatusGrid({
})}
{configuredModels.length === 0 && (
<div className="text-center py-10 border border-dashed border-zinc-800 rounded-lg bg-black/20">
<Brain className="w-8 h-8 mx-auto mb-3 text-zinc-700" />
<div className="text-xs font-mono text-zinc-500 uppercase tracking-widest">{t('noModelsConfigured', language)}</div>
<div className="text-center py-10 border border-dashed border-[rgba(26,24,19,0.14)] rounded-lg bg-nofx-bg-deeper">
<Brain className="w-8 h-8 mx-auto mb-3 text-nofx-text-muted" />
<div className="text-xs font-mono text-nofx-text-muted uppercase tracking-widest">{t('noModelsConfigured', language)}</div>
</div>
)}
</div>
</div>
{/* Exchanges Card */}
<div className="nofx-glass rounded-lg border border-white/5 overflow-hidden">
<div className="px-4 py-3 border-b border-white/5 bg-black/20 flex items-center gap-2 backdrop-blur-sm">
<div className="bg-nofx-bg-lighter rounded-lg border border-[rgba(26,24,19,0.14)] overflow-hidden">
<div className="px-4 py-3 border-b border-[rgba(26,24,19,0.14)] bg-nofx-bg-deeper flex items-center gap-2">
<Landmark className="w-4 h-4 text-nofx-gold" />
<h3 className="text-sm font-mono tracking-widest text-zinc-300 uppercase">
<h3 className="text-sm font-mono tracking-widest text-nofx-text uppercase">
{t('exchanges', language)}
</h3>
</div>
@@ -203,7 +202,7 @@ export function ConfigStatusGrid({
key={exchange.id}
role="button"
tabIndex={0}
className="group relative flex cursor-pointer items-center justify-between rounded-md border border-transparent bg-black/20 p-3 transition-all hover:border-white/10 hover:bg-white/5"
className="group relative flex cursor-pointer items-center justify-between rounded-md border border-transparent bg-nofx-bg-deeper p-3 transition-all hover:border-nofx-gold/20 hover:bg-nofx-bg"
onClick={() => onExchangeClick(exchange.id)}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
@@ -214,30 +213,29 @@ export function ConfigStatusGrid({
>
<div className="flex items-center gap-4 min-w-0">
<div className="relative">
<div className="absolute inset-0 bg-yellow-500/20 rounded-full blur-sm group-hover:bg-yellow-500/30 transition-all"></div>
<div className="w-10 h-10 rounded-full flex items-center justify-center bg-black border border-white/10 relative z-10">
<div className="w-10 h-10 rounded-full flex items-center justify-center bg-nofx-bg border border-[rgba(26,24,19,0.14)] relative z-10">
{getExchangeIcon(exchange.exchange_type || exchange.id, { width: 20, height: 20 })}
</div>
</div>
<div className="min-w-0">
<div className="font-mono text-sm text-zinc-200 group-hover:text-nofx-gold transition-colors truncate">
<div className="font-mono text-sm text-nofx-text group-hover:text-nofx-gold transition-colors truncate">
{exchange.exchange_type?.toUpperCase() || getShortName(exchange.name)}
<span className="text-[10px] text-zinc-500 ml-2 border border-zinc-800 px-1 rounded">
<span className="text-[10px] text-nofx-text-muted ml-2 border border-[rgba(26,24,19,0.14)] px-1 rounded">
{exchange.account_name || 'DEFAULT'}
</span>
</div>
<div className="text-[10px] text-zinc-500 font-mono flex items-center gap-2">
<div className="text-[10px] text-nofx-text-muted font-mono flex items-center gap-2">
{exchange.type?.toUpperCase() || 'CEX'}
</div>
<div className="mt-1 flex flex-wrap items-center gap-2 text-[10px] font-mono">
<span className={`rounded border px-1.5 py-0.5 ${stateMeta.className}`}>
{isExchangeAccountStatesLoading && !state
? (language === 'zh' ? '检查中...' : 'CHECKING...')
? (language === 'zh' ? 'CHECKING...' : 'CHECKING...')
: stateMeta.label}
</span>
{state?.status !== 'ok' && state?.error_message ? (
<span className="text-zinc-500 truncate max-w-[220px]">
<span className="text-nofx-text-muted truncate max-w-[220px]">
{state.error_message}
</span>
) : null}
@@ -255,20 +253,20 @@ export function ConfigStatusGrid({
return (
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
<span className="text-[10px] font-mono text-zinc-400 bg-black/40 px-1.5 py-0.5 rounded border border-zinc-800">
<span className="text-[10px] font-mono text-nofx-text-muted bg-nofx-bg-deeper px-1.5 py-0.5 rounded border border-[rgba(26,24,19,0.14)]">
{isVisible ? walletAddr : truncateAddress(walletAddr)}
</span>
<button
onClick={(e) => { e.stopPropagation(); onToggleExchangeAddress(exchange.id) }}
className="text-zinc-600 hover:text-zinc-300"
className="text-nofx-text-muted hover:text-nofx-text"
>
{isVisible ? <EyeOff size={10} /> : <Eye size={10} />}
</button>
<button
onClick={(e) => { e.stopPropagation(); onCopyAddress(`exchange-${exchange.id}`, walletAddr) }}
className="text-zinc-600 hover:text-nofx-gold"
className="text-nofx-text-muted hover:text-nofx-gold"
>
{isCopied ? <Check size={10} className="text-green-500" /> : <Copy size={10} />}
{isCopied ? <Check size={10} className="text-nofx-success" /> : <Copy size={10} />}
</button>
</div>
)
@@ -276,14 +274,14 @@ export function ConfigStatusGrid({
{usageInfo.totalCount > 0 ? (
<span className={`text-[10px] font-mono px-2 py-1 rounded border ${usageInfo.runningCount > 0
? 'bg-green-500/10 border-green-500/30 text-green-400'
: 'bg-yellow-500/10 border-yellow-500/30 text-yellow-400'
? 'bg-nofx-success/10 border-nofx-success/30 text-nofx-success'
: 'bg-nofx-gold/10 border-nofx-gold/30 text-nofx-gold'
}`}>
{usageInfo.runningCount}/{usageInfo.totalCount} ACTIVE
</span>
) : (
<span className="text-[10px] font-mono text-zinc-600 uppercase tracking-wider">
{language === 'zh' ? '就绪' : 'STANDBY'}
<span className="text-[10px] font-mono text-nofx-text-muted uppercase tracking-wider">
{language === 'zh' ? 'STANDBY' : 'STANDBY'}
</span>
)}
</div>
@@ -291,9 +289,9 @@ export function ConfigStatusGrid({
)
})}
{configuredExchanges.length === 0 && (
<div className="text-center py-10 border border-dashed border-zinc-800 rounded-lg bg-black/20">
<Landmark className="w-8 h-8 mx-auto mb-3 text-zinc-700" />
<div className="text-xs font-mono text-zinc-500 uppercase tracking-widest">{t('noExchangesConfigured', language)}</div>
<div className="text-center py-10 border border-dashed border-[rgba(26,24,19,0.14)] rounded-lg bg-nofx-bg-deeper">
<Landmark className="w-8 h-8 mx-auto mb-3 text-nofx-text-muted" />
<div className="text-xs font-mono text-nofx-text-muted uppercase tracking-widest">{t('noExchangesConfigured', language)}</div>
</div>
)}
</div>

View File

@@ -10,12 +10,12 @@ interface DecisionCardProps {
// Action type configuration
const ACTION_CONFIG: Record<string, { color: string; bg: string; icon: string; label: string }> = {
open_long: { color: '#0ECB81', bg: 'rgba(14, 203, 129, 0.15)', icon: '📈', label: 'LONG' },
open_short: { color: '#F6465D', bg: 'rgba(246, 70, 93, 0.15)', icon: '📉', label: 'SHORT' },
close_long: { color: '#F0B90B', bg: 'rgba(240, 185, 11, 0.15)', icon: '💰', label: 'CLOSE' },
close_short: { color: '#F0B90B', bg: 'rgba(240, 185, 11, 0.15)', icon: '💰', label: 'CLOSE' },
hold: { color: '#848E9C', bg: 'rgba(132, 142, 156, 0.15)', icon: '⏸️', label: 'HOLD' },
wait: { color: '#848E9C', bg: 'rgba(132, 142, 156, 0.15)', icon: '⏳', label: 'WAIT' },
open_long: { color: '#2E8B57', bg: 'rgba(46, 139, 87, 0.15)', icon: '📈', label: 'LONG' },
open_short: { color: '#D6433A', bg: 'rgba(214, 67, 58, 0.15)', icon: '📉', label: 'SHORT' },
close_long: { color: '#E0483B', bg: 'rgba(224, 72, 59, 0.15)', icon: '💰', label: 'CLOSE' },
close_short: { color: '#E0483B', bg: 'rgba(224, 72, 59, 0.15)', icon: '💰', label: 'CLOSE' },
hold: { color: '#8A8478', bg: 'rgba(138, 132, 120, 0.15)', icon: '⏸️', label: 'HOLD' },
wait: { color: '#8A8478', bg: 'rgba(138, 132, 120, 0.15)', icon: '⏳', label: 'WAIT' },
}
// Format price with proper decimals
@@ -36,10 +36,10 @@ function calcPctChange(entry: number | undefined, target: number | undefined, is
// Get confidence color
function getConfidenceColor(confidence: number | undefined): string {
if (!confidence) return '#848E9C'
if (confidence >= 80) return '#0ECB81'
if (confidence >= 60) return '#F0B90B'
return '#F6465D'
if (!confidence) return '#8A8478'
if (confidence >= 80) return '#2E8B57'
if (confidence >= 60) return '#E0483B'
return '#D6433A'
}
// Single Action Card Component
@@ -52,9 +52,8 @@ function ActionCard({ action, language, onSymbolClick }: { action: DecisionActio
<div
className="rounded-lg p-4 transition-all duration-200 hover:scale-[1.01]"
style={{
background: 'linear-gradient(135deg, #1E2329 0%, #181C21 100%)',
background: '#F7F4EC',
border: `1px solid ${config.color}33`,
boxShadow: `0 4px 12px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.03)`,
}}
>
{/* Header Row */}
@@ -63,7 +62,7 @@ function ActionCard({ action, language, onSymbolClick }: { action: DecisionActio
<span className="text-xl">{config.icon}</span>
<span
className="font-mono font-bold text-lg cursor-pointer transition-all duration-200 hover:scale-110"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
onClick={() => onSymbolClick?.(action.symbol)}
title="Click to view chart"
>
@@ -92,34 +91,34 @@ function ActionCard({ action, language, onSymbolClick }: { action: DecisionActio
)}
<div
className="w-2 h-2 rounded-full"
style={{ background: action.success ? '#0ECB81' : '#F6465D' }}
style={{ background: action.success ? '#2E8B57' : '#D6433A' }}
/>
</div>
</div>
{/* Trading Details Grid */}
{isOpen && (
<div className="grid grid-cols-4 gap-3 mt-3 pt-3" style={{ borderTop: '1px solid #2B3139' }}>
<div className="grid grid-cols-4 gap-3 mt-3 pt-3" style={{ borderTop: '1px solid rgba(26,24,19,0.14)' }}>
{/* Entry Price */}
<div className="text-center">
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>
<div className="text-xs mb-1" style={{ color: '#8A8478' }}>
{t('entryPrice', language)}
</div>
<div className="font-mono font-semibold" style={{ color: '#EAECEF' }}>
<div className="font-mono font-semibold" style={{ color: '#1A1813' }}>
{formatPrice(action.price)}
</div>
</div>
{/* Stop Loss */}
<div className="text-center">
<div className="text-xs mb-1" style={{ color: '#F6465D' }}>
<div className="text-xs mb-1" style={{ color: '#D6433A' }}>
{t('stopLoss', language)}
</div>
<div className="font-mono font-semibold" style={{ color: '#F6465D' }}>
<div className="font-mono font-semibold" style={{ color: '#D6433A' }}>
{formatPrice(action.stop_loss)}
</div>
{action.stop_loss && action.price && (
<div className="text-xs mt-0.5" style={{ color: '#848E9C' }}>
<div className="text-xs mt-0.5" style={{ color: '#8A8478' }}>
{calcPctChange(action.price, action.stop_loss, isLong)}
</div>
)}
@@ -127,14 +126,14 @@ function ActionCard({ action, language, onSymbolClick }: { action: DecisionActio
{/* Take Profit */}
<div className="text-center">
<div className="text-xs mb-1" style={{ color: '#0ECB81' }}>
<div className="text-xs mb-1" style={{ color: '#2E8B57' }}>
{t('takeProfit', language)}
</div>
<div className="font-mono font-semibold" style={{ color: '#0ECB81' }}>
<div className="font-mono font-semibold" style={{ color: '#2E8B57' }}>
{formatPrice(action.take_profit)}
</div>
{action.take_profit && action.price && (
<div className="text-xs mt-0.5" style={{ color: '#848E9C' }}>
<div className="text-xs mt-0.5" style={{ color: '#8A8478' }}>
{calcPctChange(action.price, action.take_profit, isLong)}
</div>
)}
@@ -142,10 +141,10 @@ function ActionCard({ action, language, onSymbolClick }: { action: DecisionActio
{/* Leverage */}
<div className="text-center">
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>
<div className="text-xs mb-1" style={{ color: '#8A8478' }}>
{t('leverage', language)}
</div>
<div className="font-mono font-semibold" style={{ color: '#F0B90B' }}>
<div className="font-mono font-semibold" style={{ color: '#E0483B' }}>
{action.leverage}x
</div>
</div>
@@ -154,26 +153,26 @@ function ActionCard({ action, language, onSymbolClick }: { action: DecisionActio
{/* Risk/Reward Ratio for open positions */}
{isOpen && action.stop_loss && action.take_profit && action.price && (
<div className="mt-3 pt-3 flex items-center justify-between" style={{ borderTop: '1px solid #2B3139' }}>
<span className="text-xs" style={{ color: '#848E9C' }}>{t('riskReward', language)}</span>
<div className="mt-3 pt-3 flex items-center justify-between" style={{ borderTop: '1px solid rgba(26,24,19,0.14)' }}>
<span className="text-xs" style={{ color: '#8A8478' }}>{t('riskReward', language)}</span>
<div className="flex items-center gap-2">
{(() => {
const slDist = Math.abs(action.price - action.stop_loss)
const tpDist = Math.abs(action.take_profit - action.price)
const ratio = slDist > 0 ? (tpDist / slDist) : 0
const ratioColor = ratio >= 3 ? '#0ECB81' : ratio >= 2 ? '#F0B90B' : '#F6465D'
const ratioColor = ratio >= 3 ? '#2E8B57' : ratio >= 2 ? '#E0483B' : '#D6433A'
return (
<>
<div className="flex gap-1">
<span style={{ color: '#F6465D' }}>1</span>
<span style={{ color: '#848E9C' }}>:</span>
<span style={{ color: '#0ECB81' }}>{ratio.toFixed(1)}</span>
<span style={{ color: '#D6433A' }}>1</span>
<span style={{ color: '#8A8478' }}>:</span>
<span style={{ color: '#2E8B57' }}>{ratio.toFixed(1)}</span>
</div>
<div
className="h-1.5 rounded-full"
style={{
width: '60px',
background: '#2B3139',
background: '#E8E2D5',
}}
>
<div
@@ -193,8 +192,8 @@ function ActionCard({ action, language, onSymbolClick }: { action: DecisionActio
{/* Reasoning */}
{action.reasoning && (
<div className="mt-3 pt-3" style={{ borderTop: '1px solid #2B3139' }}>
<div className="text-xs line-clamp-2" style={{ color: '#848E9C' }}>
<div className="mt-3 pt-3" style={{ borderTop: '1px solid rgba(26,24,19,0.14)' }}>
<div className="text-xs line-clamp-2" style={{ color: '#8A8478' }}>
💡 {action.reasoning}
</div>
</div>
@@ -205,9 +204,9 @@ function ActionCard({ action, language, onSymbolClick }: { action: DecisionActio
<div
className="mt-3 rounded p-2 text-xs"
style={{
background: 'rgba(246, 70, 93, 0.1)',
border: '1px solid rgba(246, 70, 93, 0.3)',
color: '#F6465D',
background: 'rgba(214, 67, 58, 0.1)',
border: '1px solid rgba(214, 67, 58, 0.3)',
color: '#D6433A',
}}
>
{action.error}
@@ -249,9 +248,8 @@ export function DecisionCard({ decision, language, onSymbolClick }: DecisionCard
<div
className="rounded-xl p-5 transition-all duration-300 hover:translate-y-[-2px]"
style={{
border: '1px solid #2B3139',
background: 'linear-gradient(180deg, #1E2329 0%, #181C21 100%)',
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.3)',
border: '1px solid rgba(26,24,19,0.14)',
background: '#F7F4EC',
}}
>
{/* Header */}
@@ -259,15 +257,15 @@ export function DecisionCard({ decision, language, onSymbolClick }: DecisionCard
<div className="flex items-center gap-3">
<div
className="w-10 h-10 rounded-lg flex items-center justify-center"
style={{ background: 'rgba(240, 185, 11, 0.15)' }}
style={{ background: 'rgba(224, 72, 59, 0.15)' }}
>
<span className="text-xl">🤖</span>
</div>
<div>
<div className="font-bold" style={{ color: '#EAECEF' }}>
<div className="font-bold" style={{ color: '#1A1813' }}>
{t('cycle', language)} #{decision.cycle_number}
</div>
<div className="text-xs" style={{ color: '#848E9C' }}>
<div className="text-xs" style={{ color: '#8A8478' }}>
{new Date(decision.timestamp).toLocaleString()}
</div>
</div>
@@ -276,8 +274,8 @@ export function DecisionCard({ decision, language, onSymbolClick }: DecisionCard
className="px-4 py-1.5 rounded-full text-xs font-bold tracking-wider"
style={
decision.success
? { background: 'rgba(14, 203, 129, 0.15)', color: '#0ECB81', border: '1px solid rgba(14, 203, 129, 0.3)' }
: { background: 'rgba(246, 70, 93, 0.15)', color: '#F6465D', border: '1px solid rgba(246, 70, 93, 0.3)' }
? { background: 'rgba(46, 139, 87, 0.15)', color: '#2E8B57', border: '1px solid rgba(46, 139, 87, 0.3)' }
: { background: 'rgba(214, 67, 58, 0.15)', color: '#D6433A', border: '1px solid rgba(214, 67, 58, 0.3)' }
}
>
{t(decision.success ? 'success' : 'failed', language)}
@@ -300,11 +298,11 @@ export function DecisionCard({ decision, language, onSymbolClick }: DecisionCard
<div>
<button
onClick={() => setShowSystemPrompt(!showSystemPrompt)}
className="flex items-center gap-2 text-sm transition-colors w-full justify-between p-2 rounded hover:bg-white/5"
className="flex items-center gap-2 text-sm transition-colors w-full justify-between p-2 rounded hover:bg-nofx-gold/10"
>
<div className="flex items-center gap-2">
<span className="text-base"></span>
<span className="font-semibold" style={{ color: '#a78bfa' }}>
<span className="font-semibold" style={{ color: '#E0483B' }}>
System Prompt
</span>
</div>
@@ -315,7 +313,7 @@ export function DecisionCard({ decision, language, onSymbolClick }: DecisionCard
copyToClipboard(decision.system_prompt, 'System Prompt')
}}
className="text-xs px-2.5 py-1 rounded hover:opacity-80 transition-opacity flex items-center gap-1"
style={{ background: 'rgba(167, 139, 250, 0.2)', color: '#a78bfa', border: '1px solid rgba(167, 139, 250, 0.3)' }}
style={{ background: 'rgba(224, 72, 59, 0.2)', color: '#E0483B', border: '1px solid rgba(224, 72, 59, 0.3)' }}
title="Copy to clipboard"
>
<span>📋</span>
@@ -326,14 +324,14 @@ export function DecisionCard({ decision, language, onSymbolClick }: DecisionCard
downloadAsFile(decision.system_prompt, `system-prompt-cycle-${decision.cycle_number}.txt`)
}}
className="text-xs px-2.5 py-1 rounded hover:opacity-80 transition-opacity flex items-center gap-1"
style={{ background: 'rgba(167, 139, 250, 0.2)', color: '#a78bfa', border: '1px solid rgba(167, 139, 250, 0.3)' }}
style={{ background: 'rgba(224, 72, 59, 0.2)', color: '#E0483B', border: '1px solid rgba(224, 72, 59, 0.3)' }}
title="Download as file"
>
<span>💾</span>
</button>
<span
className="text-xs px-2 py-0.5 rounded"
style={{ background: 'rgba(167, 139, 250, 0.15)', color: '#a78bfa' }}
style={{ background: 'rgba(224, 72, 59, 0.15)', color: '#E0483B' }}
>
{showSystemPrompt ? t('collapse', language) : t('expand', language)}
</span>
@@ -343,9 +341,9 @@ export function DecisionCard({ decision, language, onSymbolClick }: DecisionCard
<div
className="mt-2 rounded-lg p-4 text-sm font-mono whitespace-pre-wrap max-h-96 overflow-y-auto"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
background: '#E8E2D5',
border: '1px solid rgba(26,24,19,0.14)',
color: '#1A1813',
}}
>
{decision.system_prompt}
@@ -359,11 +357,11 @@ export function DecisionCard({ decision, language, onSymbolClick }: DecisionCard
<div>
<button
onClick={() => setShowInputPrompt(!showInputPrompt)}
className="flex items-center gap-2 text-sm transition-colors w-full justify-between p-2 rounded hover:bg-white/5"
className="flex items-center gap-2 text-sm transition-colors w-full justify-between p-2 rounded hover:bg-nofx-gold/10"
>
<div className="flex items-center gap-2">
<span className="text-base">📥</span>
<span className="font-semibold" style={{ color: '#60a5fa' }}>
<span className="font-semibold" style={{ color: '#E0483B' }}>
User Prompt
</span>
</div>
@@ -374,7 +372,7 @@ export function DecisionCard({ decision, language, onSymbolClick }: DecisionCard
copyToClipboard(decision.input_prompt, 'User Prompt')
}}
className="text-xs px-2.5 py-1 rounded hover:opacity-80 transition-opacity flex items-center gap-1"
style={{ background: 'rgba(96, 165, 250, 0.2)', color: '#60a5fa', border: '1px solid rgba(96, 165, 250, 0.3)' }}
style={{ background: 'rgba(224, 72, 59, 0.2)', color: '#E0483B', border: '1px solid rgba(224, 72, 59, 0.3)' }}
title="Copy to clipboard"
>
<span>📋</span>
@@ -385,14 +383,14 @@ export function DecisionCard({ decision, language, onSymbolClick }: DecisionCard
downloadAsFile(decision.input_prompt, `user-prompt-cycle-${decision.cycle_number}.txt`)
}}
className="text-xs px-2.5 py-1 rounded hover:opacity-80 transition-opacity flex items-center gap-1"
style={{ background: 'rgba(96, 165, 250, 0.2)', color: '#60a5fa', border: '1px solid rgba(96, 165, 250, 0.3)' }}
style={{ background: 'rgba(224, 72, 59, 0.2)', color: '#E0483B', border: '1px solid rgba(224, 72, 59, 0.3)' }}
title="Download as file"
>
<span>💾</span>
</button>
<span
className="text-xs px-2 py-0.5 rounded"
style={{ background: 'rgba(96, 165, 250, 0.15)', color: '#60a5fa' }}
style={{ background: 'rgba(224, 72, 59, 0.15)', color: '#E0483B' }}
>
{showInputPrompt ? t('collapse', language) : t('expand', language)}
</span>
@@ -402,9 +400,9 @@ export function DecisionCard({ decision, language, onSymbolClick }: DecisionCard
<div
className="mt-2 rounded-lg p-4 text-sm font-mono whitespace-pre-wrap max-h-96 overflow-y-auto"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
background: '#E8E2D5',
border: '1px solid rgba(26,24,19,0.14)',
color: '#1A1813',
}}
>
{decision.input_prompt}
@@ -418,17 +416,17 @@ export function DecisionCard({ decision, language, onSymbolClick }: DecisionCard
<div>
<button
onClick={() => setShowCoT(!showCoT)}
className="flex items-center gap-2 text-sm transition-colors w-full justify-between p-2 rounded hover:bg-white/5"
className="flex items-center gap-2 text-sm transition-colors w-full justify-between p-2 rounded hover:bg-nofx-gold/10"
>
<div className="flex items-center gap-2">
<span className="text-base">🧠</span>
<span className="font-semibold" style={{ color: '#F0B90B' }}>
<span className="font-semibold" style={{ color: '#E0483B' }}>
{t('aiThinking', language)}
</span>
</div>
<span
className="text-xs px-2 py-0.5 rounded"
style={{ background: 'rgba(240, 185, 11, 0.15)', color: '#F0B90B' }}
style={{ background: 'rgba(224, 72, 59, 0.15)', color: '#E0483B' }}
>
{showCoT ? t('collapse', language) : t('expand', language)}
</span>
@@ -437,9 +435,9 @@ export function DecisionCard({ decision, language, onSymbolClick }: DecisionCard
<div
className="mt-2 rounded-lg p-4 text-sm font-mono whitespace-pre-wrap max-h-96 overflow-y-auto"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
background: '#E8E2D5',
border: '1px solid rgba(26,24,19,0.14)',
color: '#1A1813',
}}
>
{decision.cot_trace}
@@ -453,10 +451,10 @@ export function DecisionCard({ decision, language, onSymbolClick }: DecisionCard
{decision.execution_log && decision.execution_log.length > 0 && (
<div
className="rounded-lg p-3 mt-4 text-xs font-mono space-y-1"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
style={{ background: '#E8E2D5', border: '1px solid rgba(26,24,19,0.14)' }}
>
{decision.execution_log.map((log, index) => (
<div key={`${log}-${index}`} style={{ color: '#EAECEF' }}>
<div key={`${log}-${index}`} style={{ color: '#1A1813' }}>
{log}
</div>
))}
@@ -468,9 +466,9 @@ export function DecisionCard({ decision, language, onSymbolClick }: DecisionCard
<div
className="rounded-lg p-3 mt-4 text-sm"
style={{
background: 'rgba(246, 70, 93, 0.1)',
border: '1px solid rgba(246, 70, 93, 0.4)',
color: '#F6465D',
background: 'rgba(214, 67, 58, 0.1)',
border: '1px solid rgba(214, 67, 58, 0.4)',
color: '#D6433A',
}}
>
{decision.error_message}

View File

@@ -71,15 +71,15 @@ function StepIndicator({ currentStep, labels }: { currentStep: number; labels: s
<div
className="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold transition-all"
style={{
background: index < currentStep ? '#0ECB81' : index === currentStep ? '#F0B90B' : '#2B3139',
color: index <= currentStep ? '#000' : '#848E9C',
background: index < currentStep ? '#2E8B57' : index === currentStep ? '#E0483B' : '#E8E2D5',
color: index <= currentStep ? '#fff' : '#8A8478',
}}
>
{index < currentStep ? <Check className="w-4 h-4" /> : index + 1}
</div>
<span
className="text-xs font-medium hidden sm:block"
style={{ color: index === currentStep ? '#EAECEF' : '#848E9C' }}
style={{ color: index === currentStep ? '#1A1813' : '#8A8478' }}
>
{label}
</span>
@@ -87,7 +87,7 @@ function StepIndicator({ currentStep, labels }: { currentStep: number; labels: s
{index < labels.length - 1 && (
<div
className="w-8 h-0.5 mx-1"
style={{ background: index < currentStep ? '#0ECB81' : '#2B3139' }}
style={{ background: index < currentStep ? '#2E8B57' : '#E8E2D5' }}
/>
)}
</React.Fragment>
@@ -115,8 +115,8 @@ function ExchangeCard({
disabled={disabled}
className="flex flex-col items-center gap-2 p-4 rounded-xl transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100"
style={{
background: selected ? 'rgba(240, 185, 11, 0.15)' : '#0B0E11',
border: selected ? '2px solid #F0B90B' : '2px solid #2B3139',
background: selected ? 'rgba(224, 72, 59, 0.15)' : '#F7F4EC',
border: selected ? '2px solid #E0483B' : '2px solid rgba(26,24,19,0.14)',
}}
>
<div className="relative">
@@ -124,20 +124,20 @@ function ExchangeCard({
{selected && (
<div
className="absolute -top-1 -right-1 w-5 h-5 rounded-full flex items-center justify-center"
style={{ background: '#0ECB81' }}
style={{ background: '#2E8B57' }}
>
<Check className="w-3 h-3 text-black" />
<Check className="w-3 h-3 text-white" />
</div>
)}
</div>
<span className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
<span className="text-sm font-semibold" style={{ color: '#1A1813' }}>
{getShortName(template.name)}
</span>
<span
className="text-xs px-2 py-0.5 rounded-full"
style={{
background: template.type === 'cex' ? 'rgba(240, 185, 11, 0.2)' : 'rgba(139, 92, 246, 0.2)',
color: template.type === 'cex' ? '#F0B90B' : '#A78BFA',
background: template.type === 'cex' ? 'rgba(224, 72, 59, 0.2)' : 'rgba(224, 72, 59, 0.2)',
color: template.type === 'cex' ? '#E0483B' : '#E0483B',
}}
>
{template.type.toUpperCase()}
@@ -320,7 +320,7 @@ export function ExchangeConfigModal({
if (!apiKey.trim() || !secretKey.trim() || !passphrase.trim()) return
await onSave(exchangeId, exchangeType, trimmedAccountName, apiKey.trim(), secretKey.trim(), passphrase.trim(), testnet)
} else if (currentExchangeType === 'hyperliquid') {
toast.error(language === 'zh' ? 'Hyperliquid 请使用钱包授权流程连接。' : 'Use the wallet authorization flow to connect Hyperliquid.')
toast.error(language === 'zh' ? 'Use the wallet authorization flow to connect Hyperliquid.' : 'Use the wallet authorization flow to connect Hyperliquid.')
return
} else if (currentExchangeType === 'aster') {
if (!asterUser.trim() || !asterSigner.trim() || !asterPrivateKey.trim()) return
@@ -345,17 +345,17 @@ export function ExchangeConfigModal({
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4 overflow-y-auto backdrop-blur-sm">
<div
className="rounded-2xl w-full max-w-2xl relative my-8 shadow-2xl"
style={{ background: 'linear-gradient(180deg, #1E2329 0%, #181A20 100%)', maxHeight: 'calc(100vh - 4rem)' }}
style={{ background: '#F7F4EC', maxHeight: 'calc(100vh - 4rem)' }}
>
{/* Header */}
<div className="flex items-center justify-between p-6 pb-2">
<div className="flex items-center gap-3">
{currentStep > 0 && !editingExchangeId && (
<button type="button" onClick={handleBack} className="p-2 rounded-lg hover:bg-white/10 transition-colors">
<ChevronLeft className="w-5 h-5" style={{ color: '#848E9C' }} />
<button type="button" onClick={handleBack} className="p-2 rounded-lg hover:bg-black/5 transition-colors">
<ChevronLeft className="w-5 h-5" style={{ color: '#8A8478' }} />
</button>
)}
<h3 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
<h3 className="text-xl font-bold" style={{ color: '#1A1813' }}>
{editingExchangeId ? t('editExchange', language) : t('addExchange', language)}
</h3>
</div>
@@ -365,7 +365,7 @@ export function ExchangeConfigModal({
type="button"
onClick={() => setShowGuide(true)}
className="px-3 py-2 rounded-lg text-sm font-semibold transition-all hover:scale-105 flex items-center gap-2"
style={{ background: 'rgba(240, 185, 11, 0.1)', color: '#F0B90B' }}
style={{ background: 'rgba(224, 72, 59, 0.1)', color: '#E0483B' }}
>
<BookOpen className="w-4 h-4" />
{t('viewGuide', language)}
@@ -375,13 +375,13 @@ export function ExchangeConfigModal({
<button
type="button"
onClick={() => onDelete(editingExchangeId)}
className="p-2 rounded-lg hover:bg-red-500/20 transition-colors"
style={{ color: '#F6465D' }}
className="p-2 rounded-lg hover:bg-nofx-danger/20 transition-colors"
style={{ color: '#D6433A' }}
>
<Trash2 className="w-4 h-4" />
</button>
)}
<button type="button" onClick={onClose} className="p-2 rounded-lg hover:bg-white/10 transition-colors" style={{ color: '#848E9C' }}>
<button type="button" onClick={onClose} className="p-2 rounded-lg hover:bg-black/5 transition-colors" style={{ color: '#8A8478' }}>
</button>
</div>
@@ -401,7 +401,7 @@ export function ExchangeConfigModal({
<div className="space-y-6">
{/* WebCrypto Check */}
<div className="space-y-2">
<div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wide" style={{ color: '#848E9C' }}>
<div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wide" style={{ color: '#8A8478' }}>
<Shield className="w-4 h-4" />
{t('environmentSteps.checkTitle', language)}
</div>
@@ -410,13 +410,13 @@ export function ExchangeConfigModal({
{/* Exchange Grid */}
<div className="space-y-4">
<div className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
<div className="text-sm font-semibold" style={{ color: '#1A1813' }}>
{t('exchangeConfig.chooseExchange', language)}
</div>
{/* CEX */}
<div className="space-y-3">
<div className="text-xs font-medium uppercase tracking-wide" style={{ color: '#F0B90B' }}>
<div className="text-xs font-medium uppercase tracking-wide" style={{ color: '#E0483B' }}>
{t('exchangeConfig.centralizedExchanges', language)}
</div>
<div className="grid grid-cols-3 sm:grid-cols-5 gap-3">
@@ -434,7 +434,7 @@ export function ExchangeConfigModal({
{/* DEX */}
<div className="space-y-3">
<div className="text-xs font-medium uppercase tracking-wide" style={{ color: '#A78BFA' }}>
<div className="text-xs font-medium uppercase tracking-wide" style={{ color: '#E0483B' }}>
{t('exchangeConfig.decentralizedExchanges', language)}
</div>
<div className="grid grid-cols-3 sm:grid-cols-5 gap-3">
@@ -457,13 +457,13 @@ export function ExchangeConfigModal({
{(currentStep === 1 || editingExchangeId) && selectedTemplate && (
<form onSubmit={handleSubmit} className="space-y-5">
{/* Selected Exchange Header */}
<div className="p-4 rounded-xl flex items-center gap-4" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
<div className="p-4 rounded-xl flex items-center gap-4" style={{ background: '#F1ECE2', border: '1px solid rgba(26,24,19,0.14)' }}>
{getExchangeIcon(selectedTemplate.exchange_type, { width: 48, height: 48 })}
<div className="flex-1">
<div className="font-semibold text-lg" style={{ color: '#EAECEF' }}>
<div className="font-semibold text-lg" style={{ color: '#1A1813' }}>
{getShortName(selectedTemplate.name)}
</div>
<div className="text-xs" style={{ color: '#848E9C' }}>
<div className="text-xs" style={{ color: '#8A8478' }}>
{selectedTemplate.type.toUpperCase()} {selectedTemplate.exchange_type}
</div>
</div>
@@ -472,14 +472,14 @@ export function ExchangeConfigModal({
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-4 py-2 rounded-lg transition-all hover:scale-105"
style={{ background: 'rgba(240, 185, 11, 0.1)', border: '1px solid rgba(240, 185, 11, 0.3)' }}
style={{ background: 'rgba(224, 72, 59, 0.1)', border: '1px solid rgba(224, 72, 59, 0.3)' }}
>
<UserPlus className="w-4 h-4" style={{ color: '#F0B90B' }} />
<span className="text-sm font-medium" style={{ color: '#F0B90B' }}>
<UserPlus className="w-4 h-4" style={{ color: '#E0483B' }} />
<span className="text-sm font-medium" style={{ color: '#E0483B' }}>
{t('exchangeConfig.register', language)}
</span>
{exchangeRegistrationLinks[currentExchangeType || '']?.hasReferral && (
<span className="text-xs px-1.5 py-0.5 rounded" style={{ background: 'rgba(14, 203, 129, 0.2)', color: '#0ECB81' }}>
<span className="text-xs px-1.5 py-0.5 rounded" style={{ background: 'rgba(46, 139, 87, 0.2)', color: '#2E8B57' }}>
{t('exchangeConfig.bonus', language)}
</span>
)}
@@ -488,8 +488,8 @@ export function ExchangeConfigModal({
{/* Account Name */}
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
<Key className="w-4 h-4" style={{ color: '#F0B90B' }} />
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#1A1813' }}>
<Key className="w-4 h-4" style={{ color: '#E0483B' }} />
{t('exchangeConfig.accountName', language)} *
</label>
<input
@@ -498,7 +498,7 @@ export function ExchangeConfigModal({
onChange={(e) => setAccountName(e.target.value)}
placeholder={t('exchangeConfig.accountNamePlaceholder', language)}
className="w-full px-4 py-3 rounded-xl text-base"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
style={{ background: '#F1ECE2', border: '1px solid rgba(26,24,19,0.14)', color: '#1A1813' }}
required
/>
</div>
@@ -509,26 +509,26 @@ export function ExchangeConfigModal({
{currentExchangeType === 'binance' && (
<div
className="p-4 rounded-xl cursor-pointer transition-colors"
style={{ background: '#1a3a52', border: '1px solid #2b5278' }}
style={{ background: 'rgba(224, 72, 59, 0.08)', border: '1px solid rgba(224, 72, 59, 0.2)' }}
onClick={() => setShowBinanceGuide(!showBinanceGuide)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span style={{ color: '#58a6ff' }}></span>
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>
<span style={{ color: '#E0483B' }}></span>
<span className="text-sm font-medium" style={{ color: '#1A1813' }}>
{t('exchangeConfig.useBinanceFuturesApi', language)}
</span>
</div>
<span style={{ color: '#8b949e' }}>{showBinanceGuide ? '▲' : '▼'}</span>
<span style={{ color: '#8A8478' }}>{showBinanceGuide ? '▲' : '▼'}</span>
</div>
{showBinanceGuide && (
<div className="mt-3 pt-3 text-sm" style={{ borderTop: '1px solid #2b5278', color: '#c9d1d9' }}>
<div className="mt-3 pt-3 text-sm" style={{ borderTop: '1px solid rgba(224, 72, 59, 0.2)', color: '#1A1813' }}>
<a
href="https://www.binance.com/zh-CN/support/faq/how-to-create-api-keys-on-binance-360002502072"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 hover:underline"
style={{ color: '#58a6ff' }}
style={{ color: '#E0483B' }}
onClick={(e) => e.stopPropagation()}
>
{t('exchangeConfig.viewTutorial', language)} <ExternalLink className="w-3 h-3" />
@@ -541,22 +541,22 @@ export function ExchangeConfigModal({
{editingExchangeId && selectedExchange && (
<div
className="p-3 rounded-xl text-xs"
style={{ background: 'rgba(14, 203, 129, 0.08)', border: '1px solid rgba(14, 203, 129, 0.2)', color: '#9FE8C5' }}
style={{ background: 'rgba(46, 139, 87, 0.08)', border: '1px solid rgba(46, 139, 87, 0.2)', color: '#2E8B57' }}
>
Saved credential status:
{' '}
API Key {selectedExchange.has_api_key ? '已配置' : '未配置'}
API Key {selectedExchange.has_api_key ? 'configured' : 'not configured'}
{' · '}
Secret {selectedExchange.has_secret_key ? '已配置' : '未配置'}
Secret {selectedExchange.has_secret_key ? 'configured' : 'not configured'}
{(currentExchangeType === 'okx' || currentExchangeType === 'bitget' || currentExchangeType === 'kucoin')
? ` · Passphrase ${selectedExchange.has_passphrase ? '已配置' : '未配置'}`
? ` · Passphrase ${selectedExchange.has_passphrase ? 'configured' : 'not configured'}`
: ''}
</div>
)}
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
<Key className="w-4 h-4" style={{ color: '#F0B90B' }} />
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#1A1813' }}>
<Key className="w-4 h-4" style={{ color: '#E0483B' }} />
{t('apiKey', language)}
</label>
<input
@@ -565,18 +565,18 @@ export function ExchangeConfigModal({
onChange={(e) => setApiKey(e.target.value)}
placeholder={
editingExchangeId && selectedExchange?.has_api_key
? '已保存,如需更换请重新输入'
? 'Saved. Re-enter to replace.'
: t('enterAPIKey', language)
}
className="w-full px-4 py-3 rounded-xl"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
style={{ background: '#F1ECE2', border: '1px solid rgba(26,24,19,0.14)', color: '#1A1813' }}
required
/>
</div>
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
<Shield className="w-4 h-4" style={{ color: '#F0B90B' }} />
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#1A1813' }}>
<Shield className="w-4 h-4" style={{ color: '#E0483B' }} />
{t('secretKey', language)}
</label>
<input
@@ -585,19 +585,19 @@ export function ExchangeConfigModal({
onChange={(e) => setSecretKey(e.target.value)}
placeholder={
editingExchangeId && selectedExchange?.has_secret_key
? '已保存,如需更换请重新输入'
? 'Saved. Re-enter to replace.'
: t('enterSecretKey', language)
}
className="w-full px-4 py-3 rounded-xl"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
style={{ background: '#F1ECE2', border: '1px solid rgba(26,24,19,0.14)', color: '#1A1813' }}
required
/>
</div>
{(currentExchangeType === 'okx' || currentExchangeType === 'bitget' || currentExchangeType === 'kucoin') && (
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
<Key className="w-4 h-4" style={{ color: '#F0B90B' }} />
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#1A1813' }}>
<Key className="w-4 h-4" style={{ color: '#E0483B' }} />
{t('passphrase', language)}
</label>
<input
@@ -606,34 +606,34 @@ export function ExchangeConfigModal({
onChange={(e) => setPassphrase(e.target.value)}
placeholder={
editingExchangeId && selectedExchange?.has_passphrase
? '已保存,如需更换请重新输入'
? 'Saved. Re-enter to replace.'
: t('enterPassphrase', language)
}
className="w-full px-4 py-3 rounded-xl"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
style={{ background: '#F1ECE2', border: '1px solid rgba(26,24,19,0.14)', color: '#1A1813' }}
required
/>
</div>
)}
{currentExchangeType === 'binance' && (
<div className="p-4 rounded-xl" style={{ background: 'rgba(240, 185, 11, 0.1)', border: '1px solid rgba(240, 185, 11, 0.2)' }}>
<div className="text-sm font-semibold mb-2" style={{ color: '#F0B90B' }}>
<div className="p-4 rounded-xl" style={{ background: 'rgba(224, 72, 59, 0.1)', border: '1px solid rgba(224, 72, 59, 0.2)' }}>
<div className="text-sm font-semibold mb-2" style={{ color: '#E0483B' }}>
{t('whitelistIP', language)}
</div>
<div className="text-xs mb-3" style={{ color: '#848E9C' }}>
<div className="text-xs mb-3" style={{ color: '#8A8478' }}>
{t('whitelistIPDesc', language)}
</div>
{loadingIP ? (
<div className="text-xs" style={{ color: '#848E9C' }}>{t('loadingServerIP', language)}</div>
<div className="text-xs" style={{ color: '#8A8478' }}>{t('loadingServerIP', language)}</div>
) : serverIP?.public_ip ? (
<div className="flex items-center gap-2 p-3 rounded-lg" style={{ background: '#0B0E11' }}>
<code className="flex-1 text-sm font-mono" style={{ color: '#F0B90B' }}>{serverIP.public_ip}</code>
<div className="flex items-center gap-2 p-3 rounded-lg" style={{ background: '#F1ECE2' }}>
<code className="flex-1 text-sm font-mono" style={{ color: '#E0483B' }}>{serverIP.public_ip}</code>
<button
type="button"
onClick={() => handleCopyIP(serverIP.public_ip)}
className="flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all hover:scale-105"
style={{ background: 'rgba(240, 185, 11, 0.2)', color: '#F0B90B' }}
style={{ background: 'rgba(224, 72, 59, 0.2)', color: '#E0483B' }}
>
<Copy className="w-3 h-3" />
{copiedIP ? t('ipCopied', language) : t('copyIP', language)}
@@ -648,41 +648,41 @@ export function ExchangeConfigModal({
{/* Aster Fields */}
{currentExchangeType === 'aster' && (
<>
<div className="p-4 rounded-xl" style={{ background: 'rgba(139, 92, 246, 0.1)', border: '1px solid rgba(139, 92, 246, 0.3)' }}>
<div className="p-4 rounded-xl" style={{ background: 'rgba(224, 72, 59, 0.1)', border: '1px solid rgba(224, 72, 59, 0.3)' }}>
<div className="flex items-start gap-2">
<span style={{ fontSize: '16px' }}>🔐</span>
<div>
<div className="text-sm font-semibold mb-1" style={{ color: '#A78BFA' }}>{t('asterApiProTitle', language)}</div>
<div className="text-xs" style={{ color: '#848E9C' }}>{t('asterApiProDesc', language)}</div>
<div className="text-sm font-semibold mb-1" style={{ color: '#E0483B' }}>{t('asterApiProTitle', language)}</div>
<div className="text-xs" style={{ color: '#8A8478' }}>{t('asterApiProDesc', language)}</div>
</div>
</div>
</div>
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#1A1813' }}>
{t('asterUserLabel', language)}
<Tooltip content={t('asterUserDesc', language)}>
<HelpCircle className="w-4 h-4 cursor-help" style={{ color: '#A78BFA' }} />
<HelpCircle className="w-4 h-4 cursor-help" style={{ color: '#E0483B' }} />
</Tooltip>
</label>
<input type="text" value={asterUser} onChange={(e) => setAsterUser(e.target.value)} placeholder={t('enterAsterUser', language)} className="w-full px-4 py-3 rounded-xl" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} required />
<input type="text" value={asterUser} onChange={(e) => setAsterUser(e.target.value)} placeholder={t('enterAsterUser', language)} className="w-full px-4 py-3 rounded-xl" style={{ background: '#F1ECE2', border: '1px solid rgba(26,24,19,0.14)', color: '#1A1813' }} required />
</div>
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#1A1813' }}>
{t('asterSignerLabel', language)}
<Tooltip content={t('asterSignerDesc', language)}>
<HelpCircle className="w-4 h-4 cursor-help" style={{ color: '#A78BFA' }} />
<HelpCircle className="w-4 h-4 cursor-help" style={{ color: '#E0483B' }} />
</Tooltip>
</label>
<input type="text" value={asterSigner} onChange={(e) => setAsterSigner(e.target.value)} placeholder={t('enterAsterSigner', language)} className="w-full px-4 py-3 rounded-xl" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} required />
<input type="text" value={asterSigner} onChange={(e) => setAsterSigner(e.target.value)} placeholder={t('enterAsterSigner', language)} className="w-full px-4 py-3 rounded-xl" style={{ background: '#F1ECE2', border: '1px solid rgba(26,24,19,0.14)', color: '#1A1813' }} required />
</div>
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#1A1813' }}>
{t('asterPrivateKeyLabel', language)}
<Tooltip content={t('asterPrivateKeyDesc', language)}>
<HelpCircle className="w-4 h-4 cursor-help" style={{ color: '#A78BFA' }} />
<HelpCircle className="w-4 h-4 cursor-help" style={{ color: '#E0483B' }} />
</Tooltip>
</label>
<input type="password" value={asterPrivateKey} onChange={(e) => setAsterPrivateKey(e.target.value)} placeholder={t('enterAsterPrivateKey', language)} className="w-full px-4 py-3 rounded-xl" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} required />
<input type="password" value={asterPrivateKey} onChange={(e) => setAsterPrivateKey(e.target.value)} placeholder={t('enterAsterPrivateKey', language)} className="w-full px-4 py-3 rounded-xl" style={{ background: '#F1ECE2', border: '1px solid rgba(26,24,19,0.14)', color: '#1A1813' }} required />
</div>
</>
)}
@@ -690,16 +690,16 @@ export function ExchangeConfigModal({
{/* Hyperliquid Wallet Authorization */}
{currentExchangeType === 'hyperliquid' && (
<div className="space-y-4">
<div className="p-4 rounded-xl" style={{ background: 'rgba(127, 231, 204, 0.1)', border: '1px solid rgba(127, 231, 204, 0.3)' }}>
<div className="p-4 rounded-xl" style={{ background: 'rgba(224, 72, 59, 0.1)', border: '1px solid rgba(224, 72, 59, 0.3)' }}>
<div className="flex items-start gap-2">
<span style={{ fontSize: '16px' }}>🔐</span>
<div>
<div className="text-sm font-semibold mb-1" style={{ color: '#7FE7CC' }}>
{language === 'zh' ? 'Hyperliquid 必须走钱包授权' : 'Hyperliquid requires wallet authorization'}
<div className="text-sm font-semibold mb-1" style={{ color: '#E0483B' }}>
{language === 'zh' ? 'Hyperliquid requires wallet authorization' : 'Hyperliquid requires wallet authorization'}
</div>
<div className="text-xs leading-5" style={{ color: '#848E9C' }}>
<div className="text-xs leading-5" style={{ color: '#8A8478' }}>
{language === 'zh'
? '不再支持手动填写私钥/API Key。请用 MetaMaskRabbyOKXCoinbase Wallet 等 EVM 钱包完成连接、Agent 授权和 Builder fee 授权。'
? 'Manual private-key/API-key entry is disabled. Use MetaMask, Rabby, OKX, Coinbase Wallet or another EVM wallet to connect, authorize the agent, and approve the builder fee.'
: 'Manual private-key/API-key entry is disabled. Use MetaMask, Rabby, OKX, Coinbase Wallet or another EVM wallet to connect, authorize the agent, and approve the builder fee.'}
</div>
</div>
@@ -714,45 +714,45 @@ export function ExchangeConfigModal({
{/* Lighter Fields */}
{currentExchangeType === 'lighter' && (
<>
<div className="p-4 rounded-xl" style={{ background: 'rgba(59, 130, 246, 0.1)', border: '1px solid rgba(59, 130, 246, 0.3)' }}>
<div className="p-4 rounded-xl" style={{ background: 'rgba(224, 72, 59, 0.1)', border: '1px solid rgba(224, 72, 59, 0.3)' }}>
<div className="flex items-start gap-2">
<span style={{ fontSize: '16px' }}>🔐</span>
<div>
<div className="text-sm font-semibold mb-1" style={{ color: '#3B82F6' }}>
<div className="text-sm font-semibold mb-1" style={{ color: '#E0483B' }}>
{t('exchangeConfig.lighterApiKeySetup', language)}
</div>
<div className="text-xs" style={{ color: '#848E9C' }}>
<div className="text-xs" style={{ color: '#8A8478' }}>
{t('exchangeConfig.lighterApiKeyDesc', language)}
</div>
</div>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-semibold" style={{ color: '#EAECEF' }}>{t('lighterWalletAddress', language)} *</label>
<input type="text" value={lighterWalletAddr} onChange={(e) => setLighterWalletAddr(e.target.value)} placeholder={t('enterLighterWalletAddress', language)} className="w-full px-4 py-3 rounded-xl" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} required />
<label className="text-sm font-semibold" style={{ color: '#1A1813' }}>{t('lighterWalletAddress', language)} *</label>
<input type="text" value={lighterWalletAddr} onChange={(e) => setLighterWalletAddr(e.target.value)} placeholder={t('enterLighterWalletAddress', language)} className="w-full px-4 py-3 rounded-xl" style={{ background: '#F1ECE2', border: '1px solid rgba(26,24,19,0.14)', color: '#1A1813' }} required />
</div>
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#1A1813' }}>
{t('lighterApiKeyPrivateKey', language)} *
<button type="button" onClick={() => setSecureInputTarget('lighter')} className="text-xs underline" style={{ color: '#3B82F6' }}>{t('secureInputButton', language)}</button>
<button type="button" onClick={() => setSecureInputTarget('lighter')} className="text-xs underline" style={{ color: '#E0483B' }}>{t('secureInputButton', language)}</button>
</label>
<input type="password" value={lighterApiKeyPrivateKey} onChange={(e) => setLighterApiKeyPrivateKey(e.target.value)} placeholder={t('enterLighterApiKeyPrivateKey', language)} className="w-full px-4 py-3 rounded-xl font-mono" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} required />
<input type="password" value={lighterApiKeyPrivateKey} onChange={(e) => setLighterApiKeyPrivateKey(e.target.value)} placeholder={t('enterLighterApiKeyPrivateKey', language)} className="w-full px-4 py-3 rounded-xl font-mono" style={{ background: '#F1ECE2', border: '1px solid rgba(26,24,19,0.14)', color: '#1A1813' }} required />
</div>
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#1A1813' }}>
{t('exchangeConfig.apiKeyIndex', language)}
<Tooltip content={t('exchangeConfig.apiKeyIndexTooltip', language)}>
<HelpCircle className="w-4 h-4 cursor-help" style={{ color: '#3B82F6' }} />
<HelpCircle className="w-4 h-4 cursor-help" style={{ color: '#E0483B' }} />
</Tooltip>
</label>
<input type="number" min={0} max={255} value={lighterApiKeyIndex} onChange={(e) => setLighterApiKeyIndex(parseInt(e.target.value) || 0)} className="w-full px-4 py-3 rounded-xl" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} />
<input type="number" min={0} max={255} value={lighterApiKeyIndex} onChange={(e) => setLighterApiKeyIndex(parseInt(e.target.value) || 0)} className="w-full px-4 py-3 rounded-xl" style={{ background: '#F1ECE2', border: '1px solid rgba(26,24,19,0.14)', color: '#1A1813' }} />
</div>
</>
)}
{/* Buttons */}
<div className="flex gap-3 pt-4">
<button type="button" onClick={handleBack} className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5" style={{ background: '#2B3139', color: '#848E9C' }}>
<button type="button" onClick={handleBack} className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-black/5" style={{ background: '#E8E2D5', color: '#8A8478' }}>
{currentExchangeType === 'hyperliquid' ? t('closeGuide', language) : editingExchangeId ? t('cancel', language) : t('exchangeConfig.back', language)}
</button>
{currentExchangeType !== 'hyperliquid' && (
@@ -760,7 +760,7 @@ export function ExchangeConfigModal({
type="submit"
disabled={isSaving || !accountName.trim()}
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed"
style={{ background: '#F0B90B', color: '#000' }}
style={{ background: '#E0483B', color: '#fff' }}
>
{isSaving ? t('saving', language) : (
<>{t('saveConfig', language)} <ArrowRight className="w-4 h-4" /></>
@@ -776,13 +776,13 @@ export function ExchangeConfigModal({
{/* Binance Guide Modal */}
{showGuide && (
<div className="fixed inset-0 bg-black/75 flex items-center justify-center z-50 p-4" onClick={() => setShowGuide(false)}>
<div className="rounded-2xl p-6 w-full max-w-4xl" style={{ background: '#1E2329' }} onClick={(e) => e.stopPropagation()}>
<div className="rounded-2xl p-6 w-full max-w-4xl" style={{ background: '#F7F4EC' }} onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between mb-4">
<h3 className="text-xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
<BookOpen className="w-6 h-6" style={{ color: '#F0B90B' }} />
<h3 className="text-xl font-bold flex items-center gap-2" style={{ color: '#1A1813' }}>
<BookOpen className="w-6 h-6" style={{ color: '#E0483B' }} />
{t('binanceSetupGuide', language)}
</h3>
<button onClick={() => setShowGuide(false)} className="px-4 py-2 rounded-lg text-sm font-semibold" style={{ background: '#2B3139', color: '#848E9C' }}>
<button onClick={() => setShowGuide(false)} className="px-4 py-2 rounded-lg text-sm font-semibold" style={{ background: '#E8E2D5', color: '#8A8478' }}>
{t('closeGuide', language)}
</button>
</div>

View File

@@ -17,39 +17,39 @@ export function ModelCard({ model, selected, onClick, configured }: ModelCardPro
onClick={onClick}
className="flex flex-col items-center gap-2 p-4 rounded-xl transition-all hover:scale-105"
style={{
background: selected ? 'rgba(139, 92, 246, 0.15)' : '#0B0E11',
border: selected ? '2px solid #8B5CF6' : '2px solid #2B3139',
background: selected ? 'rgba(224, 72, 59, 0.12)' : '#F7F4EC',
border: selected ? '2px solid #E0483B' : '2px solid rgba(26,24,19,0.14)',
}}
>
<div className="relative">
<div className="w-12 h-12 rounded-xl flex items-center justify-center bg-black border border-white/10">
<div className="w-12 h-12 rounded-xl flex items-center justify-center bg-nofx-bg-deeper border border-[rgba(26,24,19,0.14)]">
{getModelIcon(model.provider || model.id, { width: 32, height: 32 }) || (
<span className="text-lg font-bold" style={{ color: '#A78BFA' }}>{model.name[0]}</span>
<span className="text-lg font-bold" style={{ color: '#E0483B' }}>{model.name[0]}</span>
)}
</div>
{selected && (
<div
className="absolute -top-1 -right-1 w-5 h-5 rounded-full flex items-center justify-center"
style={{ background: '#0ECB81' }}
style={{ background: '#2E8B57' }}
>
<Check className="w-3 h-3 text-black" />
<Check className="w-3 h-3 text-white" />
</div>
)}
{configured && !selected && (
<div
className="absolute -top-1 -right-1 w-4 h-4 rounded-full flex items-center justify-center"
style={{ background: '#F0B90B' }}
style={{ background: '#E0483B' }}
>
<Check className="w-2.5 h-2.5 text-black" />
<Check className="w-2.5 h-2.5 text-white" />
</div>
)}
</div>
<span className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
<span className="text-sm font-semibold" style={{ color: '#1A1813' }}>
{getShortName(model.name)}
</span>
<span
className="text-[10px] px-2 py-0.5 rounded-full uppercase tracking-wide"
style={{ background: 'rgba(139, 92, 246, 0.2)', color: '#A78BFA' }}
style={{ background: 'rgba(224, 72, 59, 0.18)', color: '#E0483B' }}
>
{model.provider}
</span>

View File

@@ -99,9 +99,8 @@ export function ModelConfigModal({
return (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4 overflow-y-auto backdrop-blur-sm">
<div
className="rounded-2xl w-full max-w-[52rem] relative my-8 shadow-2xl"
className="rounded-2xl w-full max-w-[52rem] relative my-8 shadow-2xl bg-nofx-bg-lighter"
style={{
background: 'linear-gradient(180deg, #1E2329 0%, #181A20 100%)',
maxHeight: 'calc(100vh - 4rem)',
}}
>
@@ -112,11 +111,11 @@ export function ModelConfigModal({
<button
type="button"
onClick={handleBack}
className="p-2 rounded-lg hover:bg-white/10 transition-colors"
className="p-2 rounded-lg hover:bg-nofx-bg-deeper transition-colors"
>
<svg
className="w-5 h-5"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@@ -130,7 +129,7 @@ export function ModelConfigModal({
</svg>
</button>
)}
<h3 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
<h3 className="text-xl font-bold" style={{ color: '#1A1813' }}>
{editingModelId
? t('editAIModel', language)
: t('addAIModel', language)}
@@ -141,8 +140,8 @@ export function ModelConfigModal({
<button
type="button"
onClick={() => onDelete(editingModelId)}
className="p-2 rounded-lg hover:bg-red-500/20 transition-colors"
style={{ color: '#F6465D' }}
className="p-2 rounded-lg hover:bg-nofx-danger/20 transition-colors"
style={{ color: '#D6433A' }}
>
<Trash2 className="w-4 h-4" />
</button>
@@ -150,8 +149,8 @@ export function ModelConfigModal({
<button
type="button"
onClick={onClose}
className="p-2 rounded-lg hover:bg-white/10 transition-colors"
style={{ color: '#848E9C' }}
className="p-2 rounded-lg hover:bg-nofx-bg-deeper transition-colors"
style={{ color: '#8A8478' }}
>
</button>
@@ -242,7 +241,7 @@ function ModelSelectionStep({
}) {
return (
<div className="space-y-4">
<div className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
<div className="text-sm font-semibold" style={{ color: '#1A1813' }}>
{t('modelConfig.chooseProvider', language)}
</div>
@@ -256,9 +255,8 @@ function ModelSelectionStep({
}}
className="w-full p-5 rounded-xl text-left transition-all hover:scale-[1.01]"
style={{
background:
'linear-gradient(135deg, rgba(37, 99, 235, 0.15) 0%, rgba(139, 92, 246, 0.15) 100%)',
border: '1.5px solid rgba(37, 99, 235, 0.4)',
background: 'rgba(224, 72, 59, 0.10)',
border: '1.5px solid rgba(224, 72, 59, 0.4)',
}}
>
<div className="flex items-center justify-between">
@@ -274,7 +272,7 @@ function ModelSelectionStep({
<div>
<div
className="font-bold text-base"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
Claw402
<a
@@ -284,14 +282,14 @@ function ModelSelectionStep({
onClick={(e) => e.stopPropagation()}
className="ml-1.5 text-[10px] font-normal px-1.5 py-0.5 rounded"
style={{
color: '#60A5FA',
background: 'rgba(96, 165, 250, 0.1)',
color: '#E0483B',
background: 'rgba(224, 72, 59, 0.1)',
}}
>
claw402.ai
</a>
</div>
<div className="text-xs mt-0.5" style={{ color: '#A0AEC0' }}>
<div className="text-xs mt-0.5" style={{ color: '#8A8478' }}>
{t('modelConfig.payPerCall', language)}
</div>
</div>
@@ -302,13 +300,13 @@ function ModelSelectionStep({
) && (
<div
className="w-2 h-2 rounded-full"
style={{ background: '#00E096' }}
style={{ background: '#2E8B57' }}
/>
)}
<div
className="px-3 py-1.5 rounded-full text-xs font-bold"
style={{
background: 'linear-gradient(135deg, #2563EB, #7C3AED)',
background: '#E0483B',
color: '#fff',
}}
>
@@ -320,9 +318,9 @@ function ModelSelectionStep({
<span
className="text-[11px] px-2 py-0.5 rounded-full"
style={{
background: 'rgba(0, 224, 150, 0.1)',
color: '#00E096',
border: '1px solid rgba(0, 224, 150, 0.2)',
background: 'rgba(46, 139, 87, 0.1)',
color: '#2E8B57',
border: '1px solid rgba(46, 139, 87, 0.2)',
}}
>
GPT · Claude · DeepSeek · Gemini · Grok · Qwen · Kimi
@@ -350,14 +348,20 @@ function ModelSelectionStep({
{availableModels.some((m) => m.provider?.startsWith('blockrun')) && (
<>
<div className="flex items-center gap-3 pt-2">
<div className="flex-1 h-px" style={{ background: '#2B3139' }} />
<div
className="flex-1 h-px"
style={{ background: 'rgba(26,24,19,0.14)' }}
/>
<span
className="text-xs font-medium px-2"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
{t('modelConfig.viaBlockrunWallet', language)}
</span>
<div className="flex-1 h-px" style={{ background: '#2B3139' }} />
<div
className="flex-1 h-px"
style={{ background: 'rgba(26,24,19,0.14)' }}
/>
</div>
<div className="grid grid-cols-2 gap-3">
{availableModels
@@ -374,7 +378,7 @@ function ModelSelectionStep({
</div>
</>
)}
<div className="text-xs text-center pt-2" style={{ color: '#848E9C' }}>
<div className="text-xs text-center pt-2" style={{ color: '#8A8478' }}>
{t('modelConfig.modelsConfigured', language)}
</div>
</div>
@@ -534,9 +538,8 @@ function Claw402ConfigForm({
<div
className="p-5 rounded-xl text-center"
style={{
background:
'linear-gradient(135deg, rgba(37, 99, 235, 0.12) 0%, rgba(139, 92, 246, 0.12) 100%)',
border: '1px solid rgba(37, 99, 235, 0.3)',
background: 'rgba(224, 72, 59, 0.08)',
border: '1px solid rgba(224, 72, 59, 0.3)',
}}
>
<div className="w-14 h-14 mx-auto rounded-2xl flex items-center justify-center mb-3 overflow-hidden">
@@ -547,14 +550,14 @@ function Claw402ConfigForm({
target="_blank"
rel="noopener noreferrer"
className="text-lg font-bold inline-flex items-center gap-1.5 hover:underline"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
Claw402{' '}
<span className="text-xs font-normal" style={{ color: '#60A5FA' }}>
<span className="text-xs font-normal" style={{ color: '#E0483B' }}>
</span>
</a>
<div className="text-sm mt-1" style={{ color: '#A0AEC0' }}>
<div className="text-sm mt-1" style={{ color: '#8A8478' }}>
{t('modelConfig.allModelsClaw', language)}
</div>
<div className="flex items-center justify-center gap-3 mt-3 flex-wrap">
@@ -564,8 +567,8 @@ function Claw402ConfigForm({
key={name}
className="text-[11px] px-2 py-0.5 rounded-full"
style={{
background: 'rgba(255,255,255,0.06)',
color: '#A0AEC0',
background: 'rgba(26,24,19,0.06)',
color: '#8A8478',
}}
>
{name}
@@ -579,12 +582,12 @@ function Claw402ConfigForm({
<div className="space-y-3">
<label
className="flex items-center gap-2 text-sm font-semibold"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
<Brain className="w-4 h-4" style={{ color: '#2563EB' }} />
<Brain className="w-4 h-4" style={{ color: '#E0483B' }} />
{t('modelConfig.selectAiModel', language)}
</label>
<div className="text-xs mb-2" style={{ color: '#848E9C' }}>
<div className="text-xs mb-2" style={{ color: '#8A8478' }}>
{t('modelConfig.allModelsUnified', language)}
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
@@ -597,10 +600,12 @@ function Claw402ConfigForm({
onClick={() => onModelNameChange(m.id)}
className="flex items-start gap-2 px-3 py-2.5 rounded-xl text-left transition-all hover:scale-[1.02]"
style={{
background: isSelected ? 'rgba(37, 99, 235, 0.2)' : '#0B0E11',
background: isSelected
? 'rgba(224, 72, 59, 0.12)'
: '#F1ECE2',
border: isSelected
? '1.5px solid #2563EB'
: '1px solid #2B3139',
? '1.5px solid #E0483B'
: '1px solid rgba(26,24,19,0.14)',
}}
>
<span className="text-base mt-0.5">{m.icon}</span>
@@ -608,7 +613,7 @@ function Claw402ConfigForm({
<div className="flex items-center gap-1.5 min-w-0">
<div
className="text-xs font-semibold truncate"
style={{ color: isSelected ? '#60A5FA' : '#EAECEF' }}
style={{ color: isSelected ? '#E0483B' : '#1A1813' }}
>
{m.name}
</div>
@@ -616,9 +621,9 @@ function Claw402ConfigForm({
<span
className="shrink-0 rounded px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-[0.08em]"
style={{
color: '#00E096',
background: 'rgba(0, 224, 150, 0.12)',
border: '1px solid rgba(0, 224, 150, 0.22)',
color: '#2E8B57',
background: 'rgba(46, 139, 87, 0.12)',
border: '1px solid rgba(46, 139, 87, 0.22)',
}}
>
NEW
@@ -627,18 +632,18 @@ function Claw402ConfigForm({
</div>
<div
className="text-[10px] truncate"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
{m.provider} · {m.desc}
</div>
<div className="text-[10px]" style={{ color: '#00E096' }}>
<div className="text-[10px]" style={{ color: '#2E8B57' }}>
~${m.price}/call
</div>
</div>
{isSelected && (
<span
className="text-[10px] mt-1"
style={{ color: '#60A5FA' }}
style={{ color: '#E0483B' }}
>
</span>
@@ -653,11 +658,11 @@ function Claw402ConfigForm({
<div className="space-y-3">
<label
className="flex items-center gap-2 text-sm font-semibold"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
<svg
className="w-4 h-4"
style={{ color: '#2563EB' }}
style={{ color: '#E0483B' }}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@@ -675,27 +680,27 @@ function Claw402ConfigForm({
<div
className="p-3 rounded-xl"
style={{
background: 'rgba(37, 99, 235, 0.06)',
border: '1px solid rgba(37, 99, 235, 0.15)',
background: 'rgba(224, 72, 59, 0.06)',
border: '1px solid rgba(224, 72, 59, 0.15)',
}}
>
<div className="text-xs mb-2" style={{ color: '#A0AEC0' }}>
<div className="text-xs mb-2" style={{ color: '#8A8478' }}>
{t('modelConfig.walletInfo', language)}
</div>
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
<div className="text-xs space-y-1" style={{ color: '#8A8478' }}>
<div className="flex items-center gap-1.5">
<span style={{ color: '#00E096' }}></span>
<span style={{ color: '#2E8B57' }}></span>
{t('modelConfig.exportKey', language)}
</div>
<div className="flex items-center gap-1.5">
<span style={{ color: '#00E096' }}></span>
<span style={{ color: '#2E8B57' }}></span>
{t('modelConfig.dedicatedWallet', language)}
</div>
</div>
</div>
<div className="space-y-1.5">
<div className="text-xs font-medium" style={{ color: '#A0AEC0' }}>
<div className="text-xs font-medium" style={{ color: '#8A8478' }}>
{t('modelConfig.walletPrivateKey', language)}
</div>
<div className="flex gap-2">
@@ -706,13 +711,13 @@ function Claw402ConfigForm({
placeholder="0x..."
className="flex-1 px-4 py-3 rounded-xl font-mono text-sm"
style={{
background: '#0B0E11',
background: '#F1ECE2',
border: keyError
? '1px solid #EF4444'
? '1px solid #D6433A'
: walletAddress
? '1px solid #00E096'
: '1px solid #2B3139',
color: '#EAECEF',
? '1px solid #2E8B57'
: '1px solid rgba(26,24,19,0.14)',
color: '#1A1813',
}}
required={!walletAddress}
/>
@@ -736,13 +741,13 @@ function Claw402ConfigForm({
}}
className="shrink-0 px-3 py-3 rounded-xl text-xs font-semibold transition-all hover:scale-[1.02]"
style={{
background: 'linear-gradient(135deg, #2563EB, #7C3AED)',
background: '#E0483B',
color: '#fff',
border: 'none',
cursor: 'pointer',
}}
>
{language === 'zh' ? '🔑 创建钱包' : '🔑 Create Wallet'}
{language === 'zh' ? '🔑 Create Wallet' : '🔑 Create Wallet'}
</button>
)}
</div>
@@ -752,28 +757,28 @@ function Claw402ConfigForm({
<div
className="p-3 rounded-xl"
style={{
background: 'rgba(239, 68, 68, 0.08)',
border: '1px solid rgba(239, 68, 68, 0.3)',
background: 'rgba(214, 67, 58, 0.08)',
border: '1px solid rgba(214, 67, 58, 0.3)',
}}
>
<div
className="text-xs font-bold mb-2"
style={{ color: '#EF4444' }}
style={{ color: '#D6433A' }}
>
🚨{' '}
{language === 'zh'
? '重要:请立即备份私钥!'
? 'Important: Backup your private key NOW!'
: 'Important: Backup your private key NOW!'}
</div>
<div className="text-[11px] mb-2" style={{ color: '#F87171' }}>
<div className="text-[11px] mb-2" style={{ color: '#D6433A' }}>
{language === 'zh'
? '这是你的钱包私钥,丢失后无法恢复,钱包里的资产将永久丢失。请复制并安全保存。'
? 'This is your wallet private key. If lost, it cannot be recovered and all assets will be permanently lost. Copy and save it securely.'
: 'This is your wallet private key. If lost, it cannot be recovered and all assets will be permanently lost. Copy and save it securely.'}
</div>
<div className="flex items-center gap-2 mb-2">
<code
className="text-[10px] font-mono break-all select-all flex-1 p-2 rounded"
style={{ background: '#0B0E11', color: '#F87171' }}
style={{ background: '#F1ECE2', color: '#D6433A' }}
>
{newWalletKey}
</code>
@@ -786,8 +791,8 @@ function Claw402ConfigForm({
}}
className="shrink-0 text-[10px] px-2 py-1 rounded"
style={{
background: 'rgba(239,68,68,0.15)',
color: '#F87171',
background: 'rgba(214,67,58,0.15)',
color: '#D6433A',
border: 'none',
cursor: 'pointer',
}}
@@ -797,24 +802,24 @@ function Claw402ConfigForm({
</div>
<div
className="text-[10px] space-y-1"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
<div>
{' '}
{language === 'zh'
? '建议保存到密码管理器(1Password / Bitwarden'
? 'Save to a password manager (1Password / Bitwarden)'
: 'Save to a password manager (1Password / Bitwarden)'}
</div>
<div>
{' '}
{language === 'zh'
? '或抄在纸上放安全的地方'
? 'Or write it down and store it safely'
: 'Or write it down and store it safely'}
</div>
<div>
{' '}
{language === 'zh'
? '不要截图发给别人'
? 'Do NOT screenshot or share with anyone'
: 'Do NOT screenshot or share with anyone'}
</div>
</div>
@@ -823,7 +828,7 @@ function Claw402ConfigForm({
<div
className="flex items-start gap-1.5 text-[11px]"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
<span className="mt-px">🔒</span>
<span>{t('modelConfig.privateKeyNote', language)}</span>
@@ -837,7 +842,7 @@ function Claw402ConfigForm({
{validating && (
<div
className="flex items-center gap-2 text-xs"
style={{ color: '#60A5FA' }}
style={{ color: '#E0483B' }}
>
<span className="animate-spin"></span>
{t('modelConfig.validating', language)}
@@ -848,7 +853,7 @@ function Claw402ConfigForm({
{keyError && !validating && (
<div
className="flex items-center gap-2 text-xs"
style={{ color: '#EF4444' }}
style={{ color: '#D6433A' }}
>
<span></span>
{keyError}
@@ -861,12 +866,12 @@ function Claw402ConfigForm({
<div
className="p-2.5 rounded-lg"
style={{
background: 'rgba(96,165,250,0.06)',
border: '1px solid rgba(96,165,250,0.15)',
background: 'rgba(224,72,59,0.06)',
border: '1px solid rgba(224,72,59,0.15)',
}}
>
<div className="flex items-center justify-between mb-1">
<span className="text-[11px]" style={{ color: '#A0AEC0' }}>
<span className="text-[11px]" style={{ color: '#8A8478' }}>
{t('modelConfig.walletAddress', language)}:
</span>
<button
@@ -878,8 +883,8 @@ function Claw402ConfigForm({
}}
className="text-[10px] px-1.5 py-0.5 rounded"
style={{
background: 'rgba(96,165,250,0.1)',
color: '#60A5FA',
background: 'rgba(224,72,59,0.1)',
color: '#E0483B',
border: 'none',
cursor: 'pointer',
}}
@@ -889,17 +894,17 @@ function Claw402ConfigForm({
</div>
<code
className="text-[11px] font-mono block select-all"
style={{ color: '#60A5FA' }}
style={{ color: '#E0483B' }}
>
{walletAddress}
</code>
<div
className="text-[10px] mt-1.5"
style={{ color: '#F59E0B' }}
style={{ color: '#E0483B' }}
>
{' '}
{language === 'zh'
? '请确认这是你的钱包地址(可在 MetaMask 中核对)'
? 'Please confirm this is your wallet address (verify in MetaMask)'
: 'Please confirm this is your wallet address (verify in MetaMask)'}
</div>
</div>
@@ -907,7 +912,7 @@ function Claw402ConfigForm({
<div className="flex items-center gap-2 text-xs">
<span>💰</span>
<span
style={{ color: balanceNum > 0 ? '#00E096' : '#F59E0B' }}
style={{ color: balanceNum > 0 ? '#2E8B57' : '#E0483B' }}
>
{t('modelConfig.usdcBalance', language)}: ${usdcBalance}
</span>
@@ -916,18 +921,18 @@ function Claw402ConfigForm({
onClick={() => setShowDeposit(!showDeposit)}
className="text-[10px] px-2 py-0.5 rounded transition-all"
style={{
background: 'rgba(0,224,150,0.1)',
color: '#00E096',
background: 'rgba(46,139,87,0.1)',
color: '#2E8B57',
border: 'none',
cursor: 'pointer',
}}
>
{showDeposit
? language === 'zh'
? '收起'
? 'Hide'
: 'Hide'
: language === 'zh'
? '💳 充值'
? '💳 Deposit'
: '💳 Deposit'}
</button>
</div>
@@ -936,17 +941,17 @@ function Claw402ConfigForm({
<div
className="p-3 rounded-xl mt-1"
style={{
background: 'rgba(0, 224, 150, 0.04)',
border: '1px solid rgba(0, 224, 150, 0.15)',
background: 'rgba(46, 139, 87, 0.04)',
border: '1px solid rgba(46, 139, 87, 0.15)',
}}
>
<div
className="text-xs font-semibold mb-2"
style={{ color: '#00E096' }}
style={{ color: '#2E8B57' }}
>
💳{' '}
{language === 'zh'
? '充值 USDC (Base )'
? 'Deposit USDC (Base Chain)'
: 'Deposit USDC (Base Chain)'}
</div>
<div className="flex gap-3 items-start mb-3">
@@ -959,15 +964,15 @@ function Claw402ConfigForm({
<div className="flex-1 min-w-0">
<div
className="text-[11px] mb-1"
style={{ color: '#A0AEC0' }}
style={{ color: '#8A8478' }}
>
{language === 'zh'
? '扫码或复制地址转账'
? 'Scan QR or copy address to transfer'
: 'Scan QR or copy address to transfer'}
</div>
<code
className="text-[10px] font-mono break-all select-all block mb-1.5"
style={{ color: '#60A5FA' }}
style={{ color: '#E0483B' }}
>
{walletAddress}
</code>
@@ -980,8 +985,8 @@ function Claw402ConfigForm({
}}
className="text-[10px] px-2 py-0.5 rounded"
style={{
background: 'rgba(96,165,250,0.1)',
color: '#60A5FA',
background: 'rgba(224,72,59,0.1)',
color: '#E0483B',
border: 'none',
cursor: 'pointer',
}}
@@ -992,28 +997,28 @@ function Claw402ConfigForm({
</div>
<div
className="text-[10px] space-y-1"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
<div>
📱{' '}
{language === 'zh'
? '用交易所 App 扫描二维码直接转账'
? 'Scan QR with exchange app to transfer'
: 'Scan QR with exchange app to transfer'}
</div>
<div>
{' '}
{language === 'zh'
? '提币时网络选择 Base'
? 'Choose Base network when withdrawing'
: 'Choose Base network when withdrawing'}
</div>
<div>
{language === 'zh' ? '或跨链桥: ' : 'Or bridge: '}
{language === 'zh' ? 'Or bridge: ' : 'Or bridge: '}
<a
href="https://bridge.base.org"
target="_blank"
rel="noopener noreferrer"
className="underline"
style={{ color: '#60A5FA' }}
style={{ color: '#E0483B' }}
>
bridge.base.org
</a>
@@ -1021,7 +1026,7 @@ function Claw402ConfigForm({
<div>
{' '}
{language === 'zh'
? '最低充值 $1 USDC 即可开始'
? 'Min $1 USDC to start'
: 'Min $1 USDC to start'}
</div>
</div>
@@ -1031,7 +1036,7 @@ function Claw402ConfigForm({
<div
className="flex items-center gap-2 text-xs"
style={{
color: claw402Status === 'ok' ? '#00E096' : '#EF4444',
color: claw402Status === 'ok' ? '#2E8B57' : '#D6433A',
}}
>
<span>{claw402Status === 'ok' ? '🟢' : '🔴'}</span>
@@ -1051,9 +1056,9 @@ function Claw402ConfigForm({
disabled={testing}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-all hover:scale-[1.02] disabled:opacity-50"
style={{
background: 'rgba(37, 99, 235, 0.15)',
border: '1px solid rgba(37, 99, 235, 0.3)',
color: '#60A5FA',
background: 'rgba(224, 72, 59, 0.12)',
border: '1px solid rgba(224, 72, 59, 0.3)',
color: '#E0483B',
}}
>
<span>🔗</span>
@@ -1068,7 +1073,7 @@ function Claw402ConfigForm({
<div
className="flex items-center gap-2 text-xs"
style={{
color: testResult.status === 'ok' ? '#00E096' : '#EF4444',
color: testResult.status === 'ok' ? '#2E8B57' : '#D6433A',
}}
>
<span>{testResult.status === 'ok' ? '✅' : '❌'}</span>
@@ -1083,31 +1088,31 @@ function Claw402ConfigForm({
<div
className="p-4 rounded-xl"
style={{
background: 'rgba(0, 224, 150, 0.05)',
border: '1px solid rgba(0, 224, 150, 0.15)',
background: 'rgba(46, 139, 87, 0.05)',
border: '1px solid rgba(46, 139, 87, 0.15)',
}}
>
<div
className="text-sm font-semibold mb-2 flex items-center gap-2"
style={{ color: '#00E096' }}
style={{ color: '#2E8B57' }}
>
{'💰 ' + t('modelConfig.howToFundUsdc', language)}
</div>
<div className="text-xs space-y-1.5" style={{ color: '#848E9C' }}>
<div className="text-xs space-y-1.5" style={{ color: '#8A8478' }}>
<div className="flex items-start gap-2">
<span className="font-bold" style={{ color: '#A0AEC0' }}>
<span className="font-bold" style={{ color: '#8A8478' }}>
1.
</span>
<span>{t('modelConfig.fundStep1', language)}</span>
</div>
<div className="flex items-start gap-2">
<span className="font-bold" style={{ color: '#A0AEC0' }}>
<span className="font-bold" style={{ color: '#8A8478' }}>
2.
</span>
<span>{t('modelConfig.fundStep2', language)}</span>
</div>
<div className="flex items-start gap-2">
<span className="font-bold" style={{ color: '#A0AEC0' }}>
<span className="font-bold" style={{ color: '#8A8478' }}>
3.
</span>
<span>{t('modelConfig.fundStep3', language)}</span>
@@ -1120,8 +1125,8 @@ function Claw402ConfigForm({
<button
type="button"
onClick={onBack}
className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5"
style={{ background: '#2B3139', color: '#848E9C' }}
className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-nofx-bg-deeper"
style={{ background: '#E8E2D5', color: '#8A8478' }}
>
{editingModelId
? t('cancel', language)
@@ -1132,10 +1137,8 @@ function Claw402ConfigForm({
disabled={!isKeyValid}
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed"
style={{
background: isKeyValid
? 'linear-gradient(135deg, #2563EB, #7C3AED)'
: '#2B3139',
color: '#fff',
background: isKeyValid ? '#E0483B' : '#E8E2D5',
color: isKeyValid ? '#fff' : '#8A8478',
}}
>
{'🚀 ' + t('modelConfig.startTrading', language)}
@@ -1175,23 +1178,26 @@ function StandardProviderConfigForm({
{/* Selected Model Header */}
<div
className="p-4 rounded-xl flex items-center gap-4"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
style={{
background: '#F1ECE2',
border: '1px solid rgba(26,24,19,0.14)',
}}
>
<div className="w-12 h-12 rounded-xl flex items-center justify-center bg-black border border-white/10">
<div className="w-12 h-12 rounded-xl flex items-center justify-center bg-nofx-bg-deeper border border-nofx-gold/20">
{getModelIcon(selectedModel.provider || selectedModel.id, {
width: 32,
height: 32,
}) || (
<span className="text-lg font-bold" style={{ color: '#A78BFA' }}>
<span className="text-lg font-bold" style={{ color: '#E0483B' }}>
{selectedModel.name[0]}
</span>
)}
</div>
<div className="flex-1">
<div className="font-semibold text-lg" style={{ color: '#EAECEF' }}>
<div className="font-semibold text-lg" style={{ color: '#1A1813' }}>
{getShortName(selectedModel.name)}
</div>
<div className="text-xs" style={{ color: '#848E9C' }}>
<div className="text-xs" style={{ color: '#8A8478' }}>
{selectedModel.provider} {' '}
{AI_PROVIDER_CONFIG[selectedModel.provider]?.defaultModel ||
selectedModel.id}
@@ -1204,12 +1210,12 @@ function StandardProviderConfigForm({
rel="noopener noreferrer"
className="flex items-center gap-2 px-4 py-2 rounded-lg transition-all hover:scale-105"
style={{
background: 'rgba(139, 92, 246, 0.1)',
border: '1px solid rgba(139, 92, 246, 0.3)',
background: 'rgba(224, 72, 59, 0.1)',
border: '1px solid rgba(224, 72, 59, 0.3)',
}}
>
<ExternalLink className="w-4 h-4" style={{ color: '#A78BFA' }} />
<span className="text-sm font-medium" style={{ color: '#A78BFA' }}>
<ExternalLink className="w-4 h-4" style={{ color: '#E0483B' }} />
<span className="text-sm font-medium" style={{ color: '#E0483B' }}>
{selectedModel.provider?.startsWith('blockrun')
? t('modelConfig.getStarted', language)
: t('modelConfig.getApiKey', language)}
@@ -1223,13 +1229,13 @@ function StandardProviderConfigForm({
<div
className="p-4 rounded-xl"
style={{
background: 'rgba(246, 70, 93, 0.1)',
border: '1px solid rgba(246, 70, 93, 0.3)',
background: 'rgba(214, 67, 58, 0.1)',
border: '1px solid rgba(214, 67, 58, 0.3)',
}}
>
<div className="flex items-start gap-2">
<span style={{ fontSize: '16px' }}></span>
<div className="text-sm" style={{ color: '#F6465D' }}>
<div className="text-sm" style={{ color: '#D6433A' }}>
{t('kimiApiNote', language)}
</div>
</div>
@@ -1241,24 +1247,24 @@ function StandardProviderConfigForm({
<div
className="p-3 rounded-xl text-xs"
style={{
background: 'rgba(14, 203, 129, 0.08)',
border: '1px solid rgba(14, 203, 129, 0.2)',
color: '#9FE8C5',
background: 'rgba(46, 139, 87, 0.08)',
border: '1px solid rgba(46, 139, 87, 0.2)',
color: '#2E8B57',
}}
>
{selectedModel.has_api_key ? '已配置 API Key' : '未配置 API Key'}
Current model key status:{' '}
{selectedModel.has_api_key ? 'API Key configured' : 'API Key not configured'}
</div>
)}
<div className="space-y-2">
<label
className="flex items-center gap-2 text-sm font-semibold"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
<svg
className="w-4 h-4"
style={{ color: '#A78BFA' }}
style={{ color: '#E0483B' }}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@@ -1280,7 +1286,7 @@ function StandardProviderConfigForm({
onChange={(e) => onApiKeyChange(e.target.value)}
placeholder={
editingModelId && selectedModel.has_api_key
? '已保存,如需更换请重新输入'
? 'Saved. Re-enter to replace.'
: selectedModel.provider === 'blockrun-base'
? '0x... (EVM private key)'
: selectedModel.provider === 'blockrun-sol'
@@ -1289,9 +1295,9 @@ function StandardProviderConfigForm({
}
className="w-full px-4 py-3 rounded-xl"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
background: '#F1ECE2',
border: '1px solid rgba(26,24,19,0.14)',
color: '#1A1813',
}}
required
/>
@@ -1302,11 +1308,11 @@ function StandardProviderConfigForm({
<div className="space-y-2">
<label
className="flex items-center gap-2 text-sm font-semibold"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
<svg
className="w-4 h-4"
style={{ color: '#A78BFA' }}
style={{ color: '#E0483B' }}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@@ -1327,12 +1333,12 @@ function StandardProviderConfigForm({
placeholder={t('customBaseURLPlaceholder', language)}
className="w-full px-4 py-3 rounded-xl"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
background: '#F1ECE2',
border: '1px solid rgba(26,24,19,0.14)',
color: '#1A1813',
}}
/>
<div className="text-xs" style={{ color: '#848E9C' }}>
<div className="text-xs" style={{ color: '#8A8478' }}>
{t('leaveBlankForDefault', language)}
</div>
</div>
@@ -1343,11 +1349,11 @@ function StandardProviderConfigForm({
<div className="space-y-2">
<label
className="flex items-center gap-2 text-sm font-semibold"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
<svg
className="w-4 h-4"
style={{ color: '#A78BFA' }}
style={{ color: '#E0483B' }}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@@ -1368,12 +1374,12 @@ function StandardProviderConfigForm({
placeholder={t('customModelNamePlaceholder', language)}
className="w-full px-4 py-3 rounded-xl"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
background: '#F1ECE2',
border: '1px solid rgba(26,24,19,0.14)',
color: '#1A1813',
}}
/>
<div className="text-xs" style={{ color: '#848E9C' }}>
<div className="text-xs" style={{ color: '#8A8478' }}>
{t('leaveBlankForDefaultModel', language)}
</div>
</div>
@@ -1384,11 +1390,11 @@ function StandardProviderConfigForm({
<div className="space-y-2">
<label
className="flex items-center gap-2 text-sm font-semibold"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
<svg
className="w-4 h-4"
style={{ color: '#A78BFA' }}
style={{ color: '#E0483B' }}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@@ -1413,20 +1419,20 @@ function StandardProviderConfigForm({
className="flex flex-col items-start px-3 py-2 rounded-xl text-left transition-all"
style={{
background: isSelected
? 'rgba(37, 99, 235, 0.2)'
: '#0B0E11',
? 'rgba(224, 72, 59, 0.12)'
: '#F1ECE2',
border: isSelected
? '1px solid #2563EB'
: '1px solid #2B3139',
? '1px solid #E0483B'
: '1px solid rgba(26,24,19,0.14)',
}}
>
<span
className="text-xs font-semibold"
style={{ color: isSelected ? '#60A5FA' : '#EAECEF' }}
style={{ color: isSelected ? '#E0483B' : '#1A1813' }}
>
{m.name}
</span>
<span className="text-[10px]" style={{ color: '#848E9C' }}>
<span className="text-[10px]" style={{ color: '#8A8478' }}>
{m.desc}
</span>
</button>
@@ -1440,18 +1446,18 @@ function StandardProviderConfigForm({
<div
className="p-4 rounded-xl"
style={{
background: 'rgba(139, 92, 246, 0.1)',
border: '1px solid rgba(139, 92, 246, 0.2)',
background: 'rgba(224, 72, 59, 0.08)',
border: '1px solid rgba(224, 72, 59, 0.2)',
}}
>
<div
className="text-sm font-semibold mb-2 flex items-center gap-2"
style={{ color: '#A78BFA' }}
style={{ color: '#E0483B' }}
>
<Brain className="w-4 h-4" />
{t('information', language)}
</div>
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
<div className="text-xs space-y-1" style={{ color: '#8A8478' }}>
<div> {t('modelConfigInfo1', language)}</div>
<div> {t('modelConfigInfo2', language)}</div>
<div> {t('modelConfigInfo3', language)}</div>
@@ -1463,8 +1469,8 @@ function StandardProviderConfigForm({
<button
type="button"
onClick={onBack}
className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5"
style={{ background: '#2B3139', color: '#848E9C' }}
className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-nofx-bg-deeper"
style={{ background: '#E8E2D5', color: '#8A8478' }}
>
{editingModelId
? t('cancel', language)
@@ -1474,7 +1480,7 @@ function StandardProviderConfigForm({
type="submit"
disabled={!selectedModel || !apiKey.trim()}
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed"
style={{ background: '#8B5CF6', color: '#fff' }}
style={{ background: '#E0483B', color: '#fff' }}
>
{t('saveConfig', language)}
<svg

View File

@@ -15,15 +15,15 @@ export function ModelStepIndicator({ currentStep, labels }: ModelStepIndicatorPr
<div
className="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold transition-all"
style={{
background: index < currentStep ? '#0ECB81' : index === currentStep ? '#8B5CF6' : '#2B3139',
color: index <= currentStep ? '#000' : '#848E9C',
background: index < currentStep ? '#2E8B57' : index === currentStep ? '#E0483B' : '#E8E2D5',
color: index <= currentStep ? '#F7F4EC' : '#8A8478',
}}
>
{index < currentStep ? <Check className="w-4 h-4" /> : index + 1}
</div>
<span
className="text-xs font-medium hidden sm:block"
style={{ color: index === currentStep ? '#EAECEF' : '#848E9C' }}
style={{ color: index === currentStep ? '#1A1813' : '#8A8478' }}
>
{label}
</span>
@@ -31,7 +31,7 @@ export function ModelStepIndicator({ currentStep, labels }: ModelStepIndicatorPr
{index < labels.length - 1 && (
<div
className="w-8 h-0.5 mx-1"
style={{ background: index < currentStep ? '#0ECB81' : '#2B3139' }}
style={{ background: index < currentStep ? '#2E8B57' : '#E8E2D5' }}
/>
)}
</React.Fragment>

View File

@@ -72,14 +72,13 @@ function StatCard({
<div
className="rounded-lg p-4 transition-all duration-200 hover:scale-[1.02]"
style={{
background: 'linear-gradient(135deg, #1E2329 0%, #181C21 100%)',
border: '1px solid #2B3139',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.2)',
background: '#F7F4EC',
border: '1px solid rgba(26,24,19,0.14)',
}}
>
<div className="flex items-center gap-2 mb-2">
<span className="text-lg">{icon}</span>
<span className="text-xs" style={{ color: '#848E9C' }}>
<span className="text-xs" style={{ color: '#8A8478' }}>
{title}
</span>
{metricKey && (
@@ -89,18 +88,18 @@ function StatCard({
<div className="flex items-baseline gap-1">
<span
className="text-xl font-bold font-mono"
style={{ color: color || '#EAECEF' }}
style={{ color: color || '#1A1813' }}
>
{value}
</span>
{suffix && (
<span className="text-sm" style={{ color: '#848E9C' }}>
<span className="text-sm" style={{ color: '#8A8478' }}>
{suffix}
</span>
)}
</div>
{subtitle && (
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
<div className="text-xs mt-1" style={{ color: '#8A8478' }}>
{subtitle}
</div>
)}
@@ -112,26 +111,26 @@ function StatCard({
function SymbolStatsRow({ stat }: { stat: SymbolStats }) {
const totalPnl = stat.total_pnl || 0
const winRate = stat.win_rate || 0
const pnlColor = totalPnl >= 0 ? '#0ECB81' : '#F6465D'
const pnlColor = totalPnl >= 0 ? '#2E8B57' : '#D6433A'
const winRateColor =
winRate >= 60 ? '#0ECB81' : winRate >= 40 ? '#F0B90B' : '#F6465D'
winRate >= 60 ? '#2E8B57' : winRate >= 40 ? '#E0483B' : '#D6433A'
return (
<div
className="flex items-center justify-between p-3 rounded-lg transition-all duration-200 hover:bg-white/5"
style={{ borderBottom: '1px solid #2B3139' }}
className="flex items-center justify-between p-3 rounded-lg transition-all duration-200 hover:bg-nofx-gold/10"
style={{ borderBottom: '1px solid rgba(26,24,19,0.14)' }}
>
<div className="flex items-center gap-3">
<span className="font-mono font-semibold" style={{ color: '#EAECEF' }}>
<span className="font-mono font-semibold" style={{ color: '#1A1813' }}>
{(stat.symbol || '').replace('USDT', '')}
</span>
<span className="text-xs" style={{ color: '#848E9C' }}>
<span className="text-xs" style={{ color: '#8A8478' }}>
{stat.total_trades || 0} trades
</span>
</div>
<div className="flex items-center gap-6">
<div className="text-right">
<div className="text-xs" style={{ color: '#848E9C' }}>
<div className="text-xs" style={{ color: '#8A8478' }}>
Win Rate
</div>
<div className="font-mono font-semibold" style={{ color: winRateColor }}>
@@ -139,7 +138,7 @@ function SymbolStatsRow({ stat }: { stat: SymbolStats }) {
</div>
</div>
<div className="text-right min-w-[80px]">
<div className="text-xs" style={{ color: '#848E9C' }}>
<div className="text-xs" style={{ color: '#8A8478' }}>
P&L
</div>
<div className="font-mono font-semibold" style={{ color: pnlColor }}>
@@ -155,18 +154,18 @@ function SymbolStatsRow({ stat }: { stat: SymbolStats }) {
// Direction Stats Card
function DirectionStatsCard({ stat, language }: { stat: DirectionStats; language: Language }) {
const isLong = (stat.side || '').toLowerCase() === 'long'
const iconColor = isLong ? '#0ECB81' : '#F6465D'
const iconColor = isLong ? '#2E8B57' : '#D6433A'
const totalPnl = stat.total_pnl || 0
const winRate = stat.win_rate || 0
const tradeCount = stat.trade_count || 0
const avgPnl = stat.avg_pnl || 0
const pnlColor = totalPnl >= 0 ? '#0ECB81' : '#F6465D'
const pnlColor = totalPnl >= 0 ? '#2E8B57' : '#D6433A'
return (
<div
className="rounded-lg p-4"
style={{
background: 'linear-gradient(135deg, #1E2329 0%, #181C21 100%)',
background: '#F7F4EC',
border: `1px solid ${iconColor}33`,
}}
>
@@ -181,15 +180,15 @@ function DirectionStatsCard({ stat, language }: { stat: DirectionStats; language
</div>
<div className="grid grid-cols-4 gap-4">
<div>
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>
<div className="text-xs mb-1" style={{ color: '#8A8478' }}>
{t('positionHistory.trades', language)}
</div>
<div className="font-mono font-semibold" style={{ color: '#EAECEF' }}>
<div className="font-mono font-semibold" style={{ color: '#1A1813' }}>
{tradeCount}
</div>
</div>
<div>
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>
<div className="text-xs mb-1" style={{ color: '#8A8478' }}>
{t('positionHistory.winRate', language)}
</div>
<div
@@ -197,17 +196,17 @@ function DirectionStatsCard({ stat, language }: { stat: DirectionStats; language
style={{
color:
winRate >= 60
? '#0ECB81'
? '#2E8B57'
: winRate >= 40
? '#F0B90B'
: '#F6465D',
? '#E0483B'
: '#D6433A',
}}
>
{winRate.toFixed(1)}%
</div>
</div>
<div>
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>
<div className="text-xs mb-1" style={{ color: '#8A8478' }}>
{t('positionHistory.totalPnL', language)}
</div>
<div className="font-mono font-semibold" style={{ color: pnlColor }}>
@@ -216,10 +215,10 @@ function DirectionStatsCard({ stat, language }: { stat: DirectionStats; language
</div>
</div>
<div>
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>
<div className="text-xs mb-1" style={{ color: '#8A8478' }}>
{t('positionHistory.avgPnL', language)}
</div>
<div className="font-mono font-semibold" style={{ color: avgPnl >= 0 ? '#0ECB81' : '#F6465D' }}>
<div className="font-mono font-semibold" style={{ color: avgPnl >= 0 ? '#2E8B57' : '#D6433A' }}>
{avgPnl >= 0 ? '+' : ''}
{formatNumber(avgPnl)}
</div>
@@ -235,8 +234,8 @@ function PositionRow({ position }: { position: HistoricalPosition }) {
const isLong = side.toUpperCase() === 'LONG'
const realizedPnl = position.realized_pnl || 0
const isProfitable = realizedPnl >= 0
const sideColor = isLong ? '#0ECB81' : '#F6465D'
const pnlColor = isProfitable ? '#0ECB81' : '#F6465D'
const sideColor = isLong ? '#2E8B57' : '#D6433A'
const pnlColor = isProfitable ? '#2E8B57' : '#D6433A'
// Calculate holding time
const entryTime = position.entry_time ? new Date(position.entry_time).getTime() : 0
@@ -260,13 +259,13 @@ function PositionRow({ position }: { position: HistoricalPosition }) {
return (
<tr
className="transition-all duration-200 hover:bg-white/5"
style={{ borderBottom: '1px solid #2B3139' }}
className="transition-all duration-200 hover:bg-nofx-gold/10"
style={{ borderBottom: '1px solid rgba(26,24,19,0.14)' }}
>
{/* Symbol */}
<td className="py-3 px-4">
<div className="flex items-center gap-2">
<span className="font-mono font-semibold" style={{ color: '#EAECEF' }}>
<span className="font-mono font-semibold" style={{ color: '#1A1813' }}>
{(position.symbol || '').replace('USDT', '')}
</span>
<span
@@ -283,22 +282,22 @@ function PositionRow({ position }: { position: HistoricalPosition }) {
</td>
{/* Entry Price */}
<td className="py-3 px-4 text-right font-mono" style={{ color: '#EAECEF' }}>
<td className="py-3 px-4 text-right font-mono" style={{ color: '#1A1813' }}>
{formatPrice(entryPrice)}
</td>
{/* Exit Price */}
<td className="py-3 px-4 text-right font-mono" style={{ color: '#EAECEF' }}>
<td className="py-3 px-4 text-right font-mono" style={{ color: '#1A1813' }}>
{formatPrice(exitPrice)}
</td>
{/* Quantity */}
<td className="py-3 px-4 text-right font-mono" style={{ color: '#848E9C' }}>
<td className="py-3 px-4 text-right font-mono" style={{ color: '#8A8478' }}>
{formatQuantity(displayQty)}
</td>
{/* Position Value (Entry Price * Quantity) */}
<td className="py-3 px-4 text-right font-mono" style={{ color: '#EAECEF' }}>
<td className="py-3 px-4 text-right font-mono" style={{ color: '#1A1813' }}>
{formatNumber(entryPrice * displayQty)}
</td>
@@ -315,19 +314,19 @@ function PositionRow({ position }: { position: HistoricalPosition }) {
</td>
{/* Fee - show more precision for small fees */}
<td className="py-3 px-4 text-right font-mono text-xs" style={{ color: '#848E9C' }}>
<td className="py-3 px-4 text-right font-mono text-xs" style={{ color: '#8A8478' }}>
-{((position.fee || 0) < 0.01 && (position.fee || 0) > 0)
? (position.fee || 0).toFixed(4)
: (position.fee || 0).toFixed(2)}
</td>
{/* Duration */}
<td className="py-3 px-4 text-center text-sm" style={{ color: '#848E9C' }}>
<td className="py-3 px-4 text-center text-sm" style={{ color: '#8A8478' }}>
{formatDuration(holdingMinutes)}
</td>
{/* Exit Time */}
<td className="py-3 px-4 text-right text-xs" style={{ color: '#848E9C' }}>
<td className="py-3 px-4 text-right text-xs" style={{ color: '#8A8478' }}>
{formatDate(position.exit_time)}
</td>
</tr>
@@ -457,7 +456,7 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
return (
<div
className="flex items-center justify-center p-12"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
<div className="animate-spin mr-3">
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24">
@@ -486,9 +485,9 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
<div
className="rounded-lg p-6 text-center"
style={{
background: 'rgba(246, 70, 93, 0.1)',
border: '1px solid rgba(246, 70, 93, 0.3)',
color: '#F6465D',
background: 'rgba(214, 67, 58, 0.1)',
border: '1px solid rgba(214, 67, 58, 0.3)',
color: '#D6433A',
}}
>
{error}
@@ -501,15 +500,15 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
<div
className="rounded-lg p-12 text-center"
style={{
background: 'linear-gradient(135deg, #1E2329 0%, #181C21 100%)',
border: '1px solid #2B3139',
background: '#F7F4EC',
border: '1px solid rgba(26,24,19,0.14)',
}}
>
<div className="text-4xl mb-4">📊</div>
<div className="text-lg font-semibold mb-2" style={{ color: '#EAECEF' }}>
<div className="text-lg font-semibold mb-2" style={{ color: '#1A1813' }}>
{t('positionHistory.noHistory', language)}
</div>
<div style={{ color: '#848E9C' }}>
<div style={{ color: '#8A8478' }}>
{t('positionHistory.noHistoryDesc', language)}
</div>
</div>
@@ -535,10 +534,10 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
suffix="%"
color={
(stats.win_rate || 0) >= 60
? '#0ECB81'
? '#2E8B57'
: (stats.win_rate || 0) >= 40
? '#F0B90B'
: '#F6465D'
? '#E0483B'
: '#D6433A'
}
metricKey="win_rate"
language={language}
@@ -547,7 +546,7 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
icon="💰"
title={t('positionHistory.totalPnL', language)}
value={((stats.total_pnl || 0) >= 0 ? '+' : '') + formatNumber(stats.total_pnl || 0)}
color={(stats.total_pnl || 0) >= 0 ? '#0ECB81' : '#F6465D'}
color={(stats.total_pnl || 0) >= 0 ? '#2E8B57' : '#D6433A'}
subtitle={`${t('positionHistory.fee', language)}: -${formatNumber(stats.total_fee || 0)}`}
metricKey="total_return"
language={language}
@@ -556,7 +555,7 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
icon="📈"
title={t('positionHistory.profitFactor', language)}
value={(stats.profit_factor || 0).toFixed(2)}
color={(stats.profit_factor || 0) >= 1.5 ? '#0ECB81' : (stats.profit_factor || 0) >= 1 ? '#F0B90B' : '#F6465D'}
color={(stats.profit_factor || 0) >= 1.5 ? '#2E8B57' : (stats.profit_factor || 0) >= 1 ? '#E0483B' : '#D6433A'}
subtitle={t('positionHistory.profitFactorDesc', language)}
metricKey="profit_factor"
language={language}
@@ -565,7 +564,7 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
icon="⚖️"
title={t('positionHistory.plRatio', language)}
value={profitLossRatio === Infinity ? '∞' : profitLossRatio.toFixed(2)}
color={profitLossRatio >= 1.5 ? '#0ECB81' : profitLossRatio >= 1 ? '#F0B90B' : '#F6465D'}
color={profitLossRatio >= 1.5 ? '#2E8B57' : profitLossRatio >= 1 ? '#E0483B' : '#D6433A'}
subtitle={t('positionHistory.plRatioDesc', language)}
metricKey="expectancy"
language={language}
@@ -580,7 +579,7 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
icon="📉"
title={t('positionHistory.sharpeRatio', language)}
value={(stats.sharpe_ratio || 0).toFixed(2)}
color={(stats.sharpe_ratio || 0) >= 1 ? '#0ECB81' : (stats.sharpe_ratio || 0) >= 0 ? '#F0B90B' : '#F6465D'}
color={(stats.sharpe_ratio || 0) >= 1 ? '#2E8B57' : (stats.sharpe_ratio || 0) >= 0 ? '#E0483B' : '#D6433A'}
subtitle={t('positionHistory.sharpeRatioDesc', language)}
metricKey="sharpe_ratio"
language={language}
@@ -590,7 +589,7 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
title={t('positionHistory.maxDrawdown', language)}
value={(stats.max_drawdown_pct || 0).toFixed(1)}
suffix="%"
color={(stats.max_drawdown_pct || 0) <= 10 ? '#0ECB81' : (stats.max_drawdown_pct || 0) <= 20 ? '#F0B90B' : '#F6465D'}
color={(stats.max_drawdown_pct || 0) <= 10 ? '#2E8B57' : (stats.max_drawdown_pct || 0) <= 20 ? '#E0483B' : '#D6433A'}
metricKey="max_drawdown"
language={language}
/>
@@ -598,7 +597,7 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
icon="🏆"
title={t('positionHistory.avgWin', language)}
value={'+' + formatNumber(stats.avg_win || 0)}
color="#0ECB81"
color="#2E8B57"
metricKey="avg_trade_pnl"
language={language}
/>
@@ -606,14 +605,14 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
icon="💸"
title={t('positionHistory.avgLoss', language)}
value={'-' + formatNumber(stats.avg_loss || 0)}
color="#F6465D"
color="#D6433A"
language={language}
/>
<StatCard
icon="💵"
title={t('positionHistory.netPnL', language)}
value={((stats.total_pnl || 0) - (stats.total_fee || 0) >= 0 ? '+' : '') + formatNumber((stats.total_pnl || 0) - (stats.total_fee || 0))}
color={(stats.total_pnl || 0) - (stats.total_fee || 0) >= 0 ? '#0ECB81' : '#F6465D'}
color={(stats.total_pnl || 0) - (stats.total_fee || 0) >= 0 ? '#2E8B57' : '#D6433A'}
subtitle={t('positionHistory.netPnLDesc', language)}
language={language}
/>
@@ -634,13 +633,13 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
<div
className="rounded-lg p-4"
style={{
background: 'linear-gradient(135deg, #1E2329 0%, #181C21 100%)',
border: '1px solid #2B3139',
background: '#F7F4EC',
border: '1px solid rgba(26,24,19,0.14)',
}}
>
<div className="flex items-center gap-2 mb-4">
<span className="text-lg">🏅</span>
<span className="font-semibold" style={{ color: '#EAECEF' }}>
<span className="font-semibold" style={{ color: '#1A1813' }}>
{t('positionHistory.symbolPerformance', language)}
</span>
</div>
@@ -656,17 +655,17 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
<div
className="rounded-lg overflow-hidden"
style={{
background: 'linear-gradient(135deg, #1E2329 0%, #181C21 100%)',
border: '1px solid #2B3139',
background: '#F7F4EC',
border: '1px solid rgba(26,24,19,0.14)',
}}
>
{/* Filters */}
<div
className="flex flex-wrap items-center gap-4 p-4"
style={{ borderBottom: '1px solid #2B3139' }}
style={{ borderBottom: '1px solid rgba(26,24,19,0.14)' }}
>
<div className="flex items-center gap-2">
<span className="text-sm" style={{ color: '#848E9C' }}>
<span className="text-sm" style={{ color: '#8A8478' }}>
{t('positionHistory.symbol', language)}:
</span>
<NofxSelect
@@ -678,26 +677,26 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
]}
className="rounded px-3 py-1.5 text-sm"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
background: '#E8E2D5',
border: '1px solid rgba(26,24,19,0.14)',
color: '#1A1813',
}}
/>
</div>
<div className="flex items-center gap-2">
<span className="text-sm" style={{ color: '#848E9C' }}>
<span className="text-sm" style={{ color: '#8A8478' }}>
{t('positionHistory.side', language)}:
</span>
<div className="flex rounded overflow-hidden" style={{ border: '1px solid #2B3139' }}>
<div className="flex rounded overflow-hidden" style={{ border: '1px solid rgba(26,24,19,0.14)' }}>
{['all', 'LONG', 'SHORT'].map((side) => (
<button
key={side}
onClick={() => setFilterSide(side)}
className="px-3 py-1.5 text-sm capitalize transition-colors"
style={{
background: filterSide === side ? '#2B3139' : 'transparent',
color: filterSide === side ? '#EAECEF' : '#848E9C',
background: filterSide === side ? '#E8E2D5' : 'transparent',
color: filterSide === side ? '#1A1813' : '#8A8478',
}}
>
{side === 'all' ? t('positionHistory.all', language) : side}
@@ -707,7 +706,7 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
</div>
<div className="flex items-center gap-2 ml-auto">
<span className="text-sm" style={{ color: '#848E9C' }}>
<span className="text-sm" style={{ color: '#8A8478' }}>
{t('positionHistory.sort', language)}:
</span>
<NofxSelect
@@ -725,9 +724,9 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
]}
className="rounded px-3 py-1.5 text-sm"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
background: '#E8E2D5',
border: '1px solid rgba(26,24,19,0.14)',
color: '#1A1813',
}}
/>
</div>
@@ -737,58 +736,58 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr style={{ background: '#0B0E11' }}>
<tr style={{ background: '#E8E2D5' }}>
<th
className="py-3 px-4 text-left text-xs font-semibold uppercase tracking-wider"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
{t('positionHistory.symbol', language)}
</th>
<th
className="py-3 px-4 text-right text-xs font-semibold uppercase tracking-wider"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
{t('positionHistory.entry', language)}
</th>
<th
className="py-3 px-4 text-right text-xs font-semibold uppercase tracking-wider"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
{t('positionHistory.exit', language)}
</th>
<th
className="py-3 px-4 text-right text-xs font-semibold uppercase tracking-wider"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
{t('positionHistory.qty', language)}
</th>
<th
className="py-3 px-4 text-right text-xs font-semibold uppercase tracking-wider"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
{t('positionHistory.value', language)}
</th>
<th
className="py-3 px-4 text-right text-xs font-semibold uppercase tracking-wider"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
{t('positionHistory.pnl', language)}
</th>
<th
className="py-3 px-4 text-right text-xs font-semibold uppercase tracking-wider"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
{t('positionHistory.fee', language)}
</th>
<th
className="py-3 px-4 text-center text-xs font-semibold uppercase tracking-wider"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
{t('positionHistory.duration', language)}
</th>
<th
className="py-3 px-4 text-right text-xs font-semibold uppercase tracking-wider"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
{t('positionHistory.closedAt', language)}
</th>
@@ -805,7 +804,7 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
{/* Footer with Pagination */}
<div
className="flex flex-wrap items-center justify-between gap-4 p-4 text-sm"
style={{ borderTop: '1px solid #2B3139', color: '#848E9C' }}
style={{ borderTop: '1px solid rgba(26,24,19,0.14)', color: '#8A8478' }}
>
{/* Left: Count info */}
<div className="flex items-center gap-4">
@@ -819,8 +818,8 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
style={{
color:
filteredAndSortedPositions.reduce((sum, p) => sum + (p.realized_pnl || 0), 0) >= 0
? '#0ECB81'
: '#F6465D',
? '#2E8B57'
: '#D6433A',
}}
>
{filteredAndSortedPositions.reduce((sum, p) => sum + (p.realized_pnl || 0), 0) >= 0
@@ -838,8 +837,8 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
<div className="flex items-center gap-3">
{/* Page size selector */}
<div className="flex items-center gap-2">
<span className="text-xs" style={{ color: '#848E9C' }}>
{language === 'zh' ? '每页' : 'Per page'}:
<span className="text-xs" style={{ color: '#8A8478' }}>
{language === 'zh' ? 'Per page' : 'Per page'}:
</span>
<NofxSelect
value={pageSize}
@@ -851,9 +850,9 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
]}
className="rounded px-2 py-1 text-sm"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
background: '#E8E2D5',
border: '1px solid rgba(26,24,19,0.14)',
color: '#1A1813',
}}
/>
</div>
@@ -866,8 +865,8 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
disabled={currentPage === 1}
className="px-2 py-1 rounded text-xs transition-colors disabled:opacity-30"
style={{
background: currentPage === 1 ? 'transparent' : '#2B3139',
color: '#EAECEF',
background: currentPage === 1 ? 'transparent' : '#E8E2D5',
color: '#1A1813',
}}
>
«
@@ -877,13 +876,13 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
disabled={currentPage === 1}
className="px-2 py-1 rounded text-xs transition-colors disabled:opacity-30"
style={{
background: currentPage === 1 ? 'transparent' : '#2B3139',
color: '#EAECEF',
background: currentPage === 1 ? 'transparent' : '#E8E2D5',
color: '#1A1813',
}}
>
</button>
<span className="px-3 text-xs" style={{ color: '#EAECEF' }}>
<span className="px-3 text-xs" style={{ color: '#1A1813' }}>
{currentPage} / {totalPages}
</span>
<button
@@ -891,8 +890,8 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
disabled={currentPage === totalPages}
className="px-2 py-1 rounded text-xs transition-colors disabled:opacity-30"
style={{
background: currentPage === totalPages ? 'transparent' : '#2B3139',
color: '#EAECEF',
background: currentPage === totalPages ? 'transparent' : '#E8E2D5',
color: '#1A1813',
}}
>
@@ -902,8 +901,8 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
disabled={currentPage === totalPages}
className="px-2 py-1 rounded text-xs transition-colors disabled:opacity-30"
style={{
background: currentPage === totalPages ? 'transparent' : '#2B3139',
color: '#EAECEF',
background: currentPage === totalPages ? 'transparent' : '#E8E2D5',
color: '#1A1813',
}}
>
»

View File

@@ -16,15 +16,15 @@ function StepIndicator({ currentStep, labels }: { currentStep: number; labels: s
<div
className="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold transition-all"
style={{
background: index < currentStep ? '#0ECB81' : index === currentStep ? '#2AABEE' : '#2B3139',
color: index <= currentStep ? '#000' : '#848E9C',
background: index < currentStep ? '#2E8B57' : index === currentStep ? '#E0483B' : '#E8E2D5',
color: index <= currentStep ? '#fff' : '#8A8478',
}}
>
{index < currentStep ? <Check className="w-4 h-4" /> : index + 1}
</div>
<span
className="text-xs font-medium hidden sm:block"
style={{ color: index === currentStep ? '#EAECEF' : '#848E9C' }}
style={{ color: index === currentStep ? '#1A1813' : '#8A8478' }}
>
{label}
</span>
@@ -32,7 +32,7 @@ function StepIndicator({ currentStep, labels }: { currentStep: number; labels: s
{index < labels.length - 1 && (
<div
className="w-8 h-0.5 mx-1"
style={{ background: index < currentStep ? '#0ECB81' : '#2B3139' }}
style={{ background: index < currentStep ? '#2E8B57' : '#E8E2D5' }}
/>
)}
</React.Fragment>
@@ -123,13 +123,13 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
// Model selector shared between steps
const ModelSelector = () => (
<div className="space-y-2">
<label className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
<label className="text-sm font-semibold" style={{ color: '#1A1813' }}>
{t('telegram.selectAiModel', language)}
</label>
{models.length === 0 ? (
<div
className="px-4 py-3 rounded-xl text-xs"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#848E9C' }}
style={{ background: '#F1ECE2', border: '1px solid rgba(26,24,19,0.14)', color: '#8A8478' }}
>
{t('telegram.noEnabledModels', language)}
</div>
@@ -143,13 +143,13 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
]}
className="w-full px-4 py-3 rounded-xl text-sm"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: selectedModelId ? '#EAECEF' : '#848E9C',
background: '#F1ECE2',
border: '1px solid rgba(26,24,19,0.14)',
color: selectedModelId ? '#1A1813' : '#8A8478',
}}
/>
)}
<div className="text-xs" style={{ color: '#848E9C' }}>
<div className="text-xs" style={{ color: '#8A8478' }}>
{t('telegram.autoUseEnabled', language)}
</div>
</div>
@@ -159,7 +159,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4 overflow-y-auto backdrop-blur-sm">
<div
className="rounded-2xl w-full max-w-lg relative my-8 shadow-2xl"
style={{ background: 'linear-gradient(180deg, #1E2329 0%, #181A20 100%)' }}
style={{ background: '#F7F4EC' }}
>
{/* Header */}
<div className="flex items-center justify-between p-6 pb-2">
@@ -168,14 +168,14 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
<button
type="button"
onClick={() => setStep(step - 1)}
className="p-2 rounded-lg hover:bg-white/10 transition-colors"
className="p-2 rounded-lg hover:bg-black/5 transition-colors"
>
<ChevronLeft className="w-5 h-5" style={{ color: '#848E9C' }} />
<ChevronLeft className="w-5 h-5" style={{ color: '#8A8478' }} />
</button>
)}
<div className="flex items-center gap-2">
<MessageCircle className="w-6 h-6" style={{ color: '#2AABEE' }} />
<h3 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
<MessageCircle className="w-6 h-6" style={{ color: '#E0483B' }} />
<h3 className="text-xl font-bold" style={{ color: '#1A1813' }}>
{t('telegram.botSetup', language)}
</h3>
</div>
@@ -183,8 +183,8 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
<button
type="button"
onClick={onClose}
className="p-2 rounded-lg hover:bg-white/10 transition-colors"
style={{ color: '#848E9C' }}
className="p-2 rounded-lg hover:bg-black/5 transition-colors"
style={{ color: '#8A8478' }}
>
</button>
@@ -198,7 +198,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
{/* Content */}
<div className="px-6 pb-6 space-y-5">
{isLoading ? (
<div className="text-center py-8 text-zinc-500 text-sm font-mono">
<div className="text-center py-8 text-nofx-text-muted text-sm font-mono">
{t('telegram.loading', language)}
</div>
) : (
@@ -208,17 +208,17 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
<div className="space-y-5">
<div
className="p-4 rounded-xl space-y-3"
style={{ background: 'rgba(42, 171, 238, 0.1)', border: '1px solid rgba(42, 171, 238, 0.3)' }}
style={{ background: 'rgba(224, 72, 59, 0.1)', border: '1px solid rgba(224, 72, 59, 0.3)' }}
>
<div className="flex items-start gap-3">
<span className="text-2xl">🤖</span>
<div>
<div className="font-semibold mb-1" style={{ color: '#2AABEE' }}>
<div className="font-semibold mb-1" style={{ color: '#E0483B' }}>
{t('telegram.step1Title', language)}
</div>
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
<div>1. {t('telegram.step1Desc1', language)} <code className="text-blue-400">@BotFather</code></div>
<div>2. {t('telegram.step1Desc2', language)} <code className="text-blue-400">/newbot</code> {t('telegram.step1Desc2Suffix', language)}</div>
<div className="text-xs space-y-1" style={{ color: '#8A8478' }}>
<div>1. {t('telegram.step1Desc1', language)} <code className="text-nofx-accent">@BotFather</code></div>
<div>2. {t('telegram.step1Desc2', language)} <code className="text-nofx-accent">/newbot</code> {t('telegram.step1Desc2Suffix', language)}</div>
<div>3. {t('telegram.step1Desc3', language)}</div>
<div>4. {t('telegram.step1Desc4', language)}</div>
</div>
@@ -231,14 +231,14 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
target="_blank"
rel="noopener noreferrer"
className="w-full flex items-center justify-center gap-2 px-4 py-3 rounded-xl font-semibold transition-all hover:scale-[1.02]"
style={{ background: '#2AABEE', color: '#000' }}
style={{ background: '#E0483B', color: '#fff' }}
>
<ExternalLink className="w-4 h-4" />
{t('telegram.openBotFather', language)}
</a>
<div className="space-y-2">
<label className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
<label className="text-sm font-semibold" style={{ color: '#1A1813' }}>
{t('telegram.pasteToken', language)}
</label>
<input
@@ -247,9 +247,9 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
onChange={(e) => setToken(e.target.value)}
placeholder="123456789:ABCdefGHIjklmNOPQRstuvwxYZ"
className="w-full px-4 py-3 rounded-xl font-mono text-sm"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
style={{ background: '#F1ECE2', border: '1px solid rgba(26,24,19,0.14)', color: '#1A1813' }}
/>
<div className="text-xs" style={{ color: '#848E9C' }}>
<div className="text-xs" style={{ color: '#8A8478' }}>
{t('telegram.tokenFormat', language)}
</div>
</div>
@@ -260,7 +260,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
onClick={handleSaveToken}
disabled={isSaving || !token.trim()}
className="w-full flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed"
style={{ background: '#2AABEE', color: '#000' }}
style={{ background: '#E0483B', color: '#fff' }}
>
{isSaving
? t('telegram.savingToken', language)
@@ -275,17 +275,17 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
<div className="space-y-5">
<div
className="p-4 rounded-xl space-y-3"
style={{ background: 'rgba(14, 203, 129, 0.1)', border: '1px solid rgba(14, 203, 129, 0.3)' }}
style={{ background: 'rgba(46, 139, 87, 0.1)', border: '1px solid rgba(46, 139, 87, 0.3)' }}
>
<div className="flex items-start gap-3">
<span className="text-2xl">📱</span>
<div>
<div className="font-semibold mb-1" style={{ color: '#0ECB81' }}>
<div className="font-semibold mb-1" style={{ color: '#2E8B57' }}>
{t('telegram.step2Title', language)}
</div>
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
<div className="text-xs space-y-1" style={{ color: '#8A8478' }}>
<div>1. {t('telegram.step2Desc1', language)}</div>
<div>2. {t('telegram.step2Desc2', language)} <code className="text-green-400">/start</code></div>
<div>2. {t('telegram.step2Desc2', language)} <code className="text-nofx-success">/start</code></div>
<div>3. {t('telegram.step2Desc3', language)}</div>
</div>
</div>
@@ -295,14 +295,14 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
{config?.token_masked && (
<div
className="p-3 rounded-xl flex items-center gap-3"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
style={{ background: '#F1ECE2', border: '1px solid rgba(26,24,19,0.14)' }}
>
<div className="w-2 h-2 rounded-full bg-yellow-500 animate-pulse flex-shrink-0" />
<div className="w-2 h-2 rounded-full bg-nofx-gold animate-pulse flex-shrink-0" />
<div>
<div className="text-xs font-mono" style={{ color: '#848E9C' }}>
<div className="text-xs font-mono" style={{ color: '#8A8478' }}>
{t('telegram.currentToken', language)}
</div>
<div className="text-sm font-mono" style={{ color: '#EAECEF' }}>
<div className="text-sm font-mono" style={{ color: '#1A1813' }}>
{config.token_masked}
</div>
</div>
@@ -311,9 +311,9 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
<div
className="p-3 rounded-xl text-center"
style={{ background: 'rgba(240, 185, 11, 0.08)', border: '1px solid rgba(240, 185, 11, 0.2)' }}
style={{ background: 'rgba(224, 72, 59, 0.08)', border: '1px solid rgba(224, 72, 59, 0.2)' }}
>
<div className="text-xs" style={{ color: '#F0B90B' }}>
<div className="text-xs" style={{ color: '#E0483B' }}>
{t('telegram.waitingForStart', language)}
</div>
</div>
@@ -321,8 +321,8 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
<div className="flex gap-3">
<button
onClick={() => { setStep(0); setToken('') }}
className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5"
style={{ background: '#2B3139', color: '#848E9C' }}
className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-black/5"
style={{ background: '#E8E2D5', color: '#8A8478' }}
>
{t('telegram.reconfigureToken', language)}
</button>
@@ -342,7 +342,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
}
}}
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02]"
style={{ background: '#0ECB81', color: '#000' }}
style={{ background: '#2E8B57', color: '#fff' }}
>
<Check className="w-4 h-4" />
{t('telegram.checkStatus', language)}
@@ -356,13 +356,13 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
<div className="space-y-5">
<div
className="p-5 rounded-xl text-center space-y-3"
style={{ background: 'rgba(14, 203, 129, 0.1)', border: '1px solid rgba(14, 203, 129, 0.3)' }}
style={{ background: 'rgba(46, 139, 87, 0.1)', border: '1px solid rgba(46, 139, 87, 0.3)' }}
>
<div className="text-4xl">🎉</div>
<div className="font-bold text-lg" style={{ color: '#0ECB81' }}>
<div className="font-bold text-lg" style={{ color: '#2E8B57' }}>
{t('telegram.botActive', language)}
</div>
<div className="text-xs" style={{ color: '#848E9C' }}>
<div className="text-xs" style={{ color: '#8A8478' }}>
{t('telegram.botActiveDesc', language)}
</div>
</div>
@@ -370,14 +370,14 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
{config?.token_masked && (
<div
className="p-3 rounded-xl flex items-center gap-3"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
style={{ background: '#F1ECE2', border: '1px solid rgba(26,24,19,0.14)' }}
>
<div className="w-2 h-2 rounded-full bg-green-500 flex-shrink-0" />
<div className="w-2 h-2 rounded-full bg-nofx-success flex-shrink-0" />
<div className="min-w-0">
<div className="text-xs font-mono" style={{ color: '#848E9C' }}>
<div className="text-xs font-mono" style={{ color: '#8A8478' }}>
Bot Token
</div>
<div className="text-sm font-mono truncate" style={{ color: '#EAECEF' }}>
<div className="text-sm font-mono truncate" style={{ color: '#1A1813' }}>
{config.token_masked}
</div>
</div>
@@ -397,9 +397,9 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
{/* What you can do */}
<div
className="p-4 rounded-xl space-y-2"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
style={{ background: '#F1ECE2', border: '1px solid rgba(26,24,19,0.14)' }}
>
<div className="text-xs font-semibold uppercase tracking-wide mb-2" style={{ color: '#848E9C' }}>
<div className="text-xs font-semibold uppercase tracking-wide mb-2" style={{ color: '#8A8478' }}>
{t('telegram.supportedCommands', language)}
</div>
{[
@@ -410,10 +410,10 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
{ cmd: t('telegram.cmdStrategy', language), desc: t('telegram.cmdStrategyDesc', language) },
].map((item, i) => (
<div key={i} className="flex items-start gap-2 text-xs">
<code className="font-mono px-1.5 py-0.5 rounded flex-shrink-0" style={{ background: '#1E2329', color: '#2AABEE' }}>
<code className="font-mono px-1.5 py-0.5 rounded flex-shrink-0" style={{ background: '#E8E2D5', color: '#E0483B' }}>
{item.cmd}
</code>
<span style={{ color: '#848E9C' }}>{item.desc}</span>
<span style={{ color: '#8A8478' }}>{item.desc}</span>
</div>
))}
</div>
@@ -422,8 +422,8 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
<button
onClick={handleUnbind}
disabled={isUnbinding}
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5 disabled:opacity-50"
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D', border: '1px solid rgba(246, 70, 93, 0.2)' }}
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-black/5 disabled:opacity-50"
style={{ background: 'rgba(214, 67, 58, 0.1)', color: '#D6433A', border: '1px solid rgba(214, 67, 58, 0.2)' }}
>
<Unlink className="w-4 h-4" />
{isUnbinding ? t('telegram.unbinding', language) : t('telegram.unbindAccount', language)}
@@ -431,7 +431,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
<button
onClick={onClose}
className="flex-1 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02]"
style={{ background: '#2AABEE', color: '#000' }}
style={{ background: '#E0483B', color: '#fff' }}
>
{t('telegram.done', language)}
</button>
@@ -483,7 +483,7 @@ function BoundModelSelector({
return (
<div className="space-y-2">
<label className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
<label className="text-sm font-semibold" style={{ color: '#1A1813' }}>
{t('telegram.aiModelLabel', language)}
</label>
<div className="flex gap-2">
@@ -496,16 +496,16 @@ function BoundModelSelector({
]}
className="flex-1 px-3 py-2.5 rounded-xl text-sm"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: modelId ? '#EAECEF' : '#848E9C',
background: '#F1ECE2',
border: '1px solid rgba(26,24,19,0.14)',
color: modelId ? '#1A1813' : '#8A8478',
}}
/>
<button
onClick={handleSave}
disabled={isSaving || modelId === currentModelId}
className="px-4 py-2.5 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-40 disabled:cursor-not-allowed"
style={{ background: '#F0B90B', color: '#000', whiteSpace: 'nowrap' }}
style={{ background: '#E0483B', color: '#fff', whiteSpace: 'nowrap' }}
>
{isSaving ? '...' : t('telegram.save', language)}
</button>

View File

@@ -21,9 +21,9 @@ export function Tooltip({ content, children }: TooltipProps) {
<div
className="absolute z-10 px-3 py-2 text-sm rounded-lg shadow-lg w-64 left-1/2 transform -translate-x-1/2 bottom-full mb-2"
style={{
background: '#2B3139',
color: '#EAECEF',
border: '1px solid #474D57',
background: '#F7F4EC',
color: '#1A1813',
border: '1px solid rgba(26,24,19,0.14)',
}}
>
{content}
@@ -34,7 +34,7 @@ export function Tooltip({ content, children }: TooltipProps) {
height: 0,
borderLeft: '6px solid transparent',
borderRight: '6px solid transparent',
borderTop: '6px solid #2B3139',
borderTop: '6px solid #F7F4EC',
}}
/>
</div>

View File

@@ -19,7 +19,7 @@ import {
import { httpClient } from '../../lib/httpClient'
import { NofxSelect } from '../ui/select'
// 提取下划线后面的名称部分
// Extract the name part after the underscore
function getShortName(fullName: string): string {
const parts = fullName.split('_')
return parts.length > 1 ? parts[parts.length - 1] : fullName
@@ -37,7 +37,7 @@ function getStrategyAIConfig(strategy: Strategy) {
)
}
// 交易所注册链接配置
// Exchange registration link configuration
const EXCHANGE_REGISTRATION_LINKS: Record<
string,
{ url: string; hasReferral?: boolean }
@@ -61,7 +61,7 @@ const EXCHANGE_REGISTRATION_LINKS: Record<
hasReferral: true,
},
}
// 表单内部状态类型
// Internal form state type
interface FormState {
trader_id?: string
trader_name: string
@@ -105,7 +105,7 @@ export function TraderConfigModal({
const [isSaving, setIsSaving] = useState(false)
const [strategies, setStrategies] = useState<Strategy[]>([])
// 获取用户的策略列表
// Fetch the user's strategy list
useEffect(() => {
const fetchStrategies = async () => {
try {
@@ -115,7 +115,7 @@ export function TraderConfigModal({
if (result.success && result.data?.strategies) {
const strategyList = result.data.strategies
setStrategies(strategyList)
// 如果没有选择策略,默认选中激活的策略
// If no strategy is selected, default to the active strategy
if (!formData.strategy_id && !isEditMode) {
const activeStrategy = strategyList.find((s) => s.is_active)
if (activeStrategy) {
@@ -197,14 +197,14 @@ export function TraderConfigModal({
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 backdrop-blur-sm p-4 overflow-y-auto">
<div
className="bg-[#1E2329] border border-[#2B3139] rounded-xl shadow-2xl max-w-2xl w-full my-8"
className="bg-nofx-bg-lighter border border-nofx-gold/20 rounded-xl shadow-2xl max-w-2xl w-full my-8"
style={{ maxHeight: 'calc(100vh - 4rem)' }}
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-[#2B3139] bg-gradient-to-r from-[#1E2329] to-[#252B35] sticky top-0 z-10 rounded-t-xl">
<div className="flex items-center justify-between p-6 border-b border-nofx-gold/20 bg-nofx-bg-lighter sticky top-0 z-10 rounded-t-xl">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-[#F0B90B] to-[#E1A706] flex items-center justify-center text-black">
<div className="w-10 h-10 rounded-lg bg-nofx-gold flex items-center justify-center text-white">
{isEditMode ? (
<Pencil className="w-5 h-5" />
) : (
@@ -212,12 +212,12 @@ export function TraderConfigModal({
)}
</div>
<div>
<h2 className="text-xl font-bold text-[#EAECEF]">
<h2 className="text-xl font-bold text-nofx-text">
{isEditMode
? t('editTrader', language)
: t('createTrader', language)}
</h2>
<p className="text-sm text-[#848E9C] mt-1">
<p className="text-sm text-nofx-text-muted mt-1">
{isEditMode
? t('editTraderConfig', language)
: t('selectStrategyAndConfigParams', language)}
@@ -226,7 +226,7 @@ export function TraderConfigModal({
</div>
<button
onClick={onClose}
className="w-8 h-8 rounded-lg text-[#848E9C] hover:text-[#EAECEF] hover:bg-[#2B3139] transition-colors flex items-center justify-center"
className="w-8 h-8 rounded-lg text-nofx-text-muted hover:text-nofx-text hover:bg-nofx-bg-deeper transition-colors flex items-center justify-center"
>
<IconX className="w-4 h-4" />
</button>
@@ -238,14 +238,14 @@ export function TraderConfigModal({
style={{ maxHeight: 'calc(100vh - 16rem)' }}
>
{/* Basic Info */}
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
<h3 className="text-lg font-semibold text-[#EAECEF] mb-5 flex items-center gap-2">
<span className="text-[#F0B90B]">1</span>{' '}
<div className="bg-nofx-bg border border-nofx-gold/20 rounded-lg p-5">
<h3 className="text-lg font-semibold text-nofx-text mb-5 flex items-center gap-2">
<span className="text-nofx-gold">1</span>{' '}
{t('basicConfig', language)}
</h3>
<div className="space-y-4">
<div>
<label className="text-sm text-[#EAECEF] block mb-2">
<label className="text-sm text-nofx-text block mb-2">
{t('traderNameRequired', language)}
</label>
<input
@@ -254,19 +254,19 @@ export function TraderConfigModal({
onChange={(e) =>
handleInputChange('trader_name', e.target.value)
}
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
className="w-full px-3 py-2 bg-nofx-bg-lighter border border-nofx-gold/20 rounded text-nofx-text focus:border-nofx-gold focus:outline-none"
placeholder={t('enterTraderNamePlaceholder', language)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm text-[#EAECEF] block mb-2">
<label className="text-sm text-nofx-text block mb-2">
{t('aiModelRequired', language)}
</label>
<NofxSelect
value={formData.ai_model}
onChange={(val) => handleInputChange('ai_model', val)}
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF]"
className="w-full px-3 py-2 bg-nofx-bg-lighter border border-nofx-gold/20 rounded text-nofx-text"
options={availableModels.map((model) => ({
value: model.id,
label: getShortName(model.name || model.id).toUpperCase(),
@@ -274,13 +274,13 @@ export function TraderConfigModal({
/>
</div>
<div>
<label className="text-sm text-[#EAECEF] block mb-2">
<label className="text-sm text-nofx-text block mb-2">
{t('exchangeRequired', language)}
</label>
<NofxSelect
value={formData.exchange_id}
onChange={handleExchangeChange}
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF]"
className="w-full px-3 py-2 bg-nofx-bg-lighter border border-nofx-gold/20 rounded text-nofx-text"
options={availableExchanges.map((exchange) => ({
value: exchange.id,
label:
@@ -308,12 +308,12 @@ export function TraderConfigModal({
href={regLink.url}
target="_blank"
rel="noopener noreferrer"
className="mt-2 inline-flex items-center gap-1.5 text-xs text-[#848E9C] hover:text-[#F0B90B] transition-colors"
className="mt-2 inline-flex items-center gap-1.5 text-xs text-nofx-text-muted hover:text-nofx-gold transition-colors"
>
<UserPlus className="w-3.5 h-3.5" />
<span>{t('noExchangeAccount', language)}</span>
{regLink.hasReferral && (
<span className="px-1.5 py-0.5 bg-[#F0B90B]/10 text-[#F0B90B] rounded text-[10px]">
<span className="px-1.5 py-0.5 bg-nofx-gold/10 text-nofx-gold rounded text-[10px]">
{t('discount', language)}
</span>
)}
@@ -327,21 +327,21 @@ export function TraderConfigModal({
</div>
{/* Strategy Selection */}
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
<h3 className="text-lg font-semibold text-[#EAECEF] mb-5 flex items-center gap-2">
<span className="text-[#F0B90B]">2</span>{' '}
<div className="bg-nofx-bg border border-nofx-gold/20 rounded-lg p-5">
<h3 className="text-lg font-semibold text-nofx-text mb-5 flex items-center gap-2">
<span className="text-nofx-gold">2</span>{' '}
{t('selectTradingStrategy', language)}
<Sparkles className="w-4 h-4 text-[#F0B90B]" />
<Sparkles className="w-4 h-4 text-nofx-gold" />
</h3>
<div className="space-y-4">
<div>
<label className="text-sm text-[#EAECEF] block mb-2">
<label className="text-sm text-nofx-text block mb-2">
{t('useStrategy', language)}
</label>
<NofxSelect
value={formData.strategy_id}
onChange={(val) => handleInputChange('strategy_id', val)}
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF]"
className="w-full px-3 py-2 bg-nofx-bg-lighter border border-nofx-gold/20 rounded text-nofx-text"
options={[
{ value: '', label: t('noStrategyManual', language) },
...strategies.map((strategy) => ({
@@ -358,7 +358,7 @@ export function TraderConfigModal({
]}
/>
{strategies.length === 0 && (
<p className="text-xs text-[#848E9C] mt-2">
<p className="text-xs text-nofx-text-muted mt-2">
{t('noStrategyHint', language)}
</p>
)}
@@ -366,30 +366,30 @@ export function TraderConfigModal({
{/* Strategy Preview */}
{selectedStrategy && (
<div className="mt-3 p-4 bg-[#1E2329] border border-[#2B3139] rounded-lg">
<div className="mt-3 p-4 bg-nofx-bg-lighter border border-nofx-gold/20 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<span className="text-[#F0B90B] text-sm font-medium">
<span className="text-nofx-gold text-sm font-medium">
{t('strategyDetails', language)}
</span>
{selectedStrategy.is_active && (
<span className="px-2 py-0.5 bg-green-500/20 text-green-400 text-xs rounded">
<span className="px-2 py-0.5 bg-nofx-success/20 text-nofx-success text-xs rounded">
{t('activating', language)}
</span>
)}
</div>
<p className="text-sm text-[#848E9C] mb-2">
<p className="text-sm text-nofx-text-muted mb-2">
{selectedStrategy.description ||
(language === 'zh' ? '无描述' : 'No description')}
(language === 'zh' ? 'No description' : 'No description')}
</p>
{selectedStrategy.config.strategy_type === 'grid_trading' &&
selectedStrategy.config.grid_config ? (
<div className="grid grid-cols-2 gap-2 text-xs text-[#848E9C]">
<div className="grid grid-cols-2 gap-2 text-xs text-nofx-text-muted">
<div>
{language === 'zh' ? '交易对' : 'Symbol'}:{' '}
{language === 'zh' ? 'Symbol' : 'Symbol'}:{' '}
{selectedStrategy.config.grid_config.symbol || '-'}
</div>
<div>
{language === 'zh' ? '网格数' : 'Grids'}:{' '}
{language === 'zh' ? 'Grids' : 'Grids'}:{' '}
{selectedStrategy.config.grid_config.grid_count}
</div>
</div>
@@ -398,32 +398,32 @@ export function TraderConfigModal({
const aiConfig = getStrategyAIConfig(selectedStrategy)
if (!aiConfig) return null
return (
<div className="grid grid-cols-2 gap-2 text-xs text-[#848E9C]">
<div className="grid grid-cols-2 gap-2 text-xs text-nofx-text-muted">
<div>
{t('coinSource', language)}:{' '}
{aiConfig.coin_source.source_type === 'static'
? language === 'zh'
? '固定美股'
? 'Fixed US stocks'
: 'Fixed US stocks'
: aiConfig.coin_source.source_type ===
'vergex_signal'
? language === 'zh'
? 'Vergex 信号榜'
? 'Vergex signal board'
: 'Vergex signal board'
: aiConfig.coin_source.source_type ===
'hyper_rank'
? language === 'zh'
? 'Claw402 榜单'
? 'Claw402 board'
: 'Claw402 board'
: aiConfig.coin_source.source_type ===
'hyper_all'
? language === 'zh'
? 'Hyperliquid 全市场'
? 'Hyperliquid all markets'
: 'Hyperliquid all markets'
: aiConfig.coin_source.source_type ===
'hyper_main'
? language === 'zh'
? 'Hyperliquid 主流市场'
? 'Hyperliquid main markets'
: 'Hyperliquid main markets'
: aiConfig.coin_source.source_type ===
'ai500'
@@ -454,15 +454,15 @@ export function TraderConfigModal({
</div>
{/* Trading Parameters */}
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
<h3 className="text-lg font-semibold text-[#EAECEF] mb-5 flex items-center gap-2">
<span className="text-[#F0B90B]">3</span>{' '}
<div className="bg-nofx-bg border border-nofx-gold/20 rounded-lg p-5">
<h3 className="text-lg font-semibold text-nofx-text mb-5 flex items-center gap-2">
<span className="text-nofx-gold">3</span>{' '}
{t('tradingParams', language)}
</h3>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm text-[#EAECEF] block mb-2">
<label className="text-sm text-nofx-text block mb-2">
{t('marginMode', language)}
</label>
<div className="flex gap-2">
@@ -471,8 +471,8 @@ export function TraderConfigModal({
onClick={() => handleInputChange('is_cross_margin', true)}
className={`flex-1 px-3 py-2 rounded text-sm ${
formData.is_cross_margin
? 'bg-[#F0B90B] text-black'
: 'bg-[#0B0E11] text-[#848E9C] border border-[#2B3139]'
? 'bg-nofx-gold text-white'
: 'bg-nofx-bg-lighter text-nofx-text-muted border border-nofx-gold/20'
}`}
>
{t('crossMargin', language)}
@@ -484,8 +484,8 @@ export function TraderConfigModal({
}
className={`flex-1 px-3 py-2 rounded text-sm ${
!formData.is_cross_margin
? 'bg-[#F0B90B] text-black'
: 'bg-[#0B0E11] text-[#848E9C] border border-[#2B3139]'
? 'bg-nofx-gold text-white'
: 'bg-nofx-bg-lighter text-nofx-text-muted border border-nofx-gold/20'
}`}
>
{t('isolatedMargin', language)}
@@ -493,7 +493,7 @@ export function TraderConfigModal({
</div>
</div>
<div>
<label className="text-sm text-[#EAECEF] block mb-2">
<label className="text-sm text-nofx-text block mb-2">
{t('aiScanInterval', language)}
</label>
<input
@@ -506,12 +506,12 @@ export function TraderConfigModal({
: 3
handleInputChange('scan_interval_minutes', safeValue)
}}
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
className="w-full px-3 py-2 bg-nofx-bg-lighter border border-nofx-gold/20 rounded text-nofx-text focus:border-nofx-gold focus:outline-none"
min="3"
max="60"
step="1"
/>
<p className="text-xs text-gray-500 mt-1">
<p className="text-xs text-nofx-text-muted mt-1">
{t('scanIntervalRecommend', language)}
</p>
</div>
@@ -519,7 +519,7 @@ export function TraderConfigModal({
{/* Competition visibility */}
<div>
<label className="text-sm text-[#EAECEF] block mb-2">
<label className="text-sm text-nofx-text block mb-2">
{t('competitionDisplay', language)}
</label>
<div className="flex gap-2">
@@ -530,8 +530,8 @@ export function TraderConfigModal({
}
className={`flex-1 px-3 py-2 rounded text-sm ${
formData.show_in_competition
? 'bg-[#F0B90B] text-black'
: 'bg-[#0B0E11] text-[#848E9C] border border-[#2B3139]'
? 'bg-nofx-gold text-white'
: 'bg-nofx-bg-lighter text-nofx-text-muted border border-nofx-gold/20'
}`}
>
{t('show', language)}
@@ -543,22 +543,22 @@ export function TraderConfigModal({
}
className={`flex-1 px-3 py-2 rounded text-sm ${
!formData.show_in_competition
? 'bg-[#F0B90B] text-black'
: 'bg-[#0B0E11] text-[#848E9C] border border-[#2B3139]'
? 'bg-nofx-gold text-white'
: 'bg-nofx-bg-lighter text-nofx-text-muted border border-nofx-gold/20'
}`}
>
{t('hide', language)}
</button>
</div>
<p className="text-xs text-[#848E9C] mt-1">
<p className="text-xs text-nofx-text-muted mt-1">
{t('hiddenInCompetition', language)}
</p>
</div>
<div className="p-3 bg-[#1E2329] border border-[#2B3139] rounded flex items-center gap-2">
<div className="p-3 bg-nofx-bg-lighter border border-nofx-gold/20 rounded flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-4 h-4 text-[#F0B90B]"
className="w-4 h-4 text-nofx-gold"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@@ -570,7 +570,7 @@ export function TraderConfigModal({
<line x1="12" x2="12" y1="8" y2="12" />
<line x1="12" x2="12.01" y1="16" y2="16" />
</svg>
<span className="text-sm text-[#848E9C]">
<span className="text-sm text-nofx-text-muted">
{t('autoFetchBalanceInfo', language)}
</span>
</div>
@@ -579,10 +579,10 @@ export function TraderConfigModal({
</div>
{/* Footer */}
<div className="flex justify-end gap-3 p-6 border-t border-[#2B3139] bg-gradient-to-r from-[#1E2329] to-[#252B35] sticky bottom-0 z-10 rounded-b-xl">
<div className="flex justify-end gap-3 p-6 border-t border-nofx-gold/20 bg-nofx-bg-lighter sticky bottom-0 z-10 rounded-b-xl">
<button
onClick={onClose}
className="px-6 py-3 bg-[#2B3139] text-[#EAECEF] rounded-lg hover:bg-[#404750] transition-all duration-200 border border-[#404750]"
className="px-6 py-3 bg-nofx-bg-deeper text-nofx-text rounded-lg hover:bg-nofx-bg transition-all duration-200 border border-nofx-gold/20"
>
{t('cancel', language)}
</button>
@@ -595,7 +595,7 @@ export function TraderConfigModal({
!formData.ai_model ||
!formData.exchange_id
}
className="px-8 py-3 bg-gradient-to-r from-[#F0B90B] to-[#E1A706] text-black rounded-lg hover:from-[#E1A706] hover:to-[#D4951E] transition-all duration-200 disabled:bg-[#848E9C] disabled:cursor-not-allowed font-medium shadow-lg"
className="px-8 py-3 bg-nofx-gold text-white rounded-lg hover:bg-nofx-gold/90 transition-all duration-200 disabled:bg-nofx-bg-deeper disabled:text-nofx-text-muted disabled:cursor-not-allowed font-medium shadow-lg"
>
{isSaving
? t('saving', language)

View File

@@ -30,9 +30,9 @@ export function TraderConfigViewModal({
label: string
value: string | number | boolean
}) => (
<div className="flex justify-between items-start py-2 border-b border-[#2B3139] last:border-b-0">
<span className="text-sm text-[#848E9C] font-medium">{label}</span>
<span className="text-sm text-[#EAECEF] font-mono text-right">
<div className="flex justify-between items-start py-2 border-b border-nofx-gold/20 last:border-b-0">
<span className="text-sm text-nofx-text-muted font-medium">{label}</span>
<span className="text-sm text-nofx-text font-mono text-right">
{typeof value === 'boolean'
? value
? t('traderConfigView.yes', language)
@@ -45,11 +45,11 @@ export function TraderConfigViewModal({
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 backdrop-blur-sm">
<div
className="bg-[#1E2329] border border-[#2B3139] rounded-xl shadow-2xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto"
className="bg-nofx-bg-lighter border border-nofx-gold/20 rounded-xl shadow-2xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-[#2B3139] bg-gradient-to-r from-[#1E2329] to-[#252B35]">
<div className="flex items-center justify-between p-6 border-b border-nofx-gold/20 bg-nofx-bg-lighter">
<div className="flex items-center gap-3">
<PunkAvatar
seed={getTraderAvatar(
@@ -60,10 +60,10 @@ export function TraderConfigViewModal({
className="rounded-lg"
/>
<div>
<h2 className="text-xl font-bold text-[#EAECEF]">
<h2 className="text-xl font-bold text-nofx-text">
{t('traderConfigView.traderConfig', language)}
</h2>
<p className="text-sm text-[#848E9C] mt-1">
<p className="text-sm text-nofx-text-muted mt-1">
{t('traderConfigView.configInfo', language, {
name: traderData.trader_name,
})}
@@ -76,8 +76,8 @@ export function TraderConfigViewModal({
className="px-3 py-1 rounded-full text-xs font-bold flex items-center gap-1"
style={
traderData.is_running
? { background: 'rgba(14, 203, 129, 0.1)', color: '#0ECB81' }
: { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }
? { background: 'rgba(46, 139, 87, 0.1)', color: '#2E8B57' }
: { background: 'rgba(214, 67, 58, 0.1)', color: '#D6433A' }
}
>
<span>{traderData.is_running ? '●' : '○'}</span>
@@ -87,7 +87,7 @@ export function TraderConfigViewModal({
</div>
<button
onClick={onClose}
className="w-8 h-8 rounded-lg text-[#848E9C] hover:text-[#EAECEF] hover:bg-[#2B3139] transition-colors flex items-center justify-center"
className="w-8 h-8 rounded-lg text-nofx-text-muted hover:text-nofx-text hover:bg-nofx-bg-deeper transition-colors flex items-center justify-center"
>
</button>
@@ -97,8 +97,8 @@ export function TraderConfigViewModal({
{/* Content */}
<div className="p-6 space-y-6">
{/* Basic Info */}
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
<h3 className="text-lg font-semibold text-[#EAECEF] mb-4 flex items-center gap-2">
<div className="bg-nofx-bg border border-nofx-gold/20 rounded-lg p-5">
<h3 className="text-lg font-semibold text-nofx-text mb-4 flex items-center gap-2">
{'🤖 ' + t('traderConfigView.basicInfo', language)}
</h3>
<div className="space-y-3">
@@ -137,8 +137,8 @@ export function TraderConfigViewModal({
{/* Strategy Info - only show if strategy is bound */}
{traderData.strategy_id && (
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
<h3 className="text-lg font-semibold text-[#EAECEF] mb-4 flex items-center gap-2">
<div className="bg-nofx-bg border border-nofx-gold/20 rounded-lg p-5">
<h3 className="text-lg font-semibold text-nofx-text mb-4 flex items-center gap-2">
{'📋 ' + t('traderConfigView.strategyUsed', language)}
</h3>
<div className="space-y-3">
@@ -152,10 +152,10 @@ export function TraderConfigViewModal({
</div>
{/* Footer */}
<div className="flex justify-end p-6 border-t border-[#2B3139] bg-gradient-to-r from-[#1E2329] to-[#252B35]">
<div className="flex justify-end p-6 border-t border-nofx-gold/20 bg-nofx-bg-lighter">
<button
onClick={onClose}
className="px-6 py-3 bg-[#2B3139] text-[#EAECEF] rounded-lg hover:bg-[#404750] transition-all duration-200 border border-[#404750]"
className="px-6 py-3 bg-nofx-bg-deeper text-nofx-text rounded-lg hover:bg-nofx-bg transition-all duration-200 border border-nofx-gold/20"
>
{t('traderConfigView.close', language)}
</button>

View File

@@ -57,18 +57,18 @@ const pipeline = [
export function TraderLaunchGuestPage() {
return (
<div className="min-h-[calc(100vh-4rem)] overflow-hidden bg-[#06080B] px-4 py-10 md:px-8">
<div className="min-h-[calc(100vh-4rem)] overflow-hidden bg-nofx-bg px-4 py-10 md:px-8">
<div className="mx-auto flex w-full max-w-7xl flex-col gap-8">
<section className="grid gap-8 rounded-2xl border border-white/10 bg-[linear-gradient(135deg,rgba(16,18,25,0.94),rgba(7,10,14,0.92)_52%,rgba(18,15,3,0.78))] p-6 shadow-[0_24px_100px_rgba(0,0,0,0.45)] md:p-8 xl:grid-cols-[1.02fr_0.98fr]">
<section className="grid gap-8 rounded-2xl border border-nofx-gold/20 bg-nofx-bg-lighter p-6 md:p-8 xl:grid-cols-[1.02fr_0.98fr]">
<div className="flex flex-col justify-center">
<div className="mb-5 inline-flex w-fit items-center gap-2 rounded-full border border-nofx-gold/25 bg-nofx-gold/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.2em] text-nofx-gold">
<ShieldCheck className="h-3.5 w-3.5" />
NOFX Autopilot
</div>
<h1 className="max-w-3xl text-4xl font-bold tracking-tight text-white md:text-5xl">
<h1 className="max-w-3xl text-4xl font-bold tracking-tight text-nofx-text md:text-5xl">
One strategy. Four setup steps. Then it trades.
</h1>
<p className="mt-5 max-w-2xl text-base leading-7 text-zinc-400">
<p className="mt-5 max-w-2xl text-base leading-7 text-nofx-text-muted">
NOFX runs a single Claw402-driven strategy: board, per-market
details, liquidation structure, candles, execution. No strategy
picker, no manual symbol picking required.
@@ -82,14 +82,14 @@ export function TraderLaunchGuestPage() {
`${ROUTES.traders}?setup=claw402`
)
}
className="inline-flex items-center justify-center gap-2 rounded-xl bg-nofx-gold px-5 py-3 text-sm font-bold text-black transition hover:bg-yellow-400"
className="inline-flex items-center justify-center gap-2 rounded-xl bg-nofx-gold px-5 py-3 text-sm font-bold text-white transition hover:bg-nofx-gold/90"
>
Start setup
<ArrowRight className="h-4 w-4" />
</Link>
<Link
to={ROUTES.register}
className="inline-flex items-center justify-center rounded-xl border border-white/10 bg-white/[0.04] px-5 py-3 text-sm font-semibold text-zinc-200 transition hover:border-white/20 hover:bg-white/[0.07]"
className="inline-flex items-center justify-center rounded-xl border border-nofx-gold/20 bg-nofx-bg-deeper px-5 py-3 text-sm font-semibold text-nofx-text transition hover:border-nofx-gold/40 hover:bg-nofx-bg-deeper"
>
Create account
</Link>
@@ -100,24 +100,24 @@ export function TraderLaunchGuestPage() {
{setupSteps.map((step, index) => {
const Icon = step.icon
const cardClass =
'group rounded-xl border border-white/10 bg-black/24 p-4 text-left transition hover:border-nofx-gold/35 hover:bg-nofx-gold/[0.04]'
'group rounded-xl border border-nofx-gold/20 bg-nofx-bg-deeper p-4 text-left transition hover:border-nofx-gold/35 hover:bg-nofx-gold/[0.06]'
const content = (
<>
<div className="mb-4 flex items-center justify-between">
<div className="flex h-10 w-10 items-center justify-center rounded-xl border border-nofx-gold/20 bg-nofx-gold/10 text-nofx-gold">
<Icon className="h-4 w-4" />
</div>
<span className="font-mono text-xs text-zinc-600">
<span className="font-mono text-xs text-nofx-text-muted">
0{index + 1}
</span>
</div>
<h2 className="text-base font-semibold text-white">
<h2 className="text-base font-semibold text-nofx-text">
{step.title}
</h2>
<p className="mt-2 text-sm leading-6 text-zinc-500">
<p className="mt-2 text-sm leading-6 text-nofx-text-muted">
{step.detail}
</p>
<div className="mt-4 inline-flex items-center gap-2 text-xs font-bold text-nofx-gold transition group-hover:text-yellow-300">
<div className="mt-4 inline-flex items-center gap-2 text-xs font-bold text-nofx-gold transition group-hover:text-nofx-gold/80">
{step.action}
{step.href ? (
<ExternalLink className="h-3.5 w-3.5" />
@@ -160,12 +160,12 @@ export function TraderLaunchGuestPage() {
</div>
</section>
<section className="grid gap-5 rounded-2xl border border-white/10 bg-[#0A0D12] p-5 md:grid-cols-[0.78fr_1.22fr] md:p-6">
<section className="grid gap-5 rounded-2xl border border-nofx-gold/20 bg-nofx-bg-lighter p-5 md:grid-cols-[0.78fr_1.22fr] md:p-6">
<div>
<div className="text-sm font-semibold uppercase tracking-[0.18em] text-nofx-gold">
No trading wallet yet?
</div>
<p className="mt-3 text-sm leading-6 text-zinc-500">
<p className="mt-3 text-sm leading-6 text-nofx-text-muted">
NOFX does not need your main-wallet private key. Install or unlock
an EVM wallet, fund Hyperliquid with USDC, then authorize the NOFX
Agent after sign-in.
@@ -176,11 +176,11 @@ export function TraderLaunchGuestPage() {
href="https://rabby.io/"
target="_blank"
rel="noreferrer"
className="group rounded-xl border border-white/10 bg-white/[0.03] p-4 transition hover:border-nofx-gold/30 hover:bg-nofx-gold/[0.04]"
className="group rounded-xl border border-nofx-gold/20 bg-nofx-bg-deeper p-4 transition hover:border-nofx-gold/30 hover:bg-nofx-gold/[0.06]"
>
<Download className="mb-3 h-4 w-4 text-nofx-gold" />
<div className="font-semibold text-white">Install Rabby</div>
<p className="mt-2 text-sm leading-6 text-zinc-500">
<div className="font-semibold text-nofx-text">Install Rabby</div>
<p className="mt-2 text-sm leading-6 text-nofx-text-muted">
Create or import an EVM wallet before connecting to Hyperliquid.
</p>
</a>
@@ -188,11 +188,11 @@ export function TraderLaunchGuestPage() {
href="https://metamask.io/download/"
target="_blank"
rel="noreferrer"
className="group rounded-xl border border-white/10 bg-white/[0.03] p-4 transition hover:border-nofx-gold/30 hover:bg-nofx-gold/[0.04]"
className="group rounded-xl border border-nofx-gold/20 bg-nofx-bg-deeper p-4 transition hover:border-nofx-gold/30 hover:bg-nofx-gold/[0.06]"
>
<ExternalLink className="mb-3 h-4 w-4 text-nofx-gold" />
<div className="font-semibold text-white">MetaMask</div>
<p className="mt-2 text-sm leading-6 text-zinc-500">
<div className="font-semibold text-nofx-text">MetaMask</div>
<p className="mt-2 text-sm leading-6 text-nofx-text-muted">
Already use MetaMask? Unlock it, then continue setup inside
NOFX.
</p>
@@ -204,8 +204,8 @@ export function TraderLaunchGuestPage() {
className="group rounded-xl border border-nofx-gold/20 bg-nofx-gold/10 p-4 transition hover:bg-nofx-gold/15"
>
<ExternalLink className="mb-3 h-4 w-4 text-nofx-gold" />
<div className="font-semibold text-white">Open Hyperliquid</div>
<p className="mt-2 text-sm leading-6 text-zinc-400">
<div className="font-semibold text-nofx-text">Open Hyperliquid</div>
<p className="mt-2 text-sm leading-6 text-nofx-text-muted">
Deposit USDC there. Trading funds stay in your Hyperliquid
account.
</p>
@@ -213,12 +213,12 @@ export function TraderLaunchGuestPage() {
</div>
</section>
<section className="grid gap-4 rounded-2xl border border-white/10 bg-[#0A0D12] p-5 md:grid-cols-[0.72fr_1.28fr] md:p-6">
<section className="grid gap-4 rounded-2xl border border-nofx-gold/20 bg-nofx-bg-lighter p-5 md:grid-cols-[0.72fr_1.28fr] md:p-6">
<div>
<div className="text-sm font-semibold uppercase tracking-[0.18em] text-nofx-gold">
What runs after launch
</div>
<p className="mt-3 text-sm leading-6 text-zinc-500">
<p className="mt-3 text-sm leading-6 text-nofx-text-muted">
The same production path runs every cycle. The interface only asks
you to fund, authorize, and start.
</p>
@@ -227,10 +227,10 @@ export function TraderLaunchGuestPage() {
{pipeline.map((item) => (
<div
key={item}
className="flex gap-3 rounded-xl border border-white/8 bg-white/[0.03] p-4"
className="flex gap-3 rounded-xl border border-nofx-gold/20 bg-nofx-bg-deeper p-4"
>
<CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 text-emerald-300" />
<p className="text-sm leading-6 text-zinc-300">{item}</p>
<CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 text-nofx-success" />
<p className="text-sm leading-6 text-nofx-text">{item}</p>
</div>
))}
</div>

View File

@@ -63,11 +63,11 @@ export function TradersList({
<div className="flex items-center justify-between mb-4 md:mb-5">
<h2
className="text-lg md:text-xl font-bold flex items-center gap-2"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
<Users
className="w-5 h-5 md:w-6 md:h-6"
style={{ color: '#F0B90B' }}
style={{ color: '#E0483B' }}
/>
{t('currentTraders', language)}
</h2>
@@ -114,7 +114,7 @@ function TradersLoadingSkeleton() {
<div
key={i}
className="flex flex-col md:flex-row md:items-center justify-between p-3 md:p-4 rounded gap-3 md:gap-4 animate-pulse"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
style={{ background: '#F7F4EC', border: '1px solid rgba(26,24,19,0.14)' }}
>
<div className="flex items-center gap-3 md:gap-4">
<div className="w-10 h-10 md:w-12 md:h-12 rounded-full skeleton"></div>
@@ -146,7 +146,7 @@ function TradersEmptyState({
return (
<div
className="text-center py-12 md:py-16"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
<Bot className="w-16 h-16 md:w-24 md:h-24 mx-auto mb-3 md:mb-4 opacity-50" />
<div className="text-base md:text-lg font-semibold mb-2">
@@ -157,7 +157,7 @@ function TradersEmptyState({
</div>
{(configuredModelsCount === 0 ||
configuredExchangesCount === 0) && (
<div className="text-xs md:text-sm text-yellow-500">
<div className="text-xs md:text-sm text-nofx-gold">
{configuredModelsCount === 0 &&
configuredExchangesCount === 0
? t('configureModelsAndExchangesFirst', language)
@@ -208,7 +208,7 @@ function TraderRow({
return (
<div
className="flex flex-col md:flex-row md:items-center justify-between p-3 md:p-4 rounded transition-all hover:translate-y-[-1px] gap-3 md:gap-4"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
style={{ background: '#F7F4EC', border: '1px solid rgba(26,24,19,0.14)' }}
>
<div className="flex items-center gap-3 md:gap-4">
<div className="flex-shrink-0">
@@ -226,7 +226,7 @@ function TraderRow({
<div className="min-w-0">
<div
className="font-bold text-base md:text-lg truncate"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
{trader.trader_name}
</div>
@@ -234,8 +234,8 @@ function TraderRow({
className="text-xs md:text-sm truncate"
style={{
color: trader.ai_model.includes('deepseek')
? '#60a5fa'
: '#c084fc',
? '#E0483B'
: '#E0483B',
}}
>
{getModelDisplayName(
@@ -252,11 +252,11 @@ function TraderRow({
<div
className="flex items-center gap-1 px-2 py-1 rounded"
style={{
background: 'rgba(240, 185, 11, 0.08)',
border: '1px solid rgba(240, 185, 11, 0.2)',
background: 'rgba(224, 72, 59, 0.08)',
border: '1px solid rgba(224, 72, 59, 0.2)',
}}
>
<span className="text-xs font-mono" style={{ color: '#F0B90B' }}>
<span className="text-xs font-mono" style={{ color: '#E0483B' }}>
{isVisible ? walletAddr : truncateAddress(walletAddr)}
</span>
<button
@@ -265,13 +265,13 @@ function TraderRow({
e.stopPropagation()
onToggleTraderAddress(trader.trader_id)
}}
className="p-0.5 rounded hover:bg-gray-700 transition-colors"
title={isVisible ? (language === 'zh' ? '隐藏' : 'Hide') : (language === 'zh' ? '显示' : 'Show')}
className="p-0.5 rounded hover:bg-nofx-bg-deeper transition-colors"
title={isVisible ? (language === 'zh' ? 'Hide' : 'Hide') : (language === 'zh' ? 'Show' : 'Show')}
>
{isVisible ? (
<EyeOff className="w-3 h-3" style={{ color: '#848E9C' }} />
<EyeOff className="w-3 h-3" style={{ color: '#8A8478' }} />
) : (
<Eye className="w-3 h-3" style={{ color: '#848E9C' }} />
<Eye className="w-3 h-3" style={{ color: '#8A8478' }} />
)}
</button>
<button
@@ -280,13 +280,13 @@ function TraderRow({
e.stopPropagation()
onCopyAddress(trader.trader_id, walletAddr)
}}
className="p-0.5 rounded hover:bg-gray-700 transition-colors"
title={language === 'zh' ? '复制' : 'Copy'}
className="p-0.5 rounded hover:bg-nofx-bg-deeper transition-colors"
title={language === 'zh' ? 'Copy' : 'Copy'}
>
{isCopied ? (
<Check className="w-3 h-3" style={{ color: '#0ECB81' }} />
<Check className="w-3 h-3" style={{ color: '#2E8B57' }} />
) : (
<Copy className="w-3 h-3" style={{ color: '#848E9C' }} />
<Copy className="w-3 h-3" style={{ color: '#8A8478' }} />
)}
</button>
</div>
@@ -295,18 +295,18 @@ function TraderRow({
<div className="text-center">
<div
className={`px-2 md:px-3 py-1 rounded text-xs font-bold ${trader.is_running
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
? 'bg-nofx-success/10 text-nofx-success'
: 'bg-nofx-danger/10 text-nofx-danger'
}`}
style={
trader.is_running
? {
background: 'rgba(14, 203, 129, 0.1)',
color: '#0ECB81',
background: 'rgba(46, 139, 87, 0.1)',
color: '#2E8B57',
}
: {
background: 'rgba(246, 70, 93, 0.1)',
color: '#F6465D',
background: 'rgba(214, 67, 58, 0.1)',
color: '#D6433A',
}
}
>
@@ -329,8 +329,8 @@ function TraderRow({
}}
className="px-2 md:px-3 py-1.5 md:py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105 flex items-center gap-1 whitespace-nowrap"
style={{
background: 'rgba(99, 102, 241, 0.1)',
color: '#6366F1',
background: 'rgba(224, 72, 59, 0.1)',
color: '#E0483B',
}}
>
<BarChart3 className="w-3 h-3 md:w-4 md:h-4" />
@@ -343,9 +343,9 @@ function TraderRow({
className="px-2 md:px-3 py-1.5 md:py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap flex items-center gap-1"
style={{
background: trader.is_running
? 'rgba(132, 142, 156, 0.1)'
: 'rgba(255, 193, 7, 0.1)',
color: trader.is_running ? '#848E9C' : '#FFC107',
? 'rgba(138, 132, 120, 0.1)'
: 'rgba(224, 72, 59, 0.1)',
color: trader.is_running ? '#8A8478' : '#E0483B',
}}
>
<Pencil className="w-3 h-3 md:w-4 md:h-4" />
@@ -363,12 +363,12 @@ function TraderRow({
style={
trader.is_running
? {
background: 'rgba(246, 70, 93, 0.1)',
color: '#F6465D',
background: 'rgba(214, 67, 58, 0.1)',
color: '#D6433A',
}
: {
background: 'rgba(14, 203, 129, 0.1)',
color: '#0ECB81',
background: 'rgba(46, 139, 87, 0.1)',
color: '#2E8B57',
}
}
>
@@ -383,15 +383,15 @@ function TraderRow({
style={
trader.show_in_competition !== false
? {
background: 'rgba(14, 203, 129, 0.1)',
color: '#0ECB81',
background: 'rgba(46, 139, 87, 0.1)',
color: '#2E8B57',
}
: {
background: 'rgba(132, 142, 156, 0.1)',
color: '#848E9C',
background: 'rgba(138, 132, 120, 0.1)',
color: '#8A8478',
}
}
title={trader.show_in_competition !== false ? '在竞技场显示' : '在竞技场隐藏'}
title={trader.show_in_competition !== false ? 'Shown in arena' : 'Hidden from arena'}
>
{trader.show_in_competition !== false ? (
<Eye className="w-3 h-3 md:w-4 md:h-4" />
@@ -404,8 +404,8 @@ function TraderRow({
onClick={() => onDeleteTrader(trader.trader_id)}
className="px-2 md:px-3 py-1.5 md:py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105"
style={{
background: 'rgba(246, 70, 93, 0.1)',
color: '#F6465D',
background: 'rgba(214, 67, 58, 0.1)',
color: '#D6433A',
}}
>
<Trash2 className="w-3 h-3 md:w-4 md:h-4" />

View File

@@ -1,4 +1,4 @@
// 获取友好的AI模型名称
// Get a friendly display name for the AI model
export function getModelDisplayName(modelId: string): string {
switch (modelId.toLowerCase()) {
case 'deepseek':
@@ -12,7 +12,7 @@ export function getModelDisplayName(modelId: string): string {
}
}
// 提取下划线后面的名称部分
// Extract the name part after the underscore
export function getShortName(fullName: string): string {
const parts = fullName.split('_')
return parts.length > 1 ? parts[parts.length - 1] : fullName

View File

@@ -14,7 +14,7 @@ const AlertDialogOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 bg-black/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
'fixed inset-0 z-50 bg-[rgba(26,24,19,0.45)] backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
@@ -104,7 +104,7 @@ const AlertDialogAction = React.forwardRef<
<AlertDialogPrimitive.Action
ref={ref}
className={cn(
'inline-flex items-center justify-center whitespace-nowrap rounded-full text-sm font-semibold transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--binance-yellow)] disabled:pointer-events-none disabled:opacity-50 bg-[var(--binance-yellow)] text-black hover:brightness-95 h-10 px-8 min-w-[140px] shadow-[0_10px_30px_rgba(240,185,11,0.35)]',
'inline-flex items-center justify-center whitespace-nowrap rounded-full text-sm font-semibold transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[#E0483B] disabled:pointer-events-none disabled:opacity-50 bg-[#E0483B] text-white hover:brightness-95 h-10 px-8 min-w-[140px]',
className
)}
{...props}
@@ -119,7 +119,7 @@ const AlertDialogCancel = React.forwardRef<
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
'inline-flex items-center justify-center whitespace-nowrap rounded-full text-sm font-semibold transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--panel-border)] disabled:pointer-events-none disabled:opacity-50 border border-[var(--panel-border)] bg-transparent text-[var(--text-secondary)] hover:bg-white/5 h-10 px-8 min-w-[140px]',
'inline-flex items-center justify-center whitespace-nowrap rounded-full text-sm font-semibold transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--panel-border)] disabled:pointer-events-none disabled:opacity-50 border border-[var(--panel-border)] bg-transparent text-[var(--text-secondary)] hover:bg-[rgba(26,24,19,0.06)] h-10 px-8 min-w-[140px]',
className
)}
{...props}

View File

@@ -12,7 +12,7 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
className={cn(
'flex h-10 w-full rounded px-3 py-2 text-sm',
'bg-[var(--panel-bg)] border border-[var(--panel-border)]',
'text-[var(--text-primary)] focus:outline-none focus:border-[var(--binance-yellow)] focus:ring-1 focus:ring-[rgba(240,185,11,0.15)]',
'text-[var(--text-primary)] focus:outline-none focus:border-[#E0483B] focus:ring-1 focus:ring-[rgba(224,72,59,0.18)]',
className
)}
{...props}

View File

@@ -73,7 +73,7 @@ export function NofxSelect({ value, onChange, options, disabled, className, styl
{open && createPortal(
<div
ref={dropdownRef}
className="fixed z-[9999] rounded border border-[#2B3139] bg-[#0B0E11] shadow-xl shadow-black/50 max-h-60 overflow-y-auto"
className="fixed z-[9999] rounded border border-[rgba(26,24,19,0.14)] bg-nofx-bg-lighter shadow-xl shadow-[rgba(26,24,19,0.18)] max-h-60 overflow-y-auto"
style={{ top: pos.top, left: pos.left, minWidth: pos.width }}
>
{options.map((opt) => (
@@ -82,8 +82,8 @@ export function NofxSelect({ value, onChange, options, disabled, className, styl
className={cn(
'px-3 py-1.5 text-sm cursor-pointer transition-colors whitespace-nowrap',
String(opt.value) === String(value)
? 'bg-[#F0B90B]/10 text-[#F0B90B]'
: 'text-[#EAECEF] hover:bg-[#1E2329]',
? 'bg-[#E0483B]/10 text-[#E0483B]'
: 'text-[#1A1813] hover:bg-[#E8E2D5]',
)}
onClick={(e) => {
e.stopPropagation()