feat(web): refresh Hyperliquid-focused product UI

- Update landing, chart, settings, and data page copy for stock trading

- Adjust branding and translations around Hyperliquid positioning

- Extend frontend config types for the updated exchange settings
This commit is contained in:
tinklefund
2026-05-25 01:25:23 +08:00
parent f4ee723aa2
commit 3b2e7027db
11 changed files with 156 additions and 83 deletions

View File

@@ -20,13 +20,14 @@ type MarketType = 'hyperliquid' | 'crypto' | 'stocks' | 'forex' | 'metals'
interface SymbolInfo {
symbol: string
display?: string
name: string
category: string
}
// Market type configuration
const MARKET_CONFIG = {
hyperliquid: { exchange: 'hyperliquid', defaultSymbol: 'BTC', icon: '🔷', labelKey: 'hyperliquid' as const, color: 'cyan', hasDropdown: true },
hyperliquid: { exchange: 'hyperliquid-xyz', defaultSymbol: 'BTC', icon: '🔷', labelKey: 'hyperliquid' as const, color: 'cyan', hasDropdown: true },
crypto: { exchange: 'binance', defaultSymbol: 'BTCUSDT', icon: '₿', labelKey: 'crypto' as const, color: 'yellow', hasDropdown: false },
stocks: { exchange: 'alpaca', defaultSymbol: 'AAPL', icon: '📈', labelKey: 'stocks' as const, color: 'green', hasDropdown: false },
forex: { exchange: 'forex', defaultSymbol: 'EUR/USD', icon: '💱', labelKey: 'forex' as const, color: 'blue', hasDropdown: false },
@@ -63,6 +64,7 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
const [showDropdown, setShowDropdown] = useState(false)
const [searchFilter, setSearchFilter] = useState('')
const dropdownRef = useRef<HTMLDivElement>(null)
const chartSymbolDisplay = availableSymbols.find(s => s.symbol === chartSymbol)?.display || chartSymbol
// Auto-switch market type when exchange ID changes
useEffect(() => {
@@ -82,13 +84,13 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
.then(res => res.json())
.then(data => {
if (data.symbols) {
// Sort by category: crypto > stock > forex > commodity > index
const categoryOrder: Record<string, number> = { crypto: 0, stock: 1, forex: 2, commodity: 3, index: 4 }
// Product order for Hyperliquid XYZ board: stocks → commodities → indices → FX → pre-IPO → crypto
const categoryOrder: Record<string, number> = { stock: 0, commodity: 1, index: 2, forex: 3, pre_ipo: 4, crypto: 5 }
const sorted = [...data.symbols].sort((a: SymbolInfo, b: SymbolInfo) => {
const orderA = categoryOrder[a.category] ?? 5
const orderB = categoryOrder[b.category] ?? 5
const orderA = categoryOrder[a.category] ?? 99
const orderB = categoryOrder[b.category] ?? 99
if (orderA !== orderB) return orderA - orderB
return a.symbol.localeCompare(b.symbol)
return (a.display || a.symbol).localeCompare(b.display || b.symbol)
})
setAvailableSymbols(sorted)
}
@@ -116,9 +118,12 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
}
// Filtered symbol list
const filteredSymbols = availableSymbols.filter(s =>
s.symbol.toLowerCase().includes(searchFilter.toLowerCase())
)
const filteredSymbols = availableSymbols.filter(s => {
const q = searchFilter.toLowerCase()
return s.symbol.toLowerCase().includes(q)
|| (s.display || '').toLowerCase().includes(q)
|| s.name.toLowerCase().includes(q)
})
// Auto-switch to kline chart when symbol selected externally
useEffect(() => {
@@ -215,7 +220,7 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
onClick={() => setShowDropdown(!showDropdown)}
className="flex items-center gap-1.5 px-2.5 py-1 bg-black/40 border border-white/10 rounded text-[11px] font-bold text-nofx-text-main hover:border-nofx-gold/30 hover:text-nofx-gold transition-all"
>
<span>{chartSymbol}</span>
<span>{chartSymbolDisplay}</span>
<ChevronDown className={`w-3 h-3 text-nofx-text-muted transition-transform ${showDropdown ? 'rotate-180' : ''}`} />
</button>
{showDropdown && (
@@ -234,10 +239,10 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
</div>
</div>
<div className="overflow-y-auto max-h-60 custom-scrollbar">
{['crypto', 'stock', 'forex', 'commodity', 'index'].map(category => {
{['stock', 'commodity', 'index', 'forex', 'pre_ipo', 'crypto'].map(category => {
const categorySymbols = filteredSymbols.filter(s => s.category === category)
if (categorySymbols.length === 0) return null
const labels: Record<string, string> = { crypto: 'Crypto', stock: 'Stocks', forex: 'Forex', commodity: 'Commodities', index: 'Index' }
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>
@@ -247,7 +252,7 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
onClick={() => { setChartSymbol(s.symbol); setShowDropdown(false); setSearchFilter('') }}
className={`w-full px-3 py-2 text-left text-[11px] font-mono hover:bg-white/5 transition-all flex items-center justify-between ${chartSymbol === s.symbol ? 'bg-nofx-gold/10 text-nofx-gold' : 'text-nofx-text-muted'}`}
>
<span>{s.symbol}</span>
<span>{s.display || s.symbol}</span>
<span className="text-[9px] opacity-40">{s.name}</span>
</button>
))}

View File

@@ -11,6 +11,7 @@ import {
type UserMode,
} from '../../lib/onboarding'
import { getCurrentPageForPath, ROUTES, type Page } from '../../router/paths'
import { HyperliquidWalletConnect } from './HyperliquidWalletConnect'
interface HeaderBarProps {
onLoginClick?: () => void
@@ -208,6 +209,7 @@ export default function HeaderBar({
{/* Right Side - Social Links and User Actions */}
<div className="flex items-center gap-4">
<HyperliquidWalletConnect language={language} isLoggedIn={isLoggedIn} />
{/* Social Links - Always visible */}
<div className="flex items-center gap-1">
{/* GitHub */}

View File

@@ -136,7 +136,7 @@ export default function CommunitySection({ language }: CommunitySectionProps) {
viewport={{ once: true }}
>
<a
href="https://twitter.com/nofx_official"
href="https://x.com/vergex_ai"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-6 py-3 rounded-xl font-medium transition-all hover:scale-105"

View File

@@ -8,8 +8,8 @@ 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.',
class: 'US_STOCKS',
desc: 'Large-cap momentum and breakout trading.',
apy: '142%',
winRate: '68%',
risk: 'HIGH',
@@ -20,8 +20,8 @@ const agents = [
},
{
name: 'BETA-X',
class: 'SWING_OPS',
desc: 'Multi-day trend extraction engine.',
class: 'MACRO_FX',
desc: 'FX trend and macro regime allocation.',
apy: '89%',
winRate: '55%',
risk: 'MED',
@@ -32,8 +32,8 @@ const agents = [
},
{
name: 'GAMMA-RAY',
class: 'ARBITRAGE',
desc: 'Low-risk spatial price equalization.',
class: 'PRE_IPO',
desc: 'Private-market momentum basket engine.',
apy: '24%',
winRate: '99%',
risk: 'LOW',
@@ -70,18 +70,17 @@ export default function AgentGrid() {
<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
<Crosshair className="w-4 h-4" /> ASSET CLASS SELECT
</div>
<h2 className="text-4xl md:text-5xl font-black text-white uppercase tracking-tighter">
STRATEGY{' '}
PROFESSIONAL{' '}
<span className="text-transparent bg-clip-text bg-gradient-to-r from-nofx-gold to-white">
UNITS
AGENTS
</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.
CREATE AGENTS FOR US STOCKS, COMMODITIES, FX AND PRE-IPO MARKETS. DESCRIBE THE STRATEGY IN ONE SENTENCE.
</div>
</div>

View File

@@ -10,29 +10,29 @@ interface LogEntry {
}
const generateLog = (id: number): LogEntry => {
const types = ['EXE', 'ARB', 'LIQ', 'NET', 'SYS']
const pairs = ['BTC-USDT', 'ETH-PERP', 'SOL-USDT', 'BNB-BUSD']
const actions = ['BUY', 'SELL', 'SHORT', 'LONG']
const types = ['EXEC', 'SIGNAL', 'RISK', 'MACRO', 'SYS']
const pairs = ['AAPL-USDC', 'NVDA-USDC', 'GOLD-USDC', 'EURUSD-USDC', 'OPENAI-IPO']
const actions = ['BUY', 'SELL', 'HEDGE', 'ROTATE']
const type = types[Math.floor(Math.random() * types.length)]
let msg = ''
let color = ''
switch (type) {
case 'EXE':
msg = `BOT-${Math.floor(Math.random() * 99)} ${actions[Math.floor(Math.random() * 4)]} ${pairs[Math.floor(Math.random() * 4)]} @ ${Math.floor(Math.random() * 60000)}`
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'
break;
case 'ARB':
msg = `Spread detected: BINANCE <> BYBIT (${(Math.random()).toFixed(3)}%)`
case 'SIGNAL':
msg = `US equities momentum signal confirmed (${(Math.random()).toFixed(3)} z-score)`
color = 'text-nofx-gold'
break;
case 'LIQ':
msg = `Liquidation Alert: ${pairs[Math.floor(Math.random() * 4)]} $${Math.floor(Math.random() * 100)}k REKT`
case 'RISK':
msg = `Risk check passed: ${pairs[Math.floor(Math.random() * pairs.length)]} exposure within limits`
color = 'text-red-500'
break;
case 'NET':
msg = `Block propagation latency < ${Math.floor(Math.random() * 10)}ms`
case 'MACRO':
msg = `Macro feed latency < ${Math.floor(Math.random() * 10)}ms`
color = 'text-zinc-500'
break;
default:
@@ -91,9 +91,9 @@ export default function LiveFeed() {
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 === 'LIQ' ? 'text-red-500 bg-red-500/10 px-1 rounded' :
log.type === 'ARB' ? 'text-nofx-gold bg-nofx-gold/10 px-1 rounded' :
log.type === 'EXE' ? 'text-green-500' : 'text-zinc-500'
<span className={`font-bold w-10 ${log.type === 'RISK' ? 'text-blue-400 bg-blue-500/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}</span>
<span className={`${log.color}`}>{log.msg}</span>
</motion.div>
@@ -105,8 +105,8 @@ export default function LiveFeed() {
{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 === 'LIQ' ? 'text-red-500' :
log.type === 'ARB' ? 'text-nofx-gold' :
<span className={`font-bold w-8 shrink-0 ${log.type === 'RISK' ? 'text-blue-400' :
log.type === 'SIGNAL' ? 'text-nofx-gold' :
'text-zinc-500'
}`}>{log.type}</span>
<span className={`${log.color} truncate`}>{log.msg}</span>

View File

@@ -4,23 +4,34 @@ import { useState, useEffect } from 'react'
import { useGitHubStats } from '../../../hooks/useGitHubStats'
import AgentTerminal from '../brand/AgentTerminal'
const tickerLabels: Record<string, string> = {
AAPL: 'AAPL',
MSFT: 'MSFT',
NVDA: 'NVDA',
TSLA: 'TSLA',
GOLD: 'GOLD',
EURUSD: 'EUR/USD',
SPX: 'S&P 500',
PREIPO: 'PRE-IPO INDEX',
}
export default function TerminalHero() {
// Real-time price state
const [prices, setPrices] = useState<Record<string, string>>({
BTC: '...',
ETH: '...',
SOL: '...',
BNB: '...',
XRP: '...',
DOGE: '...',
ADA: '...',
AVAX: '...'
AAPL: '...',
MSFT: '...',
NVDA: '...',
TSLA: '...',
GOLD: '...',
EURUSD: '...',
SPX: '...',
PREIPO: '...'
})
useEffect(() => {
const fetchPrices = async () => {
const symbols = ['BTC', 'ETH', 'SOL', 'BNB', 'XRP', 'DOGE', 'ADA', 'AVAX']
const symbols = ['AAPL', 'MSFT', 'NVDA', 'TSLA', 'GOLD', 'EURUSD', 'SPX']
// We use Promise.all to fetch them in parallel for now, or sequentially if rate limited.
// Parallel is better for UI responsiveness.
@@ -28,7 +39,7 @@ export default function TerminalHero() {
const results = await Promise.all(symbols.map(async (sym) => {
try {
// Use native fetch to bypass global error handlers (toasts) in httpClient
const response = await fetch(`/api/klines?symbol=${sym}USDT&interval=1m&limit=1`)
const response = await fetch(`/api/klines?symbol=xyz:${sym}&interval=1m&limit=1`)
if (!response.ok) return null
const res = await response.json()
@@ -149,9 +160,9 @@ export default function TerminalHero() {
{/* Bottom: Network Log */}
<div className="font-mono text-[10px] text-zinc-600 space-y-1 opacity-70">
<div>&gt; CONNECTING TO MAINNET... OK</div>
<div>&gt; SYNCING NODES (424/424)... OK</div>
<div>&gt; LOADING ASSETS... DONE</div>
<div>&gt; CONNECTING TO MARKET DATA... OK</div>
<div>&gt; SYNCING VENUES (424/424)... OK</div>
<div>&gt; LOADING MULTI-ASSET UNIVERSE... DONE</div>
<div className="animate-pulse">&gt; AWAITING USER INPUT_</div>
</div>
</div>
@@ -169,7 +180,7 @@ export default function TerminalHero() {
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-nofx-gold opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-nofx-gold"></span>
</span>
<span className="text-xs font-mono text-nofx-gold tracking-widest">NOFX OPEN-SOURCE AGENTIC OS</span>
<span className="text-xs font-mono text-nofx-gold tracking-widest">NOFX PROFESSIONAL MULTI-ASSET AGENT OS</span>
</motion.div>
{/* Main Title - Massive & Impactful */}
@@ -181,8 +192,8 @@ export default function TerminalHero() {
</h1>
<p className="max-w-xl text-zinc-200 md:text-zinc-400 text-lg mb-6 font-light leading-relaxed drop-shadow-md">
The World's First Open-Source Agentic Trading OS.
Deploy autonomous high-frequency trading agents powered by advanced LLMs.
Professional AI trading agents for US stocks, commodities, FX and Pre-IPO synthetic markets.
Build institutional-grade strategies by chatting in plain English.
</p>
</div>
@@ -197,7 +208,7 @@ export default function TerminalHero() {
Live Data Feeds Active
</div>
<div className="flex flex-wrap gap-4 font-mono">
{['CRYPTO', 'US STOCKS', 'FOREX', 'METALS'].map((market) => (
{['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">
@@ -213,7 +224,7 @@ export default function TerminalHero() {
<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' })}>
<span className="text-nofx-success mr-2"></span>
<span className="text-nofx-accent mr-2">~</span>
<span className="text-zinc-500">deploy agent --strategy=hft</span>
<span className="text-zinc-500">create US stock agent --idea="breakouts"</span>
<span className="w-2 h-4 bg-nofx-gold ml-1 animate-pulse"></span>
</div>
@@ -225,7 +236,7 @@ export default function TerminalHero() {
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">
INITIALIZE PROTOCOL <ArrowRight className="w-4 h-4" />
CREATE STOCK AGENT <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>
</button>
@@ -263,13 +274,13 @@ export default function TerminalHero() {
<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>
<span className="flex items-center gap-2 text-nofx-gold"><Zap className="w-3 h-3" /> FLASH LOANS ENABLED</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>
{/* Dynamic Coins */}
{Object.entries(prices).map(([symbol, price]) => (
<span key={symbol} className="flex items-center gap-2">
{symbol.toUpperCase()}/USDT <span className="text-nofx-success">${price}</span>
{tickerLabels[symbol] || symbol.toUpperCase()} <span className="text-nofx-success">${price}</span>
</span>
))}
@@ -278,7 +289,7 @@ export default function TerminalHero() {
{/* Duplicate sequence for seamless loop effect (basic set) */}
{Object.entries(prices).map(([symbol, price]) => (
<span key={`${symbol} -dup`} className="flex items-center gap-2 md:hidden">
{symbol.toUpperCase()}/USDT <span className="text-nofx-success">${price}</span>
{tickerLabels[symbol] || symbol.toUpperCase()} <span className="text-nofx-success">${price}</span>
</span>
))}
</div>

View File

@@ -7,7 +7,7 @@ const _e = (s: string) => btoa(s)
// Encoded official links - tampering will break functionality
const ENCODED_LINKS = {
twitter: 'aHR0cHM6Ly94LmNvbS9ub2Z4X29mZmljaWFs', // https://x.com/nofx_official
twitter: 'aHR0cHM6Ly94LmNvbS92ZXJnZXhfYWk=',
telegram: 'aHR0cHM6Ly90Lm1lL25vZnhfZGV2X2NvbW11bml0eQ==', // https://t.me/nofx_dev_community
github: 'aHR0cHM6Ly9naXRodWIuY29tL3RpbmtsZS1jb21tdW5pdHkvbm9meA==', // https://github.com/NoFxAiOS/nofx
}
@@ -39,7 +39,7 @@ function getVerifiedLink(key: keyof typeof ENCODED_LINKS): string {
} catch {
// Fallback to hardcoded values if decoding fails
const fallbacks: Record<string, string> = {
twitter: 'https://x.com/nofx_official',
twitter: 'https://x.com/vergex_ai',
telegram: 'https://t.me/nofx_dev_community',
github: 'https://github.com/NoFxAiOS/nofx',
}

View File

@@ -341,10 +341,10 @@ export const translations = {
clearSelection: 'Clear All',
confirmSelection: 'Confirm',
tradingSymbolsDescription:
'Empty = use default symbols. Must end with USDT (e.g., BTCUSDT, ETHUSDT)',
'Empty = use default symbols. Use USDT perps (e.g., BTCUSDT, ETHUSDT) or Hyperliquid XYZ USDC markets (e.g., TSLA-USDC)',
btcEthLeverageValidation: 'BTC/ETH leverage must be between 1-50x',
altcoinLeverageValidation: 'Altcoin leverage must be between 1-20x',
invalidSymbolFormat: 'Invalid symbol format: {symbol}, must end with USDT',
invalidSymbolFormat: 'Invalid symbol format: {symbol}, use USDT perps or SYMBOL-USDC',
// System Prompt Templates
systemPromptTemplate: 'System Prompt Template',
@@ -1687,10 +1687,10 @@ export const translations = {
clearSelection: '清空选择',
confirmSelection: '确认选择',
tradingSymbolsDescription:
'留空 = 使用默认币种。必须以USDT结尾BTCUSDT, ETHUSDT',
'留空 = 使用默认币种。支持 USDT 合约BTCUSDT, ETHUSDT或 Hyperliquid XYZ USDC 标的TSLA-USDC',
btcEthLeverageValidation: 'BTC/ETH杠杆必须在1-50倍之间',
altcoinLeverageValidation: '山寨币杠杆必须在1-20倍之间',
invalidSymbolFormat: '无效的币种格式:{symbol}必须以USDT结尾',
invalidSymbolFormat: '无效的币种格式:{symbol}请使用 USDT 合约或 SYMBOL-USDC',
// System Prompt Templates
systemPromptTemplate: '系统提示词模板',
@@ -2965,10 +2965,10 @@ export const translations = {
selectedSymbolsCount: '{count} simbol dipilih',
clearSelection: 'Hapus Semua',
confirmSelection: 'Konfirmasi',
tradingSymbolsDescription: 'Kosong = gunakan simbol default. Harus berakhiran USDT (misal BTCUSDT, ETHUSDT)',
tradingSymbolsDescription: 'Kosong = gunakan simbol default. Gunakan perp USDT (misal BTCUSDT, ETHUSDT) atau market Hyperliquid XYZ USDC (misal TSLA-USDC)',
btcEthLeverageValidation: 'Leverage BTC/ETH harus antara 1-50x',
altcoinLeverageValidation: 'Leverage Altcoin harus antara 1-20x',
invalidSymbolFormat: 'Format simbol tidak valid: {symbol}, harus berakhiran USDT',
invalidSymbolFormat: 'Format simbol tidak valid: {symbol}, gunakan perp USDT atau SYMBOL-USDC',
systemPromptTemplate: 'Template Prompt Sistem',
promptTemplateDefault: 'Default Stabil',
promptTemplateAdaptive: 'Strategi Konservatif',

View File

@@ -7,10 +7,11 @@ export function DataPage() {
return (
<div className="w-full h-[calc(100vh-64px)]">
<iframe
src="https://nofxos.ai/dashboard"
src="https://vergex.trade/trending"
title={t('dataCenter', language)}
className="w-full h-full border-0"
allow="fullscreen"
referrerPolicy="strict-origin-when-cross-origin"
/>
</div>
)

View File

@@ -7,7 +7,7 @@ import { api } from '../lib/api'
import { ExchangeConfigModal } from '../components/trader/ExchangeConfigModal'
import { TelegramConfigModal } from '../components/trader/TelegramConfigModal'
import { ModelConfigModal } from '../components/trader/ModelConfigModal'
import type { Exchange, AIModel } from '../types'
import type { Exchange, AIModel, ExchangeAccountState } from '../types'
type Tab = 'account' | 'models' | 'exchanges' | 'telegram'
@@ -43,6 +43,8 @@ export function SettingsPage() {
// Exchanges state
const [exchanges, setExchanges] = useState<Exchange[]>([])
const [exchangeStates, setExchangeStates] = useState<Record<string, ExchangeAccountState>>({})
const [exchangeStatesLoading, setExchangeStatesLoading] = useState(false)
const [showExchangeModal, setShowExchangeModal] = useState(false)
const [editingExchange, setEditingExchange] = useState<string | null>(null)
@@ -59,8 +61,24 @@ export function SettingsPage() {
}
const refreshExchangeConfigs = async () => {
const refreshed = await api.getExchangeConfigs()
const [refreshed, accountStateResponse] = await Promise.all([
api.getExchangeConfigs(),
api.getExchangeAccountState().catch(() => ({ states: {} })),
])
setExchanges(refreshed)
setExchangeStates(accountStateResponse.states || {})
}
const refreshExchangeStates = async () => {
setExchangeStatesLoading(true)
try {
const response = await api.getExchangeAccountState()
setExchangeStates(response.states || {})
} catch {
toast.error('Failed to load exchange balances')
} finally {
setExchangeStatesLoading(false)
}
}
// Fetch data when tabs are visited
@@ -205,6 +223,10 @@ export function SettingsPage() {
lighterApiKeyIndex?: number
) => {
try {
if (exchangeType === 'hyperliquid') {
toast.error(language === 'zh' ? 'Hyperliquid 必须通过钱包授权连接,不能手动填写密钥。' : 'Hyperliquid must be connected through wallet authorization, not manual keys.')
return
}
if (exchangeId) {
const request = {
exchanges: {
@@ -406,13 +428,22 @@ export function SettingsPage() {
<p className="text-sm text-zinc-400">
{exchanges.length} account{exchanges.length !== 1 ? 's' : ''} connected
</p>
<button
onClick={() => { setEditingExchange(null); setShowExchangeModal(true) }}
className="flex items-center gap-1.5 text-xs font-medium bg-nofx-gold/10 hover:bg-nofx-gold/20 text-nofx-gold px-3 py-1.5 rounded-lg transition-colors"
>
<Plus size={14} />
Add Exchange
</button>
<div className="flex items-center gap-2">
<button
onClick={refreshExchangeStates}
disabled={exchangeStatesLoading}
className="text-xs font-medium bg-zinc-800 hover:bg-zinc-700 disabled:opacity-60 text-zinc-300 px-3 py-1.5 rounded-lg transition-colors"
>
{exchangeStatesLoading ? 'Refreshing…' : 'Refresh Balances'}
</button>
<button
onClick={() => { setEditingExchange(null); setShowExchangeModal(true) }}
className="flex items-center gap-1.5 text-xs font-medium bg-nofx-gold/10 hover:bg-nofx-gold/20 text-nofx-gold px-3 py-1.5 rounded-lg transition-colors"
>
<Plus size={14} />
Add Exchange
</button>
</div>
</div>
{exchanges.length === 0 ? (
@@ -421,7 +452,9 @@ export function SettingsPage() {
</div>
) : (
<div className="space-y-2">
{exchanges.map((exchange) => (
{exchanges.map((exchange) => {
const accountState = exchangeStates[exchange.id]
return (
<button
key={exchange.id}
onClick={() => { setEditingExchange(exchange.id); setShowExchangeModal(true) }}
@@ -442,11 +475,30 @@ export function SettingsPage() {
{exchange.has_aster_private_key ? configBadge('Aster Key', true) : null}
{exchange.has_lighter_private_key || exchange.has_lighter_api_key_private_key ? configBadge('Lighter Key', true) : null}
</div>
{accountState && (
<div className="flex flex-wrap items-center gap-2 mt-2 text-xs">
{accountState.status === 'ok' ? (
<>
<span className="rounded-full bg-emerald-500/10 px-2 py-0.5 font-mono text-emerald-300">
Balance {accountState.display_balance || `${accountState.total_equity?.toFixed(2) ?? '--'} ${accountState.asset || ''}`}
</span>
{typeof accountState.available_balance === 'number' && (
<span className="text-zinc-500">Available {accountState.available_balance.toFixed(2)} {accountState.asset || ''}</span>
)}
</>
) : (
<span className="rounded-full bg-amber-500/10 px-2 py-0.5 text-amber-300">
Balance unavailable: {accountState.error_message || accountState.status}
</span>
)}
</div>
)}
</div>
</div>
<ChevronRight size={14} className="text-zinc-600 group-hover:text-zinc-400 transition-colors" />
</button>
))}
)
})}
</div>
)}
</div>

View File

@@ -34,6 +34,7 @@ export interface Exchange {
testnet?: boolean
// Hyperliquid specific
hyperliquidWalletAddr?: string
hyperliquidBuilderApproved?: boolean
has_hyperliquid_secret?: boolean
// Aster specific
asterUser?: string
@@ -82,6 +83,7 @@ export interface CreateExchangeRequest {
passphrase?: string
testnet?: boolean
hyperliquid_wallet_addr?: string
hyperliquid_builder_approved?: boolean
aster_user?: string
aster_signer?: string
aster_private_key?: string
@@ -131,6 +133,7 @@ export interface UpdateExchangeConfigRequest {
testnet?: boolean
// Hyperliquid 特定字段
hyperliquid_wallet_addr?: string
hyperliquid_builder_approved?: boolean
// Aster 特定字段
aster_user?: string
aster_signer?: string