mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-06 04:20:59 +08:00
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:
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>> CONNECTING TO MAINNET... OK</div>
|
||||
<div>> SYNCING NODES (424/424)... OK</div>
|
||||
<div>> LOADING ASSETS... DONE</div>
|
||||
<div>> CONNECTING TO MARKET DATA... OK</div>
|
||||
<div>> SYNCING VENUES (424/424)... OK</div>
|
||||
<div>> LOADING MULTI-ASSET UNIVERSE... DONE</div>
|
||||
<div className="animate-pulse">> 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>
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user