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