release: merge dev into main (2026-04-17) (#1484)

* feat(store): prevent deletion of active strategies and update translations (#1461)

Co-authored-by: Dean <afei.wuhao@gmail.com>

* fix: allow model switching without re-entering wallet key

Users with existing wallets could not switch AI models because the
"Start Trading" button required a valid private key even when one was
already configured. Now the button is enabled when hasExistingWallet
is true, and handleSubmit passes an empty key so the backend preserves
the existing key.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: replace window.location with useNavigate for routing in auth components (#1470)

Co-authored-by: Dean <afei.wuhao@gmail.com>

* feat(trader): implement margin mode handling for order and leverage settings

* refactor(trader): update SetMarginMode to avoid legacy endpoint and improve logging

* feat(api): enhance strategy handling by integrating claw402 wallet key validation

Added validation for the claw402 model's wallet key during strategy test runs. If the selected AI model is claw402, the server now checks for a valid wallet key and returns appropriate error messages if it's missing or if the model fails to load. This ensures better error handling and user feedback when working with AI models.

* refactor(api): streamline claw402 wallet key retrieval and error handling

Refactored the strategy handling logic to encapsulate claw402 wallet key retrieval in a new method, `resolveStrategyDataWalletKey`. This improves code readability and maintains consistent error handling for missing or invalid wallet keys during strategy test runs. The changes enhance the overall robustness of the AI model integration.

* feat(trader): add claw402 wallet key resolution for trader configuration

Implemented a new method, `resolveTraderDataWalletKey`, to retrieve the claw402 wallet key based on the selected AI model and user ID. This enhancement allows for better integration of the claw402 model within the trader configuration, ensuring that the correct wallet key is used for trading operations. The `AutoTraderConfig` struct has been updated to include the new `Claw402WalletKey` field, improving the overall handling of wallet keys in the trading process.

* feat(claw402): preflight USDC balance before AI calls (#1479)

* chore: ignore nofx-server build artifact

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(claw402): preflight USDC balance before AI calls

Short-circuit claw402 Call/CallWithRequestFull when the wallet balance
can't cover the estimated cost of the call, surfacing ErrInsufficientFunds
instead of letting x402 fail mid-flight after the sign step.

- wallet: cached balance lookup (30s TTL, per-address mutex) to avoid
  hammering the Base RPC; separate error-returning and display-only APIs
  so callers can distinguish zero balance from an unreachable RPC.
- claw402: 1.5× safety multiplier on the flat per-call estimate, 4.0×
  for reasoner models whose chain-of-thought cost can blow past the
  flat rate. Fail-open on RPC errors — x402 still gates actually-empty
  wallets, and we prefer availability over extra strictness.
- shortAddr redacts the wallet in error strings to avoid leaking the
  full address into telemetry bundles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix(telemetry): report token usage for SSE streaming paths (#1475)

* fix(telemetry): report token usage for SSE streaming paths

ParseSSEStream already parsed the usage block from SSE chunks but only
printed it, so claw402 streaming calls (and native streaming) never
fired TokenUsageCallback. GA4 therefore undercounted AI usage on the
streaming path.

Return the parsed usage from ParseSSEStream and have both callers fire
the callback with their own Provider/Model.

* chore: drop leftover debug Printf in ParseSSEStream

Telemetry is now wired via TokenUsageCallback, so the Printf is
redundant noise in the stream path.

* fix(gemini): update default model to gemini-3.1-pro

Google discontinued gemini-3-pro-preview on 2026-03-26 and directs all
callers to gemini-3.1-pro / gemini-3.1-pro-preview. Users on their own
API key were getting errors from the native Gemini endpoint because the
provider default pointed at the retired ID. Claw402 was unaffected
because its route map already used gemini-3.1-pro.

Align both the native provider default and the handler's preset list
with gemini-3.1-pro so every code path sends a live model ID.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: extract ResolveClaw402WalletKey to store layer and expand OKX margin mode tests

- Move duplicated claw402 wallet resolution logic into store.AIModelStore.ResolveClaw402WalletKey
- api/strategy.go and manager/trader_manager.go now delegate to the shared method
- Add detailed doc comment on OKX SetMarginMode explaining the local-state-only approach
  and why the legacy /api/v5/account/set-isolated-mode endpoint is not called
- Add 3 new test cases: cross mode leverage, OpenShort tdMode, SetTakeProfit tdMode

* fix(auth): prevent SetupPage remount from wiping freshly-set auth token (#1481)

After #1470 moved routing into react-router, SetupPage is rendered at two
different tree positions (top-level guard + /setup Route). When register
success flushSync-sets `user`, the top-level guard stops matching and the
Route-level SetupPage mounts as a new instance, re-running its cleanup
useEffect and removing the auth_token that handlePostAuthSuccess just wrote.
Subsequent requests 401 and bounce the user back to /login.

Redirect /setup to /welcome when user is already set so SetupPage is never
re-mounted during the auth transition.

* fix(wallet): handle JSON-RPC null error field in balance query

Some RPC implementations return explicit "error": null on success.
json.RawMessage deserializes this as the 4-byte literal "null", so
len() > 0 was true, causing every balance query to fail with
"rpc error: null". Skip the null literal to avoid false positives.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: deanokk <wuhao@vergex.trade>
Co-authored-by: Dean <afei.wuhao@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: root <root@localhost.localdomain>
This commit is contained in:
Lance
2026-04-17 19:13:35 +08:00
committed by GitHub
parent c93ee337a7
commit 7ae5bf8247
39 changed files with 2549 additions and 1357 deletions

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react'
import { Eye, EyeOff } from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import { toast } from 'sonner'
import { useAuth } from '../../contexts/AuthContext'
import { useLanguage } from '../../contexts/LanguageContext'
@@ -13,12 +14,15 @@ import { invalidateSystemConfig } from '../../lib/config'
export function LoginPage() {
const { language } = useLanguage()
const { login } = useAuth()
const navigate = useNavigate()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [showPassword, setShowPassword] = useState(false)
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const [expiredToastId, setExpiredToastId] = useState<string | number | null>(null)
const [expiredToastId, setExpiredToastId] = useState<string | number | null>(
null
)
const [mode, setMode] = useState<UserMode>('beginner')
// Clean up stale auth state once on mount
@@ -31,7 +35,9 @@ export function LoginPage() {
// Show session-expired toast (re-runs on language change to update text)
useEffect(() => {
if (sessionStorage.getItem('from401') === 'true') {
const id = toast.warning(t('sessionExpired', language), { duration: Infinity })
const id = toast.warning(t('sessionExpired', language), {
duration: Infinity,
})
setExpiredToastId(id)
sessionStorage.removeItem('from401')
}
@@ -48,7 +54,9 @@ export function LoginPage() {
sessionStorage.removeItem('from401')
invalidateSystemConfig()
toast.success(t('forgotAccountSuccess', language))
setTimeout(() => { window.location.href = '/setup' }, 1500)
setTimeout(() => {
navigate('/setup')
}, 1500)
} else {
const data = await res.json()
toast.error(data.error || 'Reset failed')
@@ -79,23 +87,27 @@ export function LoginPage() {
<div className="flex-1 flex items-center justify-center px-4 py-16">
<div className="w-full max-w-sm">
{/* Logo + Title */}
<div className="text-center mb-10">
<div className="flex justify-center mb-5">
<div className="relative">
<div className="absolute -inset-3 bg-nofx-gold/15 rounded-full blur-2xl" />
<img src="/icons/nofx.svg" alt="NOFX" className="w-14 h-14 relative z-10" />
<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">Welcome back</h1>
<h1 className="text-2xl font-bold text-white mb-1.5">
Welcome back
</h1>
<p className="text-zinc-500 text-sm">Sign in to your account</p>
</div>
{/* Card */}
<div className="bg-zinc-900/60 backdrop-blur-xl border border-zinc-800/80 rounded-2xl p-8 shadow-2xl">
<form onSubmit={handleLogin} className="space-y-5">
{/* Email */}
<div>
<label className="block text-xs font-medium text-zinc-400 mb-2">
@@ -120,7 +132,7 @@ export function LoginPage() {
</label>
<button
type="button"
onClick={() => window.location.href = '/reset-password'}
onClick={() => navigate('/reset-password')}
className="text-xs text-zinc-500 hover:text-nofx-gold transition-colors"
>
{t('forgotPassword', language)}
@@ -164,7 +176,9 @@ export function LoginPage() {
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"
>
{loading ? t('loggingIn', language) || 'Signing in...' : t('signIn', language) || 'Sign In'}
{loading
? t('loggingIn', language) || 'Signing in...'
: t('signIn', language) || 'Sign In'}
</button>
</form>
@@ -178,7 +192,6 @@ export function LoginPage() {
</button>
</div>
</div>
</div>
</div>
</DeepVoidBackground>

View File

@@ -1,5 +1,6 @@
import { motion, AnimatePresence } from 'framer-motion'
import { LogIn, UserPlus, X, AlertTriangle, Terminal } from 'lucide-react'
import { Link } from 'react-router-dom'
import { DeepVoidBackground } from '../common/DeepVoidBackground'
import { useLanguage } from '../../contexts/LanguageContext'
import { t } from '../../i18n/translations'
@@ -10,7 +11,11 @@ interface LoginRequiredOverlayProps {
featureName?: string
}
export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequiredOverlayProps) {
export function LoginRequiredOverlay({
isOpen,
onClose,
featureName,
}: LoginRequiredOverlayProps) {
const { language } = useLanguage()
const tr = (key: string, params?: Record<string, string | number>) =>
@@ -20,11 +25,7 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
? tr('subtitleWithFeature', { featureName })
: tr('subtitleDefault')
const benefits = [
tr('benefit1'),
tr('benefit2'),
tr('benefit4'),
]
const benefits = [tr('benefit1'), tr('benefit2'), tr('benefit4')]
return (
<AnimatePresence>
@@ -40,7 +41,6 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
disableAnimation
onClick={onClose}
>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
@@ -53,7 +53,9 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
<div className="flex items-center justify-between px-3 py-2 bg-nofx-bg-lighter border-b border-nofx-gold/20">
<div className="flex items-center gap-2">
<Terminal size={12} className="text-nofx-gold" />
<span className="text-[10px] text-nofx-text-muted uppercase tracking-wider">auth_protocol.exe</span>
<span className="text-[10px] text-nofx-text-muted uppercase tracking-wider">
auth_protocol.exe
</span>
</div>
<button
onClick={onClose}
@@ -75,7 +77,9 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
<div className="absolute inset-0 bg-red-500/20 blur-xl animate-pulse"></div>
<div className="bg-nofx-bg border border-red-500/50 text-red-500 px-4 py-2 flex items-center gap-3 shadow-[0_0_15px_rgba(239,68,68,0.2)]">
<AlertTriangle size={18} className="animate-pulse" />
<span className="font-bold tracking-widest text-sm uppercase">{tr('accessDenied')}</span>
<span className="font-bold tracking-widest text-sm uppercase">
{tr('accessDenied')}
</span>
</div>
</div>
</div>
@@ -83,8 +87,12 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
{/* 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">{tr('title')}</h2>
<p className="text-nofx-gold text-xs uppercase tracking-widest border-b border-nofx-gold/20 pb-4 inline-block">{subtitle}</p>
<h2 className="text-xl font-bold text-white 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">
{subtitle}
</p>
</div>
<div className="bg-nofx-bg-lighter border-l-2 border-nofx-gold/20 p-3 my-4">
@@ -96,7 +104,10 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
<div className="grid grid-cols-2 gap-2">
{benefits.map((benefit, i) => (
<div key={i} className="flex items-center gap-2 text-[10px] text-nofx-text-muted uppercase tracking-wide">
<div
key={i}
className="flex items-center gap-2 text-[10px] text-nofx-text-muted uppercase tracking-wide"
>
<span className="text-nofx-gold"></span> {benefit}
</div>
))}
@@ -105,22 +116,24 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
{/* Action Buttons */}
<div className="space-y-3">
<a
href="/login"
<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"
>
<LogIn size={14} />
<span>{tr('loginButton')}</span>
<span className="opacity-0 group-hover:opacity-100 transition-opacity -ml-2 group-hover:ml-0">-&gt;</span>
</a>
<span className="opacity-0 group-hover:opacity-100 transition-opacity -ml-2 group-hover:ml-0">
-&gt;
</span>
</Link>
<a
href="/register"
<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"
>
<UserPlus size={14} />
<span>{tr('registerButton')}</span>
</a>
</Link>
</div>
<div className="mt-4 text-center">
@@ -131,14 +144,12 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
[ {tr('abort')} ]
</button>
</div>
</div>
</div>
{/* Corner Accents */}
<div className="absolute top-0 right-0 w-2 h-2 border-t border-r border-nofx-gold"></div>
<div className="absolute bottom-0 left-0 w-2 h-2 border-b border-l border-nofx-gold"></div>
</motion.div>
</DeepVoidBackground>
</motion.div>

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react'
import { Eye, EyeOff } from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import PasswordChecklist from 'react-password-checklist'
import { toast } from 'sonner'
import { useAuth } from '../../contexts/AuthContext'
@@ -13,6 +14,7 @@ import { WhitelistFullPage } from '../common/WhitelistFullPage'
export function RegisterPage() {
const { language } = useLanguage()
const { register } = useAuth()
const navigate = useNavigate()
const [view, setView] = useState<'register' | 'whitelist-full'>('register')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
@@ -61,7 +63,11 @@ export function RegisterPage() {
setLoading(true)
try {
const result = await register(email, password, betaCode.trim() || undefined)
const result = await register(
email,
password,
betaCode.trim() || undefined
)
const isWhitelistError = (msg: string) => {
const lowerMsg = msg.toLowerCase()
@@ -86,7 +92,10 @@ export function RegisterPage() {
// success path is handled in AuthContext (auto login + navigation)
} catch (e) {
console.error('Registration error:', e)
const errorMsg = e instanceof Error ? e.message : 'Registration failed due to server error'
const errorMsg =
e instanceof Error
? e.message
: 'Registration failed due to server error'
const lowerMsg = errorMsg.toLowerCase()
if (
lowerMsg.includes('whitelist') ||
@@ -106,15 +115,20 @@ export function RegisterPage() {
}
return (
<DeepVoidBackground className="min-h-screen flex items-center justify-center py-12 font-mono" disableAnimation>
<DeepVoidBackground
className="min-h-screen flex items-center justify-center py-12 font-mono"
disableAnimation
>
<div className="w-full max-w-lg relative z-10 px-6">
<div className="flex justify-between items-center mb-8">
<button
onClick={() => (window.location.href = '/')}
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"
>
<div className="w-2 h-2 rounded-full bg-red-500 group-hover:animate-pulse"></div>
<span className="text-xs font-mono uppercase tracking-widest">&lt; ABORT_REGISTRATION</span>
<span className="text-xs font-mono uppercase tracking-widest">
&lt; ABORT_REGISTRATION
</span>
</button>
</div>
@@ -122,7 +136,11 @@ export function RegisterPage() {
<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" className="w-16 h-16 object-contain relative z-10 opacity-90" />
<img
src="/icons/nofx.svg"
alt="NoFx Logo"
className="w-16 h-16 object-contain relative z-10 opacity-90"
/>
</div>
</div>
<h1 className="text-3xl font-bold tracking-tighter text-white uppercase mb-2">
@@ -140,7 +158,7 @@ export function RegisterPage() {
<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"
onClick={() => (window.location.href = '/')}
onClick={() => navigate('/')}
title="Close / Return Home"
></div>
<div className="w-2.5 h-2.5 rounded-full bg-yellow-500/50"></div>
@@ -155,7 +173,9 @@ export function RegisterPage() {
<div className="mb-6 font-mono text-xs space-y-1 text-zinc-500 border-b border-zinc-800/50 pb-4">
<div className="flex gap-2">
<span className="text-emerald-500"></span>
<span>System Check: <span className="text-emerald-500">READY</span></span>
<span>
System Check: <span className="text-emerald-500">READY</span>
</span>
</div>
<div className="flex gap-2">
<span className="text-emerald-500"></span>
@@ -165,7 +185,9 @@ export function RegisterPage() {
<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">{t('email', language)}</label>
<label className="block text-xs uppercase tracking-wider text-zinc-500 mb-1.5 ml-1 font-bold">
{t('email', language)}
</label>
<input
type="email"
value={email}
@@ -178,7 +200,9 @@ 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">{t('password', language)}</label>
<label className="block text-xs uppercase tracking-wider text-zinc-500 mb-1.5 ml-1 font-bold">
{t('password', language)}
</label>
<div className="relative">
<input
type={showPassword ? 'text' : 'password'}
@@ -199,7 +223,9 @@ export function RegisterPage() {
</div>
<div>
<label className="block text-xs uppercase tracking-wider text-zinc-500 mb-1.5 ml-1 font-bold">{t('confirmPassword', language)}</label>
<label className="block text-xs uppercase tracking-wider text-zinc-500 mb-1.5 ml-1 font-bold">
{t('confirmPassword', language)}
</label>
<div className="relative">
<input
type={showConfirmPassword ? 'text' : 'password'}
@@ -211,10 +237,16 @@ export function RegisterPage() {
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
onClick={() =>
setShowConfirmPassword(!showConfirmPassword)
}
className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-600 hover:text-zinc-400 transition-colors"
>
{showConfirmPassword ? <EyeOff size={16} /> : <Eye size={16} />}
{showConfirmPassword ? (
<EyeOff size={16} />
) : (
<Eye size={16} />
)}
</button>
</div>
</div>
@@ -227,7 +259,14 @@ export function RegisterPage() {
</div>
<div className="text-xs font-mono text-zinc-400">
<PasswordChecklist
rules={['minLength', 'capital', 'lowercase', 'number', 'specialChar', 'match']}
rules={[
'minLength',
'capital',
'lowercase',
'number',
'specialChar',
'match',
]}
minLength={8}
value={password}
valueAgain={confirmPassword}
@@ -248,17 +287,25 @@ export function RegisterPage() {
{betaMode && (
<div>
<label className="block text-xs uppercase tracking-wider text-nofx-gold mb-1.5 ml-1 font-bold">Priority Access Code</label>
<label className="block text-xs uppercase tracking-wider text-nofx-gold mb-1.5 ml-1 font-bold">
Priority Access Code
</label>
<input
type="text"
value={betaCode}
onChange={(e) => setBetaCode(e.target.value.replace(/[^a-z0-9]/gi, '').toLowerCase())}
onChange={(e) =>
setBetaCode(
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"
placeholder="XXXXXX"
maxLength={6}
required={betaMode}
/>
<p className="text-[10px] text-zinc-600 font-mono mt-1 ml-1">* CASE SENSITIVE ALPHANUMERIC</p>
<p className="text-[10px] text-zinc-600 font-mono mt-1 ml-1">
* CASE SENSITIVE ALPHANUMERIC
</p>
</div>
)}
@@ -270,7 +317,9 @@ export function RegisterPage() {
<button
type="submit"
disabled={loading || (betaMode && !betaCode.trim()) || !passwordValid}
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"
>
{loading ? (
@@ -278,7 +327,9 @@ export function RegisterPage() {
) : (
<>
<span>CREATE_ACCOUNT</span>
<span className="group-hover:translate-x-1 transition-transform">-&gt;</span>
<span className="group-hover:translate-x-1 transition-transform">
-&gt;
</span>
</>
)}
</button>
@@ -295,14 +346,14 @@ export function RegisterPage() {
<p className="text-xs font-mono text-zinc-500">
EXISTING_OPERATOR?{' '}
<button
onClick={() => (window.location.href = '/login')}
onClick={() => navigate('/login')}
className="text-nofx-gold hover:underline hover:text-yellow-300 transition-colors ml-1 uppercase"
>
ACCESS TERMINAL
</button>
</p>
<button
onClick={() => (window.location.href = '/')}
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"
>
[ ABORT_REGISTRATION_RETURN_HOME ]

View File

@@ -1,8 +1,19 @@
import { describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import { MemoryRouter } from 'react-router-dom'
import { RegistrationDisabled } from './RegistrationDisabled'
import { LanguageProvider } from '../../contexts/LanguageContext'
const mockNavigate = vi.fn()
vi.mock('react-router-dom', async () => {
const actual = await vi.importActual('react-router-dom')
return {
...actual,
useNavigate: () => mockNavigate,
}
})
// Mock useLanguage hook
vi.mock('../../contexts/LanguageContext', async () => {
const actual = await vi.importActual('../../contexts/LanguageContext')
@@ -21,9 +32,11 @@ vi.mock('../../contexts/LanguageContext', async () => {
describe('RegistrationDisabled Component', () => {
const renderComponent = () => {
return render(
<LanguageProvider>
<RegistrationDisabled />
</LanguageProvider>
<MemoryRouter>
<LanguageProvider>
<RegistrationDisabled />
</LanguageProvider>
</MemoryRouter>
)
}
@@ -48,7 +61,9 @@ describe('RegistrationDisabled Component', () => {
it('should display registration closed message', () => {
renderComponent()
const message = screen.getByText(/User registration is currently disabled/i)
const message = screen.getByText(
/User registration is currently disabled/i
)
expect(message).toBeTruthy()
})
@@ -61,19 +76,12 @@ describe('RegistrationDisabled Component', () => {
describe('Navigation', () => {
it('should navigate to login page when button is clicked', () => {
const pushStateSpy = vi.spyOn(window.history, 'pushState')
const dispatchEventSpy = vi.spyOn(window, 'dispatchEvent')
renderComponent()
const button = screen.getByRole('button', { name: /back to login/i })
fireEvent.click(button)
expect(pushStateSpy).toHaveBeenCalledWith({}, '', '/login')
expect(dispatchEventSpy).toHaveBeenCalled()
pushStateSpy.mockRestore()
dispatchEventSpy.mockRestore()
expect(mockNavigate).toHaveBeenCalledWith('/login')
})
})

View File

@@ -1,12 +1,13 @@
import { useNavigate } from 'react-router-dom'
import { useLanguage } from '../../contexts/LanguageContext'
import { t } from '../../i18n/translations'
export function RegistrationDisabled() {
const { language } = useLanguage()
const navigate = useNavigate()
const handleBackToLogin = () => {
window.history.pushState({}, '', '/login')
window.dispatchEvent(new PopStateEvent('popstate'))
navigate('/login')
}
return (

View File

@@ -1,4 +1,5 @@
import React, { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useAuth } from '../../contexts/AuthContext'
import { useLanguage } from '../../contexts/LanguageContext'
import { t } from '../../i18n/translations'
@@ -11,6 +12,7 @@ import { toast } from 'sonner'
export function ResetPasswordPage() {
const { language } = useLanguage()
const { resetPassword } = useAuth()
const navigate = useNavigate()
const [email, setEmail] = useState('')
const [newPassword, setNewPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
@@ -41,8 +43,7 @@ export function ResetPasswordPage() {
toast.success(t('resetPasswordSuccess', language) || '重置成功')
// 3秒后跳转到登录页面
setTimeout(() => {
window.history.pushState({}, '', '/login')
window.dispatchEvent(new PopStateEvent('popstate'))
navigate('/login')
}, 3000)
} else {
const msg = result.message || t('resetPasswordFailed', language)
@@ -64,10 +65,7 @@ export function ResetPasswordPage() {
<div className="w-full max-w-md">
{/* Back to Login */}
<button
onClick={() => {
window.history.pushState({}, '', '/login')
window.dispatchEvent(new PopStateEvent('popstate'))
}}
onClick={() => navigate('/login')}
className="flex items-center gap-2 mb-6 text-sm hover:text-[#F0B90B] transition-colors"
style={{ color: '#848E9C' }}
>

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
import { useLocation, useNavigate } from 'react-router-dom'
import { motion, AnimatePresence } from 'framer-motion'
import { Menu, X, ChevronDown, Settings } from 'lucide-react'
import { t, type Language } from '../../i18n/translations'
@@ -10,17 +10,7 @@ import {
setUserMode,
type UserMode,
} from '../../lib/onboarding'
type Page =
| 'competition'
| 'traders'
| 'trader'
| 'strategy'
| 'strategy-market'
| 'data'
| 'faq'
| 'login'
| 'register'
import { getCurrentPageForPath, ROUTES, type Page } from '../../router/paths'
interface HeaderBarProps {
onLoginClick?: () => void
@@ -47,16 +37,20 @@ export default function HeaderBar({
onLoginRequired,
}: HeaderBarProps) {
const navigate = useNavigate()
const location = useLocation()
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const [languageDropdownOpen, setLanguageDropdownOpen] = useState(false)
const [userDropdownOpen, setUserDropdownOpen] = useState(false)
const [userMode, setUserModeState] = useState<UserMode>(() => getUserMode() ?? 'advanced')
const [userMode, setUserModeState] = useState<UserMode>(
() => getUserMode() ?? 'advanced'
)
const dropdownRef = useRef<HTMLDivElement>(null)
const userDropdownRef = useRef<HTMLDivElement>(null)
const resolvedCurrentPage =
currentPage ?? getCurrentPageForPath(location.pathname)
const navigateInApp = (path: string) => {
navigate(path)
window.dispatchEvent(new PopStateEvent('popstate'))
}
const handleSwitchMode = (nextMode: UserMode) => {
@@ -94,14 +88,12 @@ export default function HeaderBar({
{/* Logo - Always go to home page */}
<div
onClick={() => {
window.location.href = '/'
navigateInApp(ROUTES.home)
}}
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="text-lg font-bold text-nofx-gold">NOFX</span>
</div>
{/* Desktop Menu */}
@@ -111,17 +103,67 @@ export default function HeaderBar({
{/* Navigation tabs configuration */}
{(() => {
// Define all navigation tabs
const navTabs: { page: Page; path: string; label: string; requiresAuth: boolean }[] = [
{ page: 'data', path: '/data', label: language === 'zh' ? '数据' : language === 'id' ? 'Data' : 'Data', requiresAuth: false },
{ page: 'strategy-market', path: '/strategy-market', label: language === 'zh' ? '策略市场' : language === 'id' ? 'Pasar' : 'Market', requiresAuth: true },
{ page: 'traders', path: '/traders', label: t('configNav', language), requiresAuth: true },
{ page: 'trader', path: '/dashboard', label: t('dashboardNav', language), requiresAuth: true },
{ page: 'strategy', path: '/strategy', label: t('strategyNav', language), requiresAuth: true },
{ page: 'competition', path: '/competition', label: t('realtimeNav', language), requiresAuth: true },
{ page: 'faq', path: '/faq', label: t('faqNav', language), requiresAuth: false },
const navTabs: {
page: Page
path: string
label: string
requiresAuth: boolean
}[] = [
{
page: 'data',
path: ROUTES.data,
label:
language === 'zh'
? '数据'
: language === 'id'
? 'Data'
: 'Data',
requiresAuth: false,
},
{
page: 'strategy-market',
path: ROUTES.strategyMarket,
label:
language === 'zh'
? '策略市场'
: language === 'id'
? 'Pasar'
: 'Market',
requiresAuth: true,
},
{
page: 'traders',
path: ROUTES.traders,
label: t('configNav', language),
requiresAuth: true,
},
{
page: 'trader',
path: ROUTES.dashboard,
label: t('dashboardNav', language),
requiresAuth: true,
},
{
page: 'strategy',
path: ROUTES.strategy,
label: t('strategyNav', language),
requiresAuth: true,
},
{
page: 'competition',
path: ROUTES.competition,
label: t('realtimeNav', language),
requiresAuth: true,
},
{
page: 'faq',
path: ROUTES.faq,
label: t('faqNav', language),
requiresAuth: false,
},
]
const handleNavClick = (tab: typeof navTabs[0]) => {
const handleNavClick = (tab: (typeof navTabs)[0]) => {
// If requires auth and not logged in, show login prompt
if (tab.requiresAuth && !isLoggedIn) {
onLoginRequired?.(tab.label)
@@ -131,7 +173,7 @@ export default function HeaderBar({
if (onPageChange) {
onPageChange(tab.page)
}
navigate(tab.path)
navigateInApp(tab.path)
}
return navTabs.map((tab) => (
@@ -139,12 +181,10 @@ export default function HeaderBar({
key={tab.page}
onClick={() => handleNavClick(tab)}
className={`text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500 px-3 py-2 rounded-lg
${currentPage === tab.page ? 'text-nofx-gold' : 'text-nofx-text-muted hover:text-nofx-gold'}`}
${resolvedCurrentPage === tab.page ? 'text-nofx-gold' : 'text-nofx-text-muted hover:text-nofx-gold'}`}
>
{currentPage === tab.page && (
<span
className="absolute inset-0 rounded-lg bg-nofx-gold/15 -z-10"
/>
{resolvedCurrentPage === tab.page && (
<span className="absolute inset-0 rounded-lg bg-nofx-gold/15 -z-10" />
)}
{tab.label}
</button>
@@ -164,7 +204,12 @@ export default function HeaderBar({
className="p-2 rounded-lg transition-all hover:scale-110 text-nofx-text-muted hover:text-white hover:bg-white/5"
title="GitHub"
>
<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor">
<svg
width="18"
height="18"
viewBox="0 0 16 16"
fill="currentColor"
>
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
</svg>
</a>
@@ -176,7 +221,12 @@ export default function HeaderBar({
className="p-2 rounded-lg transition-all hover:scale-110 text-nofx-text-muted hover:text-[#1DA1F2] hover:bg-[#1DA1F2]/10"
title="Twitter"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
</a>
@@ -188,7 +238,12 @@ export default function HeaderBar({
className="p-2 rounded-lg transition-all hover:scale-110 text-nofx-text-muted hover:text-[#0088cc] hover:bg-[#0088cc]/10"
title="Telegram"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
</svg>
</a>
@@ -227,7 +282,7 @@ export default function HeaderBar({
</div>
<button
onClick={() => {
window.location.href = '/settings'
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"
@@ -236,13 +291,21 @@ export default function HeaderBar({
Settings
</button>
<button
onClick={() => handleSwitchMode(userMode === 'beginner' ? 'advanced' : 'beginner')}
onClick={() =>
handleSwitchMode(
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"
>
<Settings className="w-3.5 h-3.5" />
{userMode === 'beginner'
? language === 'zh' ? '切到老手模式' : 'Switch to Advanced'
: language === 'zh' ? '切到手模式' : 'Switch to Beginner'}
? language === 'zh'
? '切到手模式'
: 'Switch to Advanced'
: language === 'zh'
? '切到新手模式'
: 'Switch to Beginner'}
</button>
{onLogout && (
<button
@@ -261,15 +324,16 @@ export default function HeaderBar({
</div>
) : (
/* Show login/register buttons when not logged in and not on login/register pages */
currentPage !== 'login' &&
currentPage !== 'register' && (
resolvedCurrentPage !== 'login' &&
resolvedCurrentPage !== 'register' && (
<div className="flex items-center gap-3">
<a
href="/login"
<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"
>
{t('signIn', language)}
</a>
</button>
</div>
)
)}
@@ -361,17 +425,67 @@ export default function HeaderBar({
{/* Navigation Links */}
<div className="flex flex-col gap-6 mb-12">
{(() => {
const navTabs: { page: Page; path: string; label: string; requiresAuth: boolean }[] = [
{ page: 'data', path: '/data', label: language === 'zh' ? '数据' : language === 'id' ? 'Data' : 'Data', requiresAuth: false },
{ page: 'strategy-market', path: '/strategy-market', label: language === 'zh' ? '策略市场' : language === 'id' ? 'Pasar' : 'Market', requiresAuth: true },
{ page: 'traders', path: '/traders', label: t('configNav', language), requiresAuth: true },
{ page: 'trader', path: '/dashboard', label: t('dashboardNav', language), requiresAuth: true },
{ page: 'strategy', path: '/strategy', label: t('strategyNav', language), requiresAuth: true },
{ page: 'competition', path: '/competition', label: t('realtimeNav', language), requiresAuth: true },
{ page: 'faq', path: '/faq', label: t('faqNav', language), requiresAuth: false },
const navTabs: {
page: Page
path: string
label: string
requiresAuth: boolean
}[] = [
{
page: 'data',
path: ROUTES.data,
label:
language === 'zh'
? '数据'
: language === 'id'
? 'Data'
: 'Data',
requiresAuth: false,
},
{
page: 'strategy-market',
path: ROUTES.strategyMarket,
label:
language === 'zh'
? '策略市场'
: language === 'id'
? 'Pasar'
: 'Market',
requiresAuth: true,
},
{
page: 'traders',
path: ROUTES.traders,
label: t('configNav', language),
requiresAuth: true,
},
{
page: 'trader',
path: ROUTES.dashboard,
label: t('dashboardNav', language),
requiresAuth: true,
},
{
page: 'strategy',
path: ROUTES.strategy,
label: t('strategyNav', language),
requiresAuth: true,
},
{
page: 'competition',
path: ROUTES.competition,
label: t('realtimeNav', language),
requiresAuth: true,
},
{
page: 'faq',
path: ROUTES.faq,
label: t('faqNav', language),
requiresAuth: false,
},
]
const handleMobileNavClick = (tab: typeof navTabs[0]) => {
const handleMobileNavClick = (tab: (typeof navTabs)[0]) => {
if (tab.requiresAuth && !isLoggedIn) {
onLoginRequired?.(tab.label)
setMobileMenuOpen(false)
@@ -380,7 +494,7 @@ export default function HeaderBar({
if (onPageChange) {
onPageChange(tab.page)
}
navigate(tab.path)
navigateInApp(tab.path)
setMobileMenuOpen(false)
}
@@ -392,9 +506,9 @@ export default function HeaderBar({
transition={{ delay: 0.1 + i * 0.05 }}
onClick={() => handleMobileNavClick(tab)}
className={`text-2xl font-black tracking-tight text-left flex items-center gap-3
${currentPage === tab.page ? 'text-nofx-gold' : 'text-zinc-500'}`}
${resolvedCurrentPage === tab.page ? 'text-nofx-gold' : 'text-zinc-500'}`}
>
{currentPage === tab.page && (
{resolvedCurrentPage === tab.page && (
<motion.div
layoutId="active-indicator"
className="w-1.5 h-1.5 rounded-full bg-nofx-gold"
@@ -438,9 +552,24 @@ export default function HeaderBar({
{/* Social Links */}
<div className="flex items-center gap-4">
{[
{ href: OFFICIAL_LINKS.github, icon: <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" /> },
{ href: OFFICIAL_LINKS.twitter, icon: <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" /> },
{ href: OFFICIAL_LINKS.telegram, icon: <path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" /> }
{
href: OFFICIAL_LINKS.github,
icon: (
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
),
},
{
href: OFFICIAL_LINKS.twitter,
icon: (
<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" />
),
},
{
href: OFFICIAL_LINKS.telegram,
icon: (
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
),
},
].map((link, i) => (
<a
key={i}
@@ -449,7 +578,12 @@ export default function HeaderBar({
rel="noopener noreferrer"
className="w-12 h-12 rounded-full bg-zinc-900 border border-zinc-800 flex items-center justify-center text-zinc-500 hover:text-nofx-gold hover:border-nofx-gold transition-colors"
>
<svg width="20" height="20" viewBox="0 0 16 16" fill="currentColor">
<svg
width="20"
height="20"
viewBox="0 0 16 16"
fill="currentColor"
>
{link.icon}
</svg>
</a>
@@ -467,10 +601,11 @@ export default function HeaderBar({
onLanguageChange?.(lang as Language)
setMobileMenuOpen(false)
}}
className={`flex-1 py-3 text-sm font-bold rounded-md transition-colors ${language === lang
? 'bg-zinc-800 text-white shadow-sm'
: 'text-zinc-500'
}`}
className={`flex-1 py-3 text-sm font-bold rounded-md transition-colors ${
language === lang
? 'bg-zinc-800 text-white shadow-sm'
: 'text-zinc-500'
}`}
>
{lang === 'zh' ? 'CN' : lang === 'id' ? 'ID' : 'EN'}
</button>
@@ -489,13 +624,18 @@ export default function HeaderBar({
{t('exitLogin', language)}
</button>
) : (
currentPage !== 'login' && currentPage !== 'register' && (
<a
href="/login"
resolvedCurrentPage !== 'login' &&
resolvedCurrentPage !== 'register' && (
<button
type="button"
onClick={() => {
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"
>
{t('signIn', language)}
</a>
</button>
)
)}
</div>

View File

@@ -0,0 +1,103 @@
import { OFFICIAL_LINKS } from '../../constants/branding'
import { t, type Language } from '../../i18n/translations'
interface SiteFooterProps {
language: Language
}
export function SiteFooter({ language }: SiteFooterProps) {
return (
<footer
className="mt-16"
style={{ borderTop: '1px solid #2B3139', background: '#181A20' }}
>
<div
className="max-w-[1920px] mx-auto px-6 py-6 text-center text-sm"
style={{ color: '#5E6673' }}
>
<p>{t('footerTitle', language)}</p>
<p className="mt-1">{t('footerWarning', language)}</p>
<div className="mt-4 flex items-center justify-center gap-3 flex-wrap">
<a
href={OFFICIAL_LINKS.github}
target="_blank"
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',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#2B3139'
e.currentTarget.style.color = '#EAECEF'
e.currentTarget.style.borderColor = '#F0B90B'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#1E2329'
e.currentTarget.style.color = '#848E9C'
e.currentTarget.style.borderColor = '#2B3139'
}}
>
<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
</svg>
GitHub
</a>
<a
href={OFFICIAL_LINKS.twitter}
target="_blank"
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',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#2B3139'
e.currentTarget.style.color = '#EAECEF'
e.currentTarget.style.borderColor = '#1DA1F2'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#1E2329'
e.currentTarget.style.color = '#848E9C'
e.currentTarget.style.borderColor = '#2B3139'
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
Twitter
</a>
<a
href={OFFICIAL_LINKS.telegram}
target="_blank"
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',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#2B3139'
e.currentTarget.style.color = '#EAECEF'
e.currentTarget.style.borderColor = '#0088cc'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#1E2329'
e.currentTarget.style.color = '#848E9C'
e.currentTarget.style.borderColor = '#2B3139'
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
</svg>
Telegram
</a>
</div>
</div>
</footer>
)
}

View File

@@ -1,5 +1,6 @@
import { motion } from 'framer-motion'
import { ShieldAlert, ArrowLeft, Twitter, Send, Lock } from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import { OFFICIAL_LINKS } from '../../constants/branding'
interface WhitelistFullPageProps {
@@ -7,11 +8,13 @@ interface WhitelistFullPageProps {
}
export function WhitelistFullPage({ onBack }: WhitelistFullPageProps) {
const navigate = useNavigate()
const handleBackToLogin = () => {
if (onBack) {
onBack()
} else {
window.location.href = '/login'
navigate('/login')
}
}
@@ -29,7 +32,6 @@ export function WhitelistFullPage({ onBack }: WhitelistFullPageProps) {
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">
{/* 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 gap-1.5 opacity-50">
@@ -60,9 +62,13 @@ export function WhitelistFullPage({ onBack }: WhitelistFullPageProps) {
{/* 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 IDENTIFIER IS NOT ON THE ACTIVE WHITELIST.
<br /><br />
Platform capacity limits have been reached for the current beta phase. Prioritized access is currently reserved for authorized operators only.
<span className="text-red-400">[SYSTEM_MESSAGE]:</span> YOUR
IDENTIFIER IS NOT ON THE ACTIVE WHITELIST.
<br />
<br />
Platform capacity limits have been reached for the current beta
phase. Prioritized access is currently reserved for authorized
operators only.
</p>
{/* Info Box */}
@@ -70,9 +76,13 @@ export function WhitelistFullPage({ onBack }: WhitelistFullPageProps) {
<div className="flex items-start gap-3">
<Lock className="w-4 h-4 text-red-500 mt-0.5" />
<div>
<h3 className="text-xs font-bold text-red-400 uppercase mb-1">Authorization Protocol</h3>
<h3 className="text-xs font-bold text-red-400 uppercase mb-1">
Authorization Protocol
</h3>
<p className="text-[10px] text-zinc-500 leading-tight">
Access is rolled out in batches. If you believe this is an error, please verify your credentials or contact system administrators.
Access is rolled out in batches. If you believe this is an
error, please verify your credentials or contact system
administrators.
</p>
</div>
</div>
@@ -109,14 +119,12 @@ export function WhitelistFullPage({ onBack }: WhitelistFullPageProps) {
</a>
</div>
</div>
</div>
{/* Footer */}
<div className="bg-black/80 p-2 text-[9px] text-zinc-700 text-center border-t border-zinc-800 font-mono uppercase">
ERR_CODE: WLIST_0x403 // SECURITY_LAYER_ACTIVE
</div>
</div>
</motion.div>
</div>

View File

@@ -1,5 +1,6 @@
import { motion } from 'framer-motion'
import { ArrowRight, Play, Github, Zap } from 'lucide-react'
import { Link } from 'react-router-dom'
import { t, Language } from '../../i18n/translations'
import { useGitHubStats } from '../../hooks/useGitHubStats'
import { useCounterAnimation } from '../../hooks/useCounterAnimation'
@@ -33,7 +34,8 @@ export default function HeroSection({ language }: HeroSectionProps) {
<div
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%)',
background:
'radial-gradient(circle, rgba(240, 185, 11, 0.08) 0%, transparent 70%)',
}}
/>
{/* Floating Orbs */}
@@ -138,8 +140,7 @@ export default function HeroSection({ language }: HeroSectionProps) {
transition={{ duration: 0.6, delay: 0.3 }}
className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-12"
>
<motion.a
href="/competition"
<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%)',
@@ -152,10 +153,12 @@ export default function HeroSection({ language }: HeroSectionProps) {
}}
whileTap={{ scale: 0.98 }}
>
<Play className="w-5 h-5" />
{t('liveCompetition', language) || 'Live Competition'}
<ArrowRight className="w-5 h-5 transition-transform group-hover:translate-x-1" />
</motion.a>
<Link to="/competition" className="flex items-center gap-3">
<Play className="w-5 h-5" />
{t('liveCompetition', language) || 'Live Competition'}
<ArrowRight className="w-5 h-5 transition-transform group-hover:translate-x-1" />
</Link>
</motion.div>
<motion.a
href={OFFICIAL_LINKS.github}
@@ -188,9 +191,18 @@ export default function HeroSection({ language }: HeroSectionProps) {
>
{[
{ label: 'GitHub Stars', value: `${(stars / 1000).toFixed(1)}K+` },
{ label: language === 'zh' ? '支持交易所' : 'Exchanges', value: '5+' },
{ label: language === 'zh' ? 'AI 模型' : 'AI Models', value: '10+' },
{ label: language === 'zh' ? '开源免费' : 'Open Source', value: '100%' },
{
label: language === 'zh' ? '支持交易所' : 'Exchanges',
value: '5+',
},
{
label: language === 'zh' ? 'AI 模型' : 'AI Models',
value: '10+',
},
{
label: language === 'zh' ? '开源免费' : 'Open Source',
value: '100%',
},
].map((stat, index) => (
<motion.div
key={stat.label}
@@ -202,7 +214,8 @@ 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%)',
background:
'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
}}

View File

@@ -1,5 +1,6 @@
import { motion } from 'framer-motion'
import { X } from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import { t, Language } from '../../i18n/translations'
interface LoginModalProps {
onClose: () => void
@@ -7,6 +8,7 @@ interface LoginModalProps {
}
export default function LoginModal({ onClose, language }: LoginModalProps) {
const navigate = useNavigate()
return (
<motion.div
@@ -49,8 +51,7 @@ export default function LoginModal({ onClose, language }: LoginModalProps) {
<div className="space-y-3">
<motion.button
onClick={() => {
window.history.pushState({}, '', '/login')
window.dispatchEvent(new PopStateEvent('popstate'))
navigate('/login')
onClose()
}}
className="block w-full px-6 py-3 rounded-lg font-semibold text-center"

View File

@@ -1,149 +1,178 @@
import { motion } from 'framer-motion'
import { TrendingUp, Layers, Zap, Hexagon, Crosshair } from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import { useAuth } from '../../../contexts/AuthContext'
const agents = [
{
name: "ALPHA-1",
// ... (rest of agents array remains, but I can't skip lines in replacement content easily without context. Wait, let's just replace the top section)
// Actually, I'll use multi_replace for targeted cleanup.
class: "SCALPER",
desc: "High-frequency microstructure exploitation.",
apy: "142%",
winRate: "68%",
risk: "HIGH",
color: "text-nofx-gold",
border: "border-nofx-gold/50",
bg_glow: "shadow-[0_0_30px_rgba(240,185,11,0.1)]",
icon: Zap
},
{
name: "BETA-X",
class: "SWING_OPS",
desc: "Multi-day trend extraction engine.",
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)]",
icon: TrendingUp
},
{
name: "GAMMA-RAY",
class: "ARBITRAGE",
desc: "Low-risk spatial price equalization.",
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)]",
icon: Layers
},
{
name: 'ALPHA-1',
// ... (rest of agents array remains, but I can't skip lines in replacement content easily without context. Wait, let's just replace the top section)
// Actually, I'll use multi_replace for targeted cleanup.
class: 'SCALPER',
desc: 'High-frequency microstructure exploitation.',
apy: '142%',
winRate: '68%',
risk: 'HIGH',
color: 'text-nofx-gold',
border: 'border-nofx-gold/50',
bg_glow: 'shadow-[0_0_30px_rgba(240,185,11,0.1)]',
icon: Zap,
},
{
name: 'BETA-X',
class: 'SWING_OPS',
desc: 'Multi-day trend extraction engine.',
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)]',
icon: TrendingUp,
},
{
name: 'GAMMA-RAY',
class: 'ARBITRAGE',
desc: 'Low-risk spatial price equalization.',
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)]',
icon: Layers,
},
]
export default function AgentGrid() {
const { user } = useAuth()
const { user } = useAuth()
const navigate = useNavigate()
const handleInitialize = () => {
if (user) {
window.location.href = '/strategy-market'
} else {
window.location.href = '/login'
}
const handleInitialize = () => {
if (user) {
navigate('/strategy-market')
} else {
navigate('/login')
}
}
return (
<section id="market-scanner" className="py-16 md:py-24 bg-nofx-bg relative overflow-hidden">
return (
<section
id="market-scanner"
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>
{/* 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="max-w-7xl mx-auto px-6 relative z-10">
<div className="flex flex-col md:flex-row justify-between items-end mb-10 md:mb-16 gap-6">
<div>
<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" /> MARKET SELECT
</div>
<h2 className="text-4xl md:text-5xl font-black text-white uppercase tracking-tighter">
STRATEGY{' '}
<span className="text-transparent bg-clip-text bg-gradient-to-r from-nofx-gold to-white">
UNITS
</span>
</h2>
</div>
<div className="font-mono text-right text-xs text-zinc-500 max-w-xs">
SELECT AN AUTONOMOUS AGENT TO BEGIN DEPLOYMENT. UNITS ARE
PRE-TRAINED ON HISTORICAL TICKS.
</div>
</div>
<div className="max-w-7xl mx-auto px-6 relative z-10">
{/* Grid Container - Removing scroll tracking for stability test */}
<div className="flex flex-row md:grid md:grid-cols-3 gap-4 md:gap-8 overflow-x-auto md:overflow-visible pb-12 md:pb-0 snap-x snap-mandatory -mx-6 px-6 md:mx-0 md:px-0 scrollbar-hide">
{agents.map((agent, i) => {
const Icon = agent.icon
<div className="flex flex-col md:flex-row justify-between items-end mb-10 md:mb-16 gap-6">
<div>
<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" /> MARKET SELECT
</div>
<h2 className="text-4xl md:text-5xl font-black text-white uppercase tracking-tighter">
STRATEGY <span className="text-transparent bg-clip-text bg-gradient-to-r from-nofx-gold to-white">UNITS</span>
</h2>
return (
<motion.div
key={i}
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 ${agent.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="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">
<Icon className={`w-8 h-8 ${agent.color}`} />
</div>
<div className="font-mono text-right text-xs text-zinc-500 max-w-xs">
SELECT AN AUTONOMOUS AGENT TO BEGIN DEPLOYMENT. UNITS ARE PRE-TRAINED ON HISTORICAL TICKS.
<div className="text-right">
<div className="text-[10px] font-mono text-zinc-500 uppercase">
Class
</div>
<div
className={`font-bold font-mono tracking-wider ${agent.color}`}
>
{agent.class}
</div>
</div>
</div>
{/* Name & Desc */}
<h3 className="text-3xl font-bold text-white mb-2 tracking-tight group-hover:text-nofx-accent transition-colors">
{agent.name}
</h3>
<p className="text-zinc-500 text-sm mb-8 leading-relaxed h-10">
{agent.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">
APY
</div>
<div className="text-green-400 font-bold">
{agent.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">
Win %
</div>
<div className="text-white font-bold">
{agent.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">
Risk
</div>
<div className={`${agent.color} font-bold`}>
{agent.risk}
</div>
</div>
</div>
{/* 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-${agent.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`}
>
<span className={agent.color}>[</span> INITIALIZE{' '}
<span className={agent.color}>]</span>
</button>
</div>
{/* Grid Container - Removing scroll tracking for stability test */}
<div className="flex flex-row md:grid md:grid-cols-3 gap-4 md:gap-8 overflow-x-auto md:overflow-visible pb-12 md:pb-0 snap-x snap-mandatory -mx-6 px-6 md:mx-0 md:px-0 scrollbar-hide">
{agents.map((agent, i) => {
const Icon = agent.icon
return (
<motion.div
key={i}
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 ${agent.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="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">
<Icon className={`w-8 h-8 ${agent.color}`} />
</div>
<div className="text-right">
<div className="text-[10px] font-mono text-zinc-500 uppercase">Class</div>
<div className={`font-bold font-mono tracking-wider ${agent.color}`}>{agent.class}</div>
</div>
</div>
{/* Name & Desc */}
<h3 className="text-3xl font-bold text-white mb-2 tracking-tight group-hover:text-nofx-accent transition-colors">{agent.name}</h3>
<p className="text-zinc-500 text-sm mb-8 leading-relaxed h-10">{agent.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">APY</div>
<div className="text-green-400 font-bold">{agent.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">Win %</div>
<div className="text-white font-bold">{agent.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">Risk</div>
<div className={`${agent.color} font-bold`}>{agent.risk}</div>
</div>
</div>
{/* 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-${agent.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`}
>
<span className={agent.color}>[</span> INITIALIZE <span className={agent.color}>]</span>
</button>
</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>
</motion.div>
)
})}
</div>
</div>
</section>
)
{/* 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>
</motion.div>
)
})}
</div>
</div>
</section>
)
}

View File

@@ -20,12 +20,7 @@ import { ModelConfigModal } from './ModelConfigModal'
import { ConfigStatusGrid } from './ConfigStatusGrid'
import { TradersList } from './TradersList'
import { BeginnerGuideCards } from './BeginnerGuideCards'
import {
AlertTriangle,
Bot,
Plus,
MessageCircle,
} from 'lucide-react'
import { AlertTriangle, Bot, Plus, MessageCircle } from 'lucide-react'
import { confirmToast } from '../../lib/notify'
import { toast } from 'sonner'
import {
@@ -55,11 +50,17 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
const [allModels, setAllModels] = useState<AIModel[]>([])
const [allExchanges, setAllExchanges] = useState<Exchange[]>([])
const [supportedModels, setSupportedModels] = useState<AIModel[]>([])
const [visibleTraderAddresses, setVisibleTraderAddresses] = useState<Set<string>>(new Set())
const [visibleExchangeAddresses, setVisibleExchangeAddresses] = useState<Set<string>>(new Set())
const [visibleTraderAddresses, setVisibleTraderAddresses] = useState<
Set<string>
>(new Set())
const [visibleExchangeAddresses, setVisibleExchangeAddresses] = useState<
Set<string>
>(new Set())
const [copiedId, setCopiedId] = useState<string | null>(null)
const [quickSetupLoading, setQuickSetupLoading] = useState(false)
const [beginnerWalletAddress, setBeginnerWalletAddress] = useState<string | null>(() => getBeginnerWalletAddress())
const [beginnerWalletAddress, setBeginnerWalletAddress] = useState<
string | null
>(() => getBeginnerWalletAddress())
const isBeginnerMode = getUserMode() === 'beginner'
const getErrorMessage = (error: unknown, fallback: string) => {
if (error instanceof Error && error.message.trim() !== '') {
@@ -74,54 +75,98 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
) => {
const traderName = params.trader_name || params.traderName || 'this trader'
const modelName = params.model_name || params.modelName || 'selected model'
const exchangeName = params.exchange_name || params.exchangeName || 'selected exchange account'
const reason = localizeTraderReason(params.reason_key, params.reason || fallback)
const exchangeName =
params.exchange_name || params.exchangeName || 'selected exchange account'
const reason = localizeTraderReason(
params.reason_key,
params.reason || fallback
)
const symbol = params.symbol || ''
const zh = language === 'zh'
switch (errorKey) {
case 'trader.create.invalid_request':
return zh ? '提交的信息不完整,或者格式不正确。请检查后重新提交。' : 'The submitted information is incomplete or invalid. Please review it and try again.'
return zh
? '提交的信息不完整,或者格式不正确。请检查后重新提交。'
: 'The submitted information is incomplete or invalid. Please review it and try again.'
case 'trader.create.invalid_btc_eth_leverage':
return zh ? 'BTC/ETH 杠杆倍数需要在 1 到 50 倍之间。' : 'BTC/ETH leverage must be between 1x and 50x.'
return zh
? 'BTC/ETH 杠杆倍数需要在 1 到 50 倍之间。'
: 'BTC/ETH leverage must be between 1x and 50x.'
case 'trader.create.invalid_altcoin_leverage':
return zh ? '山寨币杠杆倍数需要在 1 到 20 倍之间。' : 'Altcoin leverage must be between 1x and 20x.'
return zh
? '山寨币杠杆倍数需要在 1 到 20 倍之间。'
: 'Altcoin leverage must be between 1x and 20x.'
case 'trader.create.invalid_symbol':
return zh ? `交易对 ${symbol} 的格式不正确,目前只支持以 USDT 结尾的合约交易对。` : `Trading pair ${symbol} is invalid. Only perpetual pairs ending with USDT are supported.`
return zh
? `交易对 ${symbol} 的格式不正确,目前只支持以 USDT 结尾的合约交易对。`
: `Trading pair ${symbol} is invalid. Only perpetual pairs ending with USDT are supported.`
case 'trader.create.model_not_found':
return zh ? '还没有找到你选择的 AI 模型。请先到「设置 > 模型配置」添加并启用一个可用模型。' : 'The selected AI model was not found. Please add and enable a valid model in Settings > Model Config.'
return zh
? '还没有找到你选择的 AI 模型。请先到「设置 > 模型配置」添加并启用一个可用模型。'
: 'The selected AI model was not found. Please add and enable a valid model in Settings > Model Config.'
case 'trader.create.model_disabled':
return zh ? `AI 模型「${modelName}」目前还没有启用。请先启用它再创建机器人。` : `AI model "${modelName}" is currently disabled. Please enable it before creating a trader.`
return zh
? `AI 模型「${modelName}」目前还没有启用。请先启用它再创建机器人。`
: `AI model "${modelName}" is currently disabled. Please enable it before creating a trader.`
case 'trader.create.model_missing_credentials':
return zh ? `AI 模型「${modelName}」缺少 API Key 或支付凭证。请先补全模型配置。` : `AI model "${modelName}" is missing API credentials or payment setup. Please complete the model configuration first.`
return zh
? `AI 模型「${modelName}」缺少 API Key 或支付凭证。请先补全模型配置。`
: `AI model "${modelName}" is missing API credentials or payment setup. Please complete the model configuration first.`
case 'trader.create.strategy_required':
return zh ? '你还没有选择交易策略。请先选择一个策略,再继续创建机器人。' : 'No trading strategy is selected yet. Please choose a strategy before creating a trader.'
return zh
? '你还没有选择交易策略。请先选择一个策略,再继续创建机器人。'
: 'No trading strategy is selected yet. Please choose a strategy before creating a trader.'
case 'trader.create.strategy_not_found':
return zh ? '你选择的策略不存在,或者已经被删除了。请重新选择一个可用策略。' : 'The selected strategy no longer exists. Please choose another available strategy.'
return zh
? '你选择的策略不存在,或者已经被删除了。请重新选择一个可用策略。'
: 'The selected strategy no longer exists. Please choose another available strategy.'
case 'trader.create.exchange_not_found':
return zh ? '还没有找到你选择的交易所账户。请先到「设置 > 交易所配置」添加一个可用账户。' : 'The selected exchange account was not found. Please add an exchange account in Settings > Exchange Config.'
return zh
? '还没有找到你选择的交易所账户。请先到「设置 > 交易所配置」添加一个可用账户。'
: 'The selected exchange account was not found. Please add an exchange account in Settings > Exchange Config.'
case 'trader.create.exchange_disabled':
return zh ? `交易所账户「${exchangeName}」目前处于未启用状态。请先启用它。` : `Exchange account "${exchangeName}" is currently disabled. Please enable it first.`
return zh
? `交易所账户「${exchangeName}」目前处于未启用状态。请先启用它。`
: `Exchange account "${exchangeName}" is currently disabled. Please enable it first.`
case 'trader.create.exchange_missing_fields':
return zh ? `交易所账户「${exchangeName}」的配置还不完整。请先补全必填信息。` : `Exchange account "${exchangeName}" is incomplete. Please fill in the required fields first.`
return zh
? `交易所账户「${exchangeName}」的配置还不完整。请先补全必填信息。`
: `Exchange account "${exchangeName}" is incomplete. Please fill in the required fields first.`
case 'trader.create.exchange_unsupported':
return zh ? `交易所账户「${exchangeName}」当前类型暂不支持机器人创建。` : `Exchange account "${exchangeName}" uses a type that is not supported for trader creation.`
return zh
? `交易所账户「${exchangeName}」当前类型暂不支持机器人创建。`
: `Exchange account "${exchangeName}" uses a type that is not supported for trader creation.`
case 'trader.create.exchange_probe_failed':
return zh ? `交易所账户「${exchangeName}」没有通过初始化校验,原因是:${reason}` : `Exchange account "${exchangeName}" failed initialization checks: ${reason}`
return zh
? `交易所账户「${exchangeName}」没有通过初始化校验,原因是:${reason}`
: `Exchange account "${exchangeName}" failed initialization checks: ${reason}`
case 'trader.start.strategy_missing':
return zh ? `机器人「${traderName}」缺少有效的交易策略配置。` : `Trader "${traderName}" does not have a valid strategy configuration.`
return zh
? `机器人「${traderName}」缺少有效的交易策略配置。`
: `Trader "${traderName}" does not have a valid strategy configuration.`
case 'trader.start.model_not_found':
return zh ? `机器人「${traderName}」关联的 AI 模型不存在。请检查模型配置。` : `Trader "${traderName}" references an AI model that no longer exists. Please check the model configuration.`
return zh
? `机器人「${traderName}」关联的 AI 模型不存在。请检查模型配置。`
: `Trader "${traderName}" references an AI model that no longer exists. Please check the model configuration.`
case 'trader.start.model_disabled':
return zh ? `机器人「${traderName}」关联的 AI 模型「${modelName}」目前还没有启用。` : `Trader "${traderName}" uses AI model "${modelName}", which is currently disabled.`
return zh
? `机器人「${traderName}」关联的 AI 模型「${modelName}」目前还没有启用。`
: `Trader "${traderName}" uses AI model "${modelName}", which is currently disabled.`
case 'trader.start.exchange_not_found':
return zh ? `机器人「${traderName}」关联的交易所账户不存在。请检查交易所配置。` : `Trader "${traderName}" references an exchange account that no longer exists. Please check the exchange configuration.`
return zh
? `机器人「${traderName}」关联的交易所账户不存在。请检查交易所配置。`
: `Trader "${traderName}" references an exchange account that no longer exists. Please check the exchange configuration.`
case 'trader.start.exchange_disabled':
return zh ? `机器人「${traderName}」关联的交易所账户「${exchangeName}」目前还没有启用。` : `Trader "${traderName}" uses exchange account "${exchangeName}", which is currently disabled.`
return zh
? `机器人「${traderName}」关联的交易所账户「${exchangeName}」目前还没有启用。`
: `Trader "${traderName}" uses exchange account "${exchangeName}", which is currently disabled.`
case 'trader.start.setup_invalid':
case 'trader.start.load_failed':
return zh ? `机器人「${traderName}」暂时还不能启动,原因是:${reason}` : `Trader "${traderName}" cannot be started yet because ${reason}`
return zh
? `机器人「${traderName}」暂时还不能启动,原因是:${reason}`
: `Trader "${traderName}" cannot be started yet because ${reason}`
default:
return fallback
}
@@ -131,34 +176,69 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
switch (reasonKey) {
case 'trader.reason.strategy_config_invalid':
return zh ? '当前策略配置内容已损坏,系统暂时无法解析' : 'the current strategy configuration is corrupted and cannot be parsed'
return zh
? '当前策略配置内容已损坏,系统暂时无法解析'
: 'the current strategy configuration is corrupted and cannot be parsed'
case 'trader.reason.strategy_missing':
return zh ? '当前机器人缺少有效的交易策略配置' : 'the trader is missing a valid strategy configuration'
return zh
? '当前机器人缺少有效的交易策略配置'
: 'the trader is missing a valid strategy configuration'
case 'trader.reason.private_key_invalid':
return zh ? '私钥格式不正确,系统无法识别' : 'the private key format is invalid and cannot be recognized'
return zh
? '私钥格式不正确,系统无法识别'
: 'the private key format is invalid and cannot be recognized'
case 'trader.reason.hyperliquid_init_failed':
return zh ? 'Hyperliquid 账户初始化失败,请确认私钥、主钱包地址和 Agent Wallet 配置是否正确' : 'Hyperliquid account initialization failed. Please verify the private key, main wallet address, and Agent Wallet configuration'
return zh
? 'Hyperliquid 账户初始化失败,请确认私钥、主钱包地址和 Agent Wallet 配置是否正确'
: 'Hyperliquid account initialization failed. Please verify the private key, main wallet address, and Agent Wallet configuration'
case 'trader.reason.aster_init_failed':
return zh ? 'Aster 账户初始化失败,请确认 Aster User、Signer 和私钥是否正确' : 'Aster account initialization failed. Please verify the Aster User, Signer, and private key'
return zh
? 'Aster 账户初始化失败,请确认 Aster User、Signer 和私钥是否正确'
: 'Aster account initialization failed. Please verify the Aster User, Signer, and private key'
case 'trader.reason.exchange_meta_unavailable':
return zh ? '系统暂时无法从交易所读取账户元信息' : 'the system could not read account metadata from the exchange'
return zh
? '系统暂时无法从交易所读取账户元信息'
: 'the system could not read account metadata from the exchange'
case 'trader.reason.hyperliquid_agent_balance_too_high':
return zh ? 'Hyperliquid Agent Wallet 余额过高,不符合当前安全要求' : 'the Hyperliquid Agent Wallet balance is too high for the current safety requirements'
return zh
? 'Hyperliquid Agent Wallet 余额过高,不符合当前安全要求'
: 'the Hyperliquid Agent Wallet balance is too high for the current safety requirements'
case 'trader.reason.exchange_account_init_failed':
return zh ? '交易所账户初始化失败,请确认钱包地址和 API Key 是否匹配' : 'exchange account initialization failed. Please verify that the wallet address and API key match'
return zh
? '交易所账户初始化失败,请确认钱包地址和 API Key 是否匹配'
: 'exchange account initialization failed. Please verify that the wallet address and API key match'
case 'trader.reason.exchange_unsupported':
return zh ? '当前交易所类型暂不支持机器人初始化' : 'the selected exchange type is not currently supported for trader initialization'
return zh
? '当前交易所类型暂不支持机器人初始化'
: 'the selected exchange type is not currently supported for trader initialization'
case 'trader.reason.exchange_balance_unavailable':
return zh ? '系统暂时无法从交易所读取账户余额' : 'the system could not read the account balance from the exchange'
return zh
? '系统暂时无法从交易所读取账户余额'
: 'the system could not read the account balance from the exchange'
case 'trader.reason.exchange_service_unreachable':
return zh ? '系统暂时无法连接交易所服务' : 'the system could not reach the exchange service right now'
return zh
? '系统暂时无法连接交易所服务'
: 'the system could not reach the exchange service right now'
default:
return fallback || (zh ? '系统返回了一个未知错误' : 'an unknown error was returned by the system')
return (
fallback ||
(zh
? '系统返回了一个未知错误'
: 'an unknown error was returned by the system')
)
}
}
const normalizeActionableDescription = (error: unknown, message: string, title: string) => {
const normalizeActionableDescription = (
error: unknown,
message: string,
title: string
) => {
if (error instanceof ApiError && error.errorKey) {
return formatActionableDescriptionByKey(error.errorKey, error.errorParams, message)
return formatActionableDescriptionByKey(
error.errorKey,
error.errorParams,
message
)
}
const prefixes = [
@@ -247,12 +327,11 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
const navigateInApp = (path: string) => {
navigate(path)
window.dispatchEvent(new PopStateEvent('popstate'))
}
// Toggle wallet address visibility for a trader
const toggleTraderAddressVisibility = (traderId: string) => {
setVisibleTraderAddresses(prev => {
setVisibleTraderAddresses((prev) => {
const next = new Set(prev)
if (next.has(traderId)) {
next.delete(traderId)
@@ -265,7 +344,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
// Toggle wallet address visibility for an exchange
const toggleExchangeAddressVisibility = (exchangeId: string) => {
setVisibleExchangeAddresses(prev => {
setVisibleExchangeAddresses((prev) => {
const next = new Set(prev)
if (next.has(exchangeId)) {
next.delete(exchangeId)
@@ -287,11 +366,13 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
}
}
const { data: traders, mutate: mutateTraders, isLoading: isTradersLoading } = useSWR<TraderInfo[]>(
user && token ? 'traders' : null,
api.getTraders,
{ refreshInterval: 5000 }
)
const {
data: traders,
mutate: mutateTraders,
isLoading: isTradersLoading,
} = useSWR<TraderInfo[]>(user && token ? 'traders' : null, api.getTraders, {
refreshInterval: 5000,
})
const {
data: exchangeAccountStateData,
mutate: mutateExchangeAccountStates,
@@ -323,18 +404,15 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
}
try {
const [
modelConfigs,
exchangeConfigs,
models,
] = await Promise.all([
const [modelConfigs, exchangeConfigs, models] = await Promise.all([
api.getModelConfigs(),
api.getExchangeConfigs(),
api.getSupportedModels(),
])
setAllModels(modelConfigs)
const clawWalletAddress =
modelConfigs.find((model) => model.provider === 'claw402')?.walletAddress || null
modelConfigs.find((model) => model.provider === 'claw402')
?.walletAddress || null
if (clawWalletAddress) {
setBeginnerWalletAddress(clawWalletAddress)
persistBeginnerWalletAddress(clawWalletAddress)
@@ -365,10 +443,15 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
}) || []
const enabledModels = allModels?.filter((m) => m.enabled) || []
const enabledClaw402Model = enabledModels.find((model) => model.provider === 'claw402') || null
const enabledClaw402Balance = parseBalanceUsdc(enabledClaw402Model?.balanceUsdc)
const enabledClaw402Model =
enabledModels.find((model) => model.provider === 'claw402') || null
const enabledClaw402Balance = parseBalanceUsdc(
enabledClaw402Model?.balanceUsdc
)
const claw402BalanceAlert =
enabledClaw402Model && enabledClaw402Balance !== null && enabledClaw402Balance < 1
enabledClaw402Model &&
enabledClaw402Balance !== null &&
enabledClaw402Balance < 1
? {
blocking: enabledClaw402Balance <= 0,
title:
@@ -379,7 +462,10 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
: enabledClaw402Balance <= 0
? 'Claw402 wallet balance is zero'
: 'Claw402 wallet balance is low',
description: getClaw402BalanceMessage(enabledClaw402Balance, enabledClaw402Balance <= 0),
description: getClaw402BalanceMessage(
enabledClaw402Balance,
enabledClaw402Balance <= 0
),
}
: null
const enabledExchanges =
@@ -415,7 +501,8 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
}
const getExchangeUsageInfo = (exchangeId: string) => {
const usingTraders = traders?.filter((tr) => tr.exchange_id === exchangeId) || []
const usingTraders =
traders?.filter((tr) => tr.exchange_id === exchangeId) || []
const runningCount = usingTraders.filter((tr) => tr.is_running).length
const totalCount = usingTraders.length
return { runningCount, totalCount, usingTraders }
@@ -548,17 +635,26 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
} catch (error) {
console.error('Failed to toggle trader:', error)
showActionableError(
running ? t('aiTradersToast.stopFailed', language) : t('aiTradersToast.startFailed', language),
running
? t('aiTradersToast.stopFailed', language)
: t('aiTradersToast.startFailed', language),
error
)
}
}
const handleToggleCompetition = async (traderId: string, currentShowInCompetition: boolean) => {
const handleToggleCompetition = async (
traderId: string,
currentShowInCompetition: boolean
) => {
try {
const newValue = !currentShowInCompetition
await api.toggleCompetition(traderId, newValue)
toast.success(newValue ? t('aiTradersToast.showInCompetition', language) : t('aiTradersToast.hideInCompetition', language))
toast.success(
newValue
? t('aiTradersToast.showInCompetition', language)
: t('aiTradersToast.hideInCompetition', language)
)
await mutateTraders()
} catch (error) {
@@ -695,12 +791,12 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
allModels?.map((m) =>
m.id === modelId
? {
...m,
apiKey,
customApiUrl: customApiUrl || '',
customModelName: customModelName || '',
enabled: true,
}
...m,
apiKey,
customApiUrl: customApiUrl || '',
customModelName: customModelName || '',
enabled: true,
}
: m
) || []
} else {
@@ -816,7 +912,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
}
await api.updateExchangeConfigsEncrypted(request)
toast.success(t('aiTradersToast.exchangeConfigUpdated', language))
toast.success(t('aiTradersToast.exchangeConfigUpdated', language))
} else {
const createRequest = {
exchange_type: exchangeType,
@@ -837,7 +933,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
}
await api.createExchangeEncrypted(createRequest)
toast.success(t('aiTradersToast.exchangeCreated', language))
toast.success(t('aiTradersToast.exchangeCreated', language))
}
const refreshedExchanges = await api.getExchangeConfigs()
@@ -888,10 +984,13 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
}
}
const claw402Configured = configuredModels.some((model) => model.provider === 'claw402')
const claw402Configured = configuredModels.some(
(model) => model.provider === 'claw402'
)
const hasStrategies = (strategies?.length || 0) > 0
const hasCreatedTrader = (traders?.length || 0) > 0
const canCreateTrader = configuredModels.length > 0 && configuredExchanges.length > 0
const canCreateTrader =
configuredModels.length > 0 && configuredExchanges.length > 0
return (
<DeepVoidBackground className="py-8" disableAnimation>
@@ -952,7 +1051,10 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
<button
onClick={() => setShowCreateModal(true)}
disabled={configuredModels.length === 0 || configuredExchanges.length === 0}
disabled={
configuredModels.length === 0 ||
configuredExchanges.length === 0
}
className="group relative px-6 py-2 rounded text-xs font-bold font-mono uppercase tracking-wider transition-all disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap overflow-hidden bg-nofx-gold text-black hover:bg-yellow-400 shadow-[0_0_20px_rgba(240,185,11,0.2)] hover:shadow-[0_0_30px_rgba(240,185,11,0.4)]"
>
<span className="relative z-10 flex items-center gap-2">
@@ -984,15 +1086,21 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
<div
className="mb-6 rounded-xl border px-4 py-4 md:px-5 md:py-4 flex flex-col md:flex-row md:items-start md:justify-between gap-3"
style={{
borderColor: claw402BalanceAlert.blocking ? 'rgba(239, 68, 68, 0.55)' : 'rgba(245, 158, 11, 0.45)',
background: claw402BalanceAlert.blocking ? 'rgba(127, 29, 29, 0.22)' : 'rgba(120, 53, 15, 0.18)',
borderColor: claw402BalanceAlert.blocking
? 'rgba(239, 68, 68, 0.55)'
: 'rgba(245, 158, 11, 0.45)',
background: claw402BalanceAlert.blocking
? 'rgba(127, 29, 29, 0.22)'
: 'rgba(120, 53, 15, 0.18)',
}}
>
<div className="flex items-start gap-3">
<div
className="mt-0.5 rounded-full p-2"
style={{
background: claw402BalanceAlert.blocking ? 'rgba(239, 68, 68, 0.16)' : 'rgba(245, 158, 11, 0.14)',
background: claw402BalanceAlert.blocking
? 'rgba(239, 68, 68, 0.16)'
: 'rgba(245, 158, 11, 0.14)',
color: claw402BalanceAlert.blocking ? '#F87171' : '#FBBF24',
}}
>
@@ -1001,11 +1109,16 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
<div>
<div
className="text-sm font-semibold"
style={{ color: claw402BalanceAlert.blocking ? '#FCA5A5' : '#FDE68A' }}
style={{
color: claw402BalanceAlert.blocking ? '#FCA5A5' : '#FDE68A',
}}
>
{claw402BalanceAlert.title}
</div>
<div className="text-sm mt-1 leading-6" style={{ color: '#D4D4D8' }}>
<div
className="text-sm mt-1 leading-6"
style={{ color: '#D4D4D8' }}
>
{claw402BalanceAlert.description}
</div>
</div>
@@ -1013,10 +1126,14 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
<button
type="button"
onClick={() => enabledClaw402Model && handleModelClick(enabledClaw402Model.id)}
onClick={() =>
enabledClaw402Model && handleModelClick(enabledClaw402Model.id)
}
className="px-4 py-2 rounded text-xs font-mono uppercase tracking-wider border whitespace-nowrap self-start"
style={{
borderColor: claw402BalanceAlert.blocking ? 'rgba(248, 113, 113, 0.45)' : 'rgba(251, 191, 36, 0.35)',
borderColor: claw402BalanceAlert.blocking
? 'rgba(248, 113, 113, 0.45)'
: 'rgba(251, 191, 36, 0.35)',
color: claw402BalanceAlert.blocking ? '#FCA5A5' : '#FDE68A',
background: 'rgba(0, 0, 0, 0.18)',
}}