mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-06-06 05:51:19 +08:00
feat(agent): surface Hyperliquid stock trading context
- Add stock symbol panel and agent chat page wiring - Update onboarding and tool visibility for focused trader flows - Tighten related tests around configuration and trader scope
This commit is contained in:
@@ -286,7 +286,7 @@ func TestLoadExchangeOptionsHidesInvisibleExchangeRows(t *testing.T) {
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("seed legacy hidden exchange: %v", err)
|
||||
}
|
||||
if _, err := st.Exchange().Create("default", "okx", "我的主力OKX账户", true, "api-test", "secret-test", "pass-test", false, "", false, "", "", "", "", "", "", 0); err != nil {
|
||||
if _, err := st.Exchange().Create("default", "okx", "我的主力OKX账户", true, "api-test", "secret-test", "pass-test", false, "", false, false, "", "", "", "", "", "", 0); err != nil {
|
||||
t.Fatalf("create visible exchange: %v", err)
|
||||
}
|
||||
|
||||
@@ -307,7 +307,7 @@ func TestDescribeExchangeIncludesTypeSpecificVisibleFields(t *testing.T) {
|
||||
}
|
||||
a := New(nil, st, DefaultConfig(), slog.Default())
|
||||
|
||||
hyperID, err := st.Exchange().Create("default", "hyperliquid", "Dex Pro", true, "hyper-api-key", "", "", true, "0xabc", true, "", "", "", "", "", "", 0)
|
||||
hyperID, err := st.Exchange().Create("default", "hyperliquid", "Dex Pro", true, "hyper-api-key", "", "", true, "0xabc", true, false, "", "", "", "", "", "", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("seed hyperliquid exchange: %v", err)
|
||||
}
|
||||
@@ -321,7 +321,7 @@ func TestDescribeExchangeIncludesTypeSpecificVisibleFields(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
lighterID, err := st.Exchange().Create("default", "lighter", "Lighter Main", false, "", "", "", false, "", true, "", "", "", "wallet-1", "", "lighter-secret", 7)
|
||||
lighterID, err := st.Exchange().Create("default", "lighter", "Lighter Main", false, "", "", "", false, "", true, false, "", "", "", "wallet-1", "", "lighter-secret", 7)
|
||||
if err != nil {
|
||||
t.Fatalf("seed lighter exchange: %v", err)
|
||||
}
|
||||
@@ -463,7 +463,7 @@ func TestToolUpdateTraderRejectsRenameOutsideManualPanel(t *testing.T) {
|
||||
if err := st.AIModel().UpdateWithName("default", "default_deepseek", "DeepSeek", true, "sk-test-12345", "", "deepseek-chat"); err != nil {
|
||||
t.Fatalf("seed model: %v", err)
|
||||
}
|
||||
exchangeID, err := st.Exchange().Create("default", "binance", "Main", true, "api-test", "secret-test", "", false, "", false, "", "", "", "", "", "", 0)
|
||||
exchangeID, err := st.Exchange().Create("default", "binance", "Main", true, "api-test", "secret-test", "", false, "", false, false, "", "", "", "", "", "", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("seed exchange: %v", err)
|
||||
}
|
||||
@@ -515,7 +515,7 @@ func TestToolCreateTraderResponseHidesLegacyTraderTuningFields(t *testing.T) {
|
||||
if err := st.AIModel().UpdateWithName("default", "default_deepseek", "DeepSeek", true, "sk-test-12345", "", "deepseek-chat"); err != nil {
|
||||
t.Fatalf("seed model: %v", err)
|
||||
}
|
||||
exchangeID, err := st.Exchange().Create("default", "binance", "Main", true, "api-test", "secret-test", "", false, "", false, "", "", "", "", "", "", 0)
|
||||
exchangeID, err := st.Exchange().Create("default", "binance", "Main", true, "api-test", "secret-test", "", false, "", false, false, "", "", "", "", "", "", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("seed exchange: %v", err)
|
||||
}
|
||||
@@ -566,7 +566,7 @@ func TestToolCreateTraderAutoReadsInitialBalanceFromExchange(t *testing.T) {
|
||||
if err := st.AIModel().UpdateWithName("default", "default_deepseek", "DeepSeek", true, "sk-test-12345", "", "deepseek-chat"); err != nil {
|
||||
t.Fatalf("seed model: %v", err)
|
||||
}
|
||||
exchangeID, err := st.Exchange().Create("default", "binance", "Main", true, "api-test", "secret-test", "", false, "", false, "", "", "", "", "", "", 0)
|
||||
exchangeID, err := st.Exchange().Create("default", "binance", "Main", true, "api-test", "secret-test", "", false, "", false, false, "", "", "", "", "", "", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("seed exchange: %v", err)
|
||||
}
|
||||
|
||||
@@ -455,7 +455,7 @@ func (a *Agent) saveSetupExchange(storeUserID string, state *SetupState) (string
|
||||
storeUserID, ex.ID, true,
|
||||
apiKey, apiSecret, passphrase,
|
||||
false,
|
||||
hlWallet, hlUnified,
|
||||
hlWallet, hlUnified, false,
|
||||
"", "", "",
|
||||
"", "", "", 0,
|
||||
); err != nil {
|
||||
@@ -472,7 +472,7 @@ func (a *Agent) saveSetupExchange(storeUserID string, state *SetupState) (string
|
||||
true,
|
||||
apiKey, apiSecret, passphrase,
|
||||
false,
|
||||
hlWallet, hlUnified,
|
||||
hlWallet, hlUnified, false,
|
||||
"", "", "",
|
||||
"", "", "", 0,
|
||||
)
|
||||
|
||||
@@ -1516,6 +1516,7 @@ func (a *Agent) toolManageExchangeConfig(storeUserID, argsJSON string) string {
|
||||
testnet,
|
||||
strings.TrimSpace(args.HyperliquidWalletAddr),
|
||||
unified,
|
||||
false,
|
||||
strings.TrimSpace(args.AsterUser),
|
||||
strings.TrimSpace(args.AsterSigner),
|
||||
strings.TrimSpace(args.AsterPrivateKey),
|
||||
@@ -1647,6 +1648,7 @@ func (a *Agent) toolManageExchangeConfig(storeUserID, argsJSON string) string {
|
||||
testnet,
|
||||
hyperWallet,
|
||||
unified,
|
||||
existing.HyperliquidBuilderApproved,
|
||||
asterUser,
|
||||
asterSigner,
|
||||
strings.TrimSpace(args.AsterPrivateKey),
|
||||
@@ -2518,9 +2520,13 @@ func (a *Agent) toolStartTrader(storeUserID, traderID string) string {
|
||||
if a.traderManager == nil {
|
||||
return `{"error":"trader manager unavailable"}`
|
||||
}
|
||||
if _, err := a.store.Trader().GetFullConfig(storeUserID, traderID); err != nil {
|
||||
fullCfg, err := a.store.Trader().GetFullConfig(storeUserID, traderID)
|
||||
if err != nil {
|
||||
return fmt.Sprintf(`{"error":"trader not found or inaccessible: %s"}`, err)
|
||||
}
|
||||
if fullCfg != nil && fullCfg.Exchange != nil && fullCfg.Exchange.ExchangeType == "hyperliquid" && !fullCfg.Exchange.HyperliquidBuilderApproved {
|
||||
return `{"error":"Hyperliquid trading authorization is incomplete; reconnect Hyperliquid wallet and complete trading authorization before starting this trader"}`
|
||||
}
|
||||
if existing, err := a.traderManager.GetTrader(traderID); err == nil {
|
||||
if running, ok := existing.GetStatus()["is_running"].(bool); ok && running {
|
||||
return `{"error":"trader is already running"}`
|
||||
|
||||
@@ -372,7 +372,7 @@ func TestHydrateCreateTraderSlotReferencesNormalizesExchangeIDFromVisibleName(t
|
||||
}
|
||||
a := New(nil, st, DefaultConfig(), slog.Default())
|
||||
|
||||
exchangeID, err := st.Exchange().Create("default", "okx", "小偶", true, "api-test", "secret-test", "pass", false, "", false, "", "", "", "", "", "", 0)
|
||||
exchangeID, err := st.Exchange().Create("default", "okx", "小偶", true, "api-test", "secret-test", "pass", false, "", false, false, "", "", "", "", "", "", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("seed exchange: %v", err)
|
||||
}
|
||||
@@ -737,7 +737,7 @@ func TestBuildTraderCreateMissingPromptListsAllMissingSlots(t *testing.T) {
|
||||
if err := st.AIModel().UpdateWithName("default", "default_deepseek", "DeepSeek AI", true, "sk-test-12345", "", "deepseek-chat"); err != nil {
|
||||
t.Fatalf("seed model: %v", err)
|
||||
}
|
||||
exchangeID, err := st.Exchange().Create("default", "okx", "OKX 主账户", true, "api-test", "secret-test", "pass", false, "", false, "", "", "", "", "", "", 0)
|
||||
exchangeID, err := st.Exchange().Create("default", "okx", "OKX 主账户", true, "api-test", "secret-test", "pass", false, "", false, false, "", "", "", "", "", "", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("seed exchange: %v", err)
|
||||
}
|
||||
|
||||
259
web/src/components/agent/HyperliquidSymbolsPanel.tsx
Normal file
259
web/src/components/agent/HyperliquidSymbolsPanel.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Search, Zap, TrendingDown, TrendingUp } from 'lucide-react'
|
||||
import { api } from '../../lib/api'
|
||||
import type { MarketSymbol } from '../../lib/api/data'
|
||||
|
||||
interface HyperliquidSymbolsPanelProps {
|
||||
language: string
|
||||
disabled?: boolean
|
||||
onTradeSymbol: (symbol: MarketSymbol) => void
|
||||
}
|
||||
|
||||
function formatVolume(value?: number) {
|
||||
const n = Number(value || 0)
|
||||
if (n >= 1e9) return `$${(n / 1e9).toFixed(1)}B`
|
||||
if (n >= 1e6) return `$${(n / 1e6).toFixed(1)}M`
|
||||
if (n >= 1e3) return `$${(n / 1e3).toFixed(0)}K`
|
||||
return n > 0 ? `$${n.toFixed(0)}` : '—'
|
||||
}
|
||||
|
||||
function formatPrice(value?: number) {
|
||||
const n = Number(value || 0)
|
||||
if (!n) return '—'
|
||||
if (n >= 1000) return `$${n.toLocaleString('en-US', { maximumFractionDigits: 2 })}`
|
||||
if (n >= 1) return `$${n.toFixed(2)}`
|
||||
return `$${n.toFixed(5)}`
|
||||
}
|
||||
|
||||
function formatChange(value?: number) {
|
||||
const n = Number(value || 0)
|
||||
if (!Number.isFinite(n) || n === 0) return '—'
|
||||
return `${n > 0 ? '+' : ''}${n.toFixed(2)}%`
|
||||
}
|
||||
|
||||
const CATEGORY_LABEL: Record<string, { zh: string; en: string }> = {
|
||||
stock: { zh: '股票', en: 'Stocks' },
|
||||
commodity: { zh: '大宗', en: 'Commodities' },
|
||||
index: { zh: '指数', en: 'Indices' },
|
||||
forex: { zh: '外汇', en: 'FX' },
|
||||
pre_ipo: { zh: 'Pre-IPO', en: 'Pre-IPO' },
|
||||
crypto: { zh: '加密', en: 'Crypto' },
|
||||
}
|
||||
|
||||
const CATEGORY_ORDER = ['stock', 'commodity', 'index', 'forex', 'pre_ipo', 'crypto']
|
||||
|
||||
export function HyperliquidSymbolsPanel({
|
||||
language,
|
||||
disabled,
|
||||
onTradeSymbol,
|
||||
}: HyperliquidSymbolsPanelProps) {
|
||||
const [symbols, setSymbols] = useState<MarketSymbol[]>([])
|
||||
const [query, setQuery] = useState('')
|
||||
const [category, setCategory] = useState('stock')
|
||||
const [ranking, setRanking] = useState<'gainers' | 'losers' | 'volume'>('gainers')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
setLoading(true)
|
||||
api
|
||||
.getSymbols('hyperliquid-xyz')
|
||||
.then((res) => {
|
||||
if (cancelled) return
|
||||
setSymbols(res.symbols || [])
|
||||
setError('')
|
||||
})
|
||||
.catch((err) => {
|
||||
if (cancelled) return
|
||||
setError(err?.message || 'Failed to load symbols')
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false)
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
const categories = useMemo(() => {
|
||||
const unique = new Set(symbols.map((s) => s.category).filter(Boolean))
|
||||
return CATEGORY_ORDER.filter((c) => unique.has(c))
|
||||
}, [symbols])
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = query.trim().toLowerCase()
|
||||
return symbols
|
||||
.filter((s) => (category === 'all' ? true : s.category === category))
|
||||
.filter((s) => {
|
||||
if (!q) return true
|
||||
return [s.symbol, s.display, s.name, s.category]
|
||||
.filter(Boolean)
|
||||
.some((v) => String(v).toLowerCase().includes(q))
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (ranking === 'gainers') return Number(b.change_24h_pct || 0) - Number(a.change_24h_pct || 0)
|
||||
if (ranking === 'losers') return Number(a.change_24h_pct || 0) - Number(b.change_24h_pct || 0)
|
||||
return Number(b.volume_24h || 0) - Number(a.volume_24h || 0)
|
||||
})
|
||||
.slice(0, 80)
|
||||
}, [category, query, ranking, symbols])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ padding: 12, color: '#848E9C', fontSize: 12 }}>
|
||||
{language === 'zh' ? '正在加载 Hyperliquid 全市场标的…' : 'Loading Hyperliquid markets…'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div style={{ padding: 12, color: '#F6465D', fontSize: 12 }}>
|
||||
{language === 'zh' ? '标的列表加载失败:' : 'Failed to load symbols: '}{error}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
padding: '8px 10px',
|
||||
borderRadius: 10,
|
||||
border: '1px solid rgba(255,255,255,0.06)',
|
||||
background: 'rgba(255,255,255,0.025)',
|
||||
}}
|
||||
>
|
||||
<Search size={13} color="#6c6c82" />
|
||||
<input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder={language === 'zh' ? '搜索 SAMSUNG / TESLA / GOLD…' : 'Search SAMSUNG / TESLA / GOLD…'}
|
||||
style={{
|
||||
width: '100%',
|
||||
border: 0,
|
||||
outline: 'none',
|
||||
background: 'transparent',
|
||||
color: '#EAECEF',
|
||||
fontSize: 12,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 6 }}>
|
||||
{([
|
||||
{ key: 'gainers', icon: TrendingUp, zh: '涨幅榜', en: 'Gainers' },
|
||||
{ key: 'losers', icon: TrendingDown, zh: '跌幅榜', en: 'Losers' },
|
||||
{ key: 'volume', icon: Zap, zh: '成交额', en: 'Volume' },
|
||||
] as const).map((item) => {
|
||||
const Icon = item.icon
|
||||
const active = ranking === item.key
|
||||
return (
|
||||
<button
|
||||
key={item.key}
|
||||
onClick={() => setRanking(item.key)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 5,
|
||||
padding: '7px 6px',
|
||||
borderRadius: 10,
|
||||
border: active ? '1px solid rgba(240,185,11,0.55)' : '1px solid rgba(255,255,255,0.06)',
|
||||
background: active ? 'rgba(240,185,11,0.12)' : 'rgba(255,255,255,0.025)',
|
||||
color: active ? '#F0B90B' : '#848E9C',
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<Icon size={12} />
|
||||
{language === 'zh' ? item.zh : item.en}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="hide-scrollbar" style={{ display: 'flex', gap: 6, overflowX: 'auto' }}>
|
||||
{categories.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setCategory(c)}
|
||||
style={{
|
||||
padding: '5px 8px',
|
||||
borderRadius: 999,
|
||||
border: c === category ? '1px solid rgba(240,185,11,0.55)' : '1px solid rgba(255,255,255,0.06)',
|
||||
background: c === category ? 'rgba(240,185,11,0.12)' : 'rgba(255,255,255,0.025)',
|
||||
color: c === category ? '#F0B90B' : '#848E9C',
|
||||
fontSize: 10.5,
|
||||
whiteSpace: 'nowrap',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{c === 'all'
|
||||
? language === 'zh'
|
||||
? '全部'
|
||||
: 'All'
|
||||
: CATEGORY_LABEL[c]?.[language === 'zh' ? 'zh' : 'en'] || c}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, maxHeight: 340, overflowY: 'auto', paddingRight: 2 }}>
|
||||
{filtered.map((s) => {
|
||||
const display = s.display || s.symbol
|
||||
return (
|
||||
<button
|
||||
key={`${s.exchange || 'hyper'}-${s.symbol}`}
|
||||
disabled={disabled}
|
||||
onClick={() => onTradeSymbol(s)}
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr auto',
|
||||
gap: 8,
|
||||
textAlign: 'left',
|
||||
padding: '10px 11px',
|
||||
borderRadius: 10,
|
||||
border: '1px solid rgba(255,255,255,0.05)',
|
||||
background: 'rgba(255,255,255,0.025)',
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
opacity: disabled ? 0.6 : 1,
|
||||
}}
|
||||
title={language === 'zh' ? `点击让 Agent 交易 ${display}` : `Ask agent to trade ${display}`}
|
||||
>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<span style={{ color: '#EAECEF', fontWeight: 700, fontSize: 12.5 }}>{display}</span>
|
||||
<span style={{ color: '#4c4c62', fontSize: 10 }}>{s.category}</span>
|
||||
{!!s.maxLeverage && <span style={{ color: '#F0B90B', fontSize: 10 }}>{s.maxLeverage}x</span>}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 6, alignItems: 'center', color: '#6c6c82', fontSize: 10.5, marginTop: 3 }}>
|
||||
<span style={{ color: '#6c6c82', fontSize: 10.5 }}>
|
||||
Vol {formatVolume(s.volume_24h)} · {formatPrice(s.mark_price)}
|
||||
</span>
|
||||
<span style={{ color: Number(s.change_24h_pct || 0) >= 0 ? '#0ECB81' : '#F6465D', fontSize: 10.5, fontWeight: 700 }}>
|
||||
{formatChange(s.change_24h_pct)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', color: '#F0B90B', gap: 4, fontSize: 11, fontWeight: 700 }}>
|
||||
<Zap size={13} />
|
||||
{language === 'zh' ? '交易' : 'Trade'}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{ color: '#4c4c62', fontSize: 10.5, lineHeight: 1.45 }}>
|
||||
{language === 'zh'
|
||||
? `已列出 ${symbols.length} 个 Hyperliquid USDC 标的。默认按涨幅榜排序,也可切换跌幅榜/成交额;点击交易会直接创建固定标的 Trader。`
|
||||
: `${symbols.length} Hyperliquid USDC markets loaded. Default ranking is 24h gainers; switch to losers/volume. Click Trade to create a fixed-symbol trader directly.`}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import {
|
||||
Zap,
|
||||
BarChart3,
|
||||
Lightbulb,
|
||||
Search,
|
||||
Bot,
|
||||
} from 'lucide-react'
|
||||
|
||||
interface SuggestionCard {
|
||||
@@ -21,16 +21,16 @@ interface WelcomeScreenProps {
|
||||
export function WelcomeScreen({ language, onSend }: WelcomeScreenProps) {
|
||||
const suggestions: SuggestionCard[] = language === 'zh'
|
||||
? [
|
||||
{ icon: <BarChart3 size={18} />, title: '分析 BTC 走势', subtitle: '技术分析 + 市场情绪', cmd: '分析一下 BTC 的走势' },
|
||||
{ icon: <Zap size={18} />, title: '做多 ETH', subtitle: 'Agent 帮你自动下单', cmd: '帮我做多 ETH 0.01 手' },
|
||||
{ icon: <Search size={18} />, title: '搜索股票', subtitle: '输入名称或代码即可', cmd: '搜索一下中远海控' },
|
||||
{ icon: <Lightbulb size={18} />, title: '策略建议', subtitle: '根据当前市场给出建议', cmd: '当前市场适合什么策略?' },
|
||||
{ icon: <Bot size={18} />, title: '创建美股 Agent', subtitle: '强势股 + 严格风控', cmd: '创建一个美股趋势交易 Agent,默认选择5个强势美股,严格风控' },
|
||||
{ icon: <Zap size={18} />, title: '一句话建策略', subtitle: '从想法到 Agent', cmd: '我想做美股强趋势突破,帮我生成策略和Agent' },
|
||||
{ icon: <Search size={18} />, title: '搜索美股', subtitle: '输入名称或代码即可', cmd: '搜索一下 NVIDIA 和 Apple' },
|
||||
{ icon: <Lightbulb size={18} />, title: '全球资产策略', subtitle: '美股/黄金/外汇', cmd: '当前美股、黄金、外汇适合什么策略?' },
|
||||
]
|
||||
: [
|
||||
{ icon: <BarChart3 size={18} />, title: 'Analyze BTC', subtitle: 'Technical analysis + sentiment', cmd: 'Analyze BTC price action' },
|
||||
{ icon: <Zap size={18} />, title: 'Trade ETH', subtitle: 'Agent executes for you', cmd: 'Open a long position on ETH 0.01' },
|
||||
{ icon: <Search size={18} />, title: 'Search Stocks', subtitle: 'Enter name or ticker', cmd: 'Search for NVIDIA stock' },
|
||||
{ icon: <Lightbulb size={18} />, title: 'Strategy Ideas', subtitle: 'Market-based suggestions', cmd: 'What strategy fits the current market?' },
|
||||
{ icon: <Bot size={18} />, title: 'Create US Stock Agent', subtitle: 'Strong stocks + strict risk', cmd: 'Create a US stock trend-following agent with 5 strong stocks and strict risk control' },
|
||||
{ icon: <Zap size={18} />, title: 'One-line strategy', subtitle: 'Idea to agent', cmd: 'I want a US stock breakout strategy; build the strategy and agent' },
|
||||
{ icon: <Search size={18} />, title: 'Search US stocks', subtitle: 'Name or ticker', cmd: 'Search for NVIDIA and Apple stocks' },
|
||||
{ icon: <Lightbulb size={18} />, title: 'Global strategy', subtitle: 'Stocks/gold/FX', cmd: 'What strategy fits US stocks, gold, and FX now?' },
|
||||
]
|
||||
|
||||
return (
|
||||
@@ -72,7 +72,7 @@ export function WelcomeScreen({ language, onSend }: WelcomeScreenProps) {
|
||||
margin: '0 0 8px',
|
||||
letterSpacing: '-0.02em',
|
||||
}}>
|
||||
{language === 'zh' ? '跟 NOFXi 聊点什么' : 'What can I help with?'}
|
||||
{language === 'zh' ? '快速创建你的美股 Agent' : 'Create your US stock agent'}
|
||||
</h1>
|
||||
<p style={{
|
||||
fontSize: 13.5,
|
||||
@@ -81,8 +81,8 @@ export function WelcomeScreen({ language, onSend }: WelcomeScreenProps) {
|
||||
lineHeight: 1.5,
|
||||
}}>
|
||||
{language === 'zh'
|
||||
? '分析行情、执行交易、搜索股票 — 用自然语言就行'
|
||||
: 'Analyze markets, execute trades, search stocks — just ask'}
|
||||
? '美股、大宗、外汇、Pre-IPO — 用自然语言描述策略即可'
|
||||
: 'US stocks, commodities, FX, Pre-IPO — describe the strategy in plain English'}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
BLOCKRUN_MODELS,
|
||||
CLAW402_MODELS,
|
||||
AI_PROVIDER_CONFIG,
|
||||
DEFAULT_CLAW402_MODEL,
|
||||
getShortName,
|
||||
} from './model-constants'
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Wallet,
|
||||
Bot,
|
||||
Bookmark,
|
||||
Zap,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
} from 'lucide-react'
|
||||
@@ -19,6 +20,8 @@ import { WelcomeScreen } from '../components/agent/WelcomeScreen'
|
||||
import { ChatMessages } from '../components/agent/ChatMessages'
|
||||
import { ChatInput, type ChatInputHandle } from '../components/agent/ChatInput'
|
||||
import { UserPreferencesPanel } from '../components/agent/UserPreferencesPanel'
|
||||
import { HyperliquidSymbolsPanel } from '../components/agent/HyperliquidSymbolsPanel'
|
||||
import { createHyperliquidQuickTrader } from '../lib/hyperliquidQuickTrade'
|
||||
import { useAgentChatStore } from '../stores/agentChatStore'
|
||||
import type { AgentMessage as Message, AgentStep } from '../types/agent'
|
||||
import {
|
||||
@@ -459,6 +462,7 @@ export function AgentChatPage() {
|
||||
const setDraftText = useAgentChatStore((state) => state.setDraftText)
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
const chatInputRef = useRef<ChatInputHandle>(null)
|
||||
const pendingHyperSymbolRef = useRef<string | null>(null)
|
||||
|
||||
// Sidebar section collapse state
|
||||
const [sections, setSections] = useState({
|
||||
@@ -466,6 +470,7 @@ export function AgentChatPage() {
|
||||
positions: true,
|
||||
traders: false,
|
||||
preferences: true,
|
||||
hyperliquid: true,
|
||||
})
|
||||
|
||||
const toggleSection = (key: keyof typeof sections) => {
|
||||
@@ -577,9 +582,88 @@ export function AgentChatPage() {
|
||||
chatInputRef.current?.focus()
|
||||
}
|
||||
|
||||
const tradeHyperliquidSymbol = async (symbol: { symbol: string; display?: string; category?: string }) => {
|
||||
const label = symbol.display || symbol.symbol
|
||||
const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
patchMessagesInStore(
|
||||
(prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: nextId(),
|
||||
role: 'user',
|
||||
text:
|
||||
language === 'zh'
|
||||
? `快速创建 Hyperliquid ${label} 单标的交易员`
|
||||
: `Quick-create a Hyperliquid ${label} single-symbol trader`,
|
||||
time,
|
||||
},
|
||||
{
|
||||
id: nextId(),
|
||||
role: 'bot',
|
||||
text: language === 'zh' ? '正在直接创建策略和 Trader,不再走聊天意图猜测…' : 'Creating the strategy and trader directly, without routing through chat intent guessing…',
|
||||
time,
|
||||
streaming: true,
|
||||
},
|
||||
],
|
||||
user?.id || storageUserId
|
||||
)
|
||||
try {
|
||||
const result = await createHyperliquidQuickTrader(symbol, language === 'zh' ? 'zh' : 'en')
|
||||
patchMessagesInStore(
|
||||
(prev) =>
|
||||
prev.map((m) =>
|
||||
m.streaming
|
||||
? {
|
||||
...m,
|
||||
streaming: false,
|
||||
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
|
||||
text:
|
||||
language === 'zh'
|
||||
? `${result.reusedTrader ? '已找到并复用' : '已创建'} Hyperliquid ${result.display} 单标的 Trader:${result.traderName}\n\n策略:${result.strategyName}\n标的:${result.display}\n\n我没有自动启动实盘交易。请到 Traders 面板确认风控后手动 Start。`
|
||||
: `${result.reusedTrader ? 'Reused existing' : 'Created'} Hyperliquid ${result.display} single-symbol trader: ${result.traderName}\n\nStrategy: ${result.strategyName}\nSymbol: ${result.display}\n\nLive trading was not auto-started. Review risk controls in Traders, then start manually.`,
|
||||
}
|
||||
: m
|
||||
),
|
||||
user?.id || storageUserId
|
||||
)
|
||||
window.dispatchEvent(new CustomEvent('agent-config-refresh'))
|
||||
} catch (err: any) {
|
||||
patchMessagesInStore(
|
||||
(prev) =>
|
||||
prev.map((m) =>
|
||||
m.streaming
|
||||
? {
|
||||
...m,
|
||||
streaming: false,
|
||||
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
|
||||
text: `⚠️ ${err?.message || 'Failed to create quick trader'}`,
|
||||
}
|
||||
: m
|
||||
),
|
||||
user?.id || storageUserId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const symbol = params.get('hyperSymbol')
|
||||
if (!symbol || pendingHyperSymbolRef.current === symbol || loading) return
|
||||
pendingHyperSymbolRef.current = symbol
|
||||
void tradeHyperliquidSymbol({
|
||||
symbol,
|
||||
display: params.get('hyperDisplay') || symbol,
|
||||
category: params.get('hyperCategory') || 'stock',
|
||||
})
|
||||
const cleanUrl = `${window.location.pathname}${window.location.hash}`
|
||||
window.history.replaceState({}, '', cleanUrl)
|
||||
}, [loading, language])
|
||||
|
||||
const quickActions =
|
||||
language === 'zh'
|
||||
? [
|
||||
{ label: '🇺🇸 创建美股Agent', cmd: '创建一个美股趋势交易 Agent,默认选择5个强势美股,严格风控' },
|
||||
{ label: '🗣 一句话策略', cmd: '我想做美股强趋势突破,帮我生成策略和Agent' },
|
||||
{ label: '💼 持仓', cmd: '/positions' },
|
||||
{ label: '💰 余额', cmd: '/balance' },
|
||||
{ label: '📋 Traders', cmd: '/traders' },
|
||||
@@ -588,6 +672,8 @@ export function AgentChatPage() {
|
||||
{ label: '❓ 帮助', cmd: '/help' },
|
||||
]
|
||||
: [
|
||||
{ label: '🇺🇸 Create US Stock Agent', cmd: 'Create a US stock trend-following agent with 5 strong stocks and strict risk control' },
|
||||
{ label: '🗣 One-line strategy', cmd: 'I want a US stock breakout strategy; build the strategy and agent' },
|
||||
{ label: '💼 Positions', cmd: '/positions' },
|
||||
{ label: '💰 Balance', cmd: '/balance' },
|
||||
{ label: '📋 Traders', cmd: '/traders' },
|
||||
@@ -603,6 +689,18 @@ export function AgentChatPage() {
|
||||
title: language === 'zh' ? '市场行情' : 'Market',
|
||||
component: <MarketTicker />,
|
||||
},
|
||||
{
|
||||
key: 'hyperliquid' as const,
|
||||
icon: <Zap size={14} />,
|
||||
title: language === 'zh' ? '美股/全球标的' : 'US & Global Markets',
|
||||
component: (
|
||||
<HyperliquidSymbolsPanel
|
||||
language={language}
|
||||
disabled={loading}
|
||||
onTradeSymbol={tradeHyperliquidSymbol}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'positions' as const,
|
||||
icon: <Wallet size={14} />,
|
||||
|
||||
Reference in New Issue
Block a user