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:
tinklefund
2026-05-25 01:25:10 +08:00
parent 5bdffee3b0
commit f4ee723aa2
8 changed files with 386 additions and 24 deletions

View File

@@ -286,7 +286,7 @@ func TestLoadExchangeOptionsHidesInvisibleExchangeRows(t *testing.T) {
}).Error; err != nil { }).Error; err != nil {
t.Fatalf("seed legacy hidden exchange: %v", err) 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) t.Fatalf("create visible exchange: %v", err)
} }
@@ -307,7 +307,7 @@ func TestDescribeExchangeIncludesTypeSpecificVisibleFields(t *testing.T) {
} }
a := New(nil, st, DefaultConfig(), slog.Default()) 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 { if err != nil {
t.Fatalf("seed hyperliquid exchange: %v", err) 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 { if err != nil {
t.Fatalf("seed lighter exchange: %v", err) 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 { if err := st.AIModel().UpdateWithName("default", "default_deepseek", "DeepSeek", true, "sk-test-12345", "", "deepseek-chat"); err != nil {
t.Fatalf("seed model: %v", err) 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 { if err != nil {
t.Fatalf("seed exchange: %v", err) 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 { if err := st.AIModel().UpdateWithName("default", "default_deepseek", "DeepSeek", true, "sk-test-12345", "", "deepseek-chat"); err != nil {
t.Fatalf("seed model: %v", err) 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 { if err != nil {
t.Fatalf("seed exchange: %v", err) 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 { if err := st.AIModel().UpdateWithName("default", "default_deepseek", "DeepSeek", true, "sk-test-12345", "", "deepseek-chat"); err != nil {
t.Fatalf("seed model: %v", err) 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 { if err != nil {
t.Fatalf("seed exchange: %v", err) t.Fatalf("seed exchange: %v", err)
} }

View File

@@ -455,7 +455,7 @@ func (a *Agent) saveSetupExchange(storeUserID string, state *SetupState) (string
storeUserID, ex.ID, true, storeUserID, ex.ID, true,
apiKey, apiSecret, passphrase, apiKey, apiSecret, passphrase,
false, false,
hlWallet, hlUnified, hlWallet, hlUnified, false,
"", "", "", "", "", "",
"", "", "", 0, "", "", "", 0,
); err != nil { ); err != nil {
@@ -472,7 +472,7 @@ func (a *Agent) saveSetupExchange(storeUserID string, state *SetupState) (string
true, true,
apiKey, apiSecret, passphrase, apiKey, apiSecret, passphrase,
false, false,
hlWallet, hlUnified, hlWallet, hlUnified, false,
"", "", "", "", "", "",
"", "", "", 0, "", "", "", 0,
) )

View File

@@ -1516,6 +1516,7 @@ func (a *Agent) toolManageExchangeConfig(storeUserID, argsJSON string) string {
testnet, testnet,
strings.TrimSpace(args.HyperliquidWalletAddr), strings.TrimSpace(args.HyperliquidWalletAddr),
unified, unified,
false,
strings.TrimSpace(args.AsterUser), strings.TrimSpace(args.AsterUser),
strings.TrimSpace(args.AsterSigner), strings.TrimSpace(args.AsterSigner),
strings.TrimSpace(args.AsterPrivateKey), strings.TrimSpace(args.AsterPrivateKey),
@@ -1647,6 +1648,7 @@ func (a *Agent) toolManageExchangeConfig(storeUserID, argsJSON string) string {
testnet, testnet,
hyperWallet, hyperWallet,
unified, unified,
existing.HyperliquidBuilderApproved,
asterUser, asterUser,
asterSigner, asterSigner,
strings.TrimSpace(args.AsterPrivateKey), strings.TrimSpace(args.AsterPrivateKey),
@@ -2518,9 +2520,13 @@ func (a *Agent) toolStartTrader(storeUserID, traderID string) string {
if a.traderManager == nil { if a.traderManager == nil {
return `{"error":"trader manager unavailable"}` 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) 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 existing, err := a.traderManager.GetTrader(traderID); err == nil {
if running, ok := existing.GetStatus()["is_running"].(bool); ok && running { if running, ok := existing.GetStatus()["is_running"].(bool); ok && running {
return `{"error":"trader is already running"}` return `{"error":"trader is already running"}`

View File

@@ -372,7 +372,7 @@ func TestHydrateCreateTraderSlotReferencesNormalizesExchangeIDFromVisibleName(t
} }
a := New(nil, st, DefaultConfig(), slog.Default()) 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 { if err != nil {
t.Fatalf("seed exchange: %v", err) 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 { if err := st.AIModel().UpdateWithName("default", "default_deepseek", "DeepSeek AI", true, "sk-test-12345", "", "deepseek-chat"); err != nil {
t.Fatalf("seed model: %v", err) 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 { if err != nil {
t.Fatalf("seed exchange: %v", err) t.Fatalf("seed exchange: %v", err)
} }

View 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>
)
}

View File

@@ -1,9 +1,9 @@
import { motion } from 'framer-motion' import { motion } from 'framer-motion'
import { import {
Zap, Zap,
BarChart3,
Lightbulb, Lightbulb,
Search, Search,
Bot,
} from 'lucide-react' } from 'lucide-react'
interface SuggestionCard { interface SuggestionCard {
@@ -21,16 +21,16 @@ interface WelcomeScreenProps {
export function WelcomeScreen({ language, onSend }: WelcomeScreenProps) { export function WelcomeScreen({ language, onSend }: WelcomeScreenProps) {
const suggestions: SuggestionCard[] = language === 'zh' const suggestions: SuggestionCard[] = language === 'zh'
? [ ? [
{ icon: <BarChart3 size={18} />, title: '分析 BTC 走势', subtitle: '技术分析 + 市场情绪', cmd: '分析一下 BTC 的走势' }, { icon: <Bot size={18} />, title: '创建美股 Agent', subtitle: '强势股 + 严格风控', cmd: '创建一个美股趋势交易 Agent默认选择5个强势美股严格风控' },
{ icon: <Zap size={18} />, title: '做多 ETH', subtitle: 'Agent 帮你自动下单', cmd: '帮我做多 ETH 0.01 手' }, { icon: <Zap size={18} />, title: '一句话建策略', subtitle: '从想法到 Agent', cmd: '我想做美股强趋势突破帮我生成策略和Agent' },
{ icon: <Search size={18} />, title: '搜索股', subtitle: '输入名称或代码即可', cmd: '搜索一下中远海控' }, { icon: <Search size={18} />, title: '搜索股', subtitle: '输入名称或代码即可', cmd: '搜索一下 NVIDIA 和 Apple' },
{ icon: <Lightbulb size={18} />, title: '策略建议', subtitle: '根据当前市场给出建议', cmd: '当前市场适合什么策略?' }, { icon: <Lightbulb size={18} />, title: '全球资产策略', subtitle: '美股/黄金/外汇', cmd: '当前美股、黄金、外汇适合什么策略?' },
] ]
: [ : [
{ icon: <BarChart3 size={18} />, title: 'Analyze BTC', subtitle: 'Technical analysis + sentiment', cmd: 'Analyze BTC price action' }, { 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: 'Trade ETH', subtitle: 'Agent executes for you', cmd: 'Open a long position on ETH 0.01' }, { 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 Stocks', subtitle: 'Enter name or ticker', cmd: 'Search for NVIDIA stock' }, { icon: <Search size={18} />, title: 'Search US stocks', subtitle: 'Name or ticker', cmd: 'Search for NVIDIA and Apple stocks' },
{ icon: <Lightbulb size={18} />, title: 'Strategy Ideas', subtitle: 'Market-based suggestions', cmd: 'What strategy fits the current market?' }, { icon: <Lightbulb size={18} />, title: 'Global strategy', subtitle: 'Stocks/gold/FX', cmd: 'What strategy fits US stocks, gold, and FX now?' },
] ]
return ( return (
@@ -72,7 +72,7 @@ export function WelcomeScreen({ language, onSend }: WelcomeScreenProps) {
margin: '0 0 8px', margin: '0 0 8px',
letterSpacing: '-0.02em', letterSpacing: '-0.02em',
}}> }}>
{language === 'zh' ? '跟 NOFXi 聊点什么' : 'What can I help with?'} {language === 'zh' ? '快速创建你的美股 Agent' : 'Create your US stock agent'}
</h1> </h1>
<p style={{ <p style={{
fontSize: 13.5, fontSize: 13.5,
@@ -81,8 +81,8 @@ export function WelcomeScreen({ language, onSend }: WelcomeScreenProps) {
lineHeight: 1.5, lineHeight: 1.5,
}}> }}>
{language === 'zh' {language === 'zh'
? '分析行情、执行交易、搜索股票 — 用自然语言就行' ? '美股、大宗、外汇、Pre-IPO — 用自然语言描述策略即可'
: 'Analyze markets, execute trades, search stocks — just ask'} : 'US stocks, commodities, FX, Pre-IPO — describe the strategy in plain English'}
</p> </p>
</motion.div> </motion.div>

View File

@@ -11,7 +11,6 @@ import {
BLOCKRUN_MODELS, BLOCKRUN_MODELS,
CLAW402_MODELS, CLAW402_MODELS,
AI_PROVIDER_CONFIG, AI_PROVIDER_CONFIG,
DEFAULT_CLAW402_MODEL,
getShortName, getShortName,
} from './model-constants' } from './model-constants'

View File

@@ -7,6 +7,7 @@ import {
Wallet, Wallet,
Bot, Bot,
Bookmark, Bookmark,
Zap,
ChevronDown, ChevronDown,
ChevronRight, ChevronRight,
} from 'lucide-react' } from 'lucide-react'
@@ -19,6 +20,8 @@ import { WelcomeScreen } from '../components/agent/WelcomeScreen'
import { ChatMessages } from '../components/agent/ChatMessages' import { ChatMessages } from '../components/agent/ChatMessages'
import { ChatInput, type ChatInputHandle } from '../components/agent/ChatInput' import { ChatInput, type ChatInputHandle } from '../components/agent/ChatInput'
import { UserPreferencesPanel } from '../components/agent/UserPreferencesPanel' import { UserPreferencesPanel } from '../components/agent/UserPreferencesPanel'
import { HyperliquidSymbolsPanel } from '../components/agent/HyperliquidSymbolsPanel'
import { createHyperliquidQuickTrader } from '../lib/hyperliquidQuickTrade'
import { useAgentChatStore } from '../stores/agentChatStore' import { useAgentChatStore } from '../stores/agentChatStore'
import type { AgentMessage as Message, AgentStep } from '../types/agent' import type { AgentMessage as Message, AgentStep } from '../types/agent'
import { import {
@@ -459,6 +462,7 @@ export function AgentChatPage() {
const setDraftText = useAgentChatStore((state) => state.setDraftText) const setDraftText = useAgentChatStore((state) => state.setDraftText)
const messagesEndRef = useRef<HTMLDivElement>(null) const messagesEndRef = useRef<HTMLDivElement>(null)
const chatInputRef = useRef<ChatInputHandle>(null) const chatInputRef = useRef<ChatInputHandle>(null)
const pendingHyperSymbolRef = useRef<string | null>(null)
// Sidebar section collapse state // Sidebar section collapse state
const [sections, setSections] = useState({ const [sections, setSections] = useState({
@@ -466,6 +470,7 @@ export function AgentChatPage() {
positions: true, positions: true,
traders: false, traders: false,
preferences: true, preferences: true,
hyperliquid: true,
}) })
const toggleSection = (key: keyof typeof sections) => { const toggleSection = (key: keyof typeof sections) => {
@@ -577,9 +582,88 @@ export function AgentChatPage() {
chatInputRef.current?.focus() 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 = const quickActions =
language === 'zh' language === 'zh'
? [ ? [
{ label: '🇺🇸 创建美股Agent', cmd: '创建一个美股趋势交易 Agent默认选择5个强势美股严格风控' },
{ label: '🗣 一句话策略', cmd: '我想做美股强趋势突破帮我生成策略和Agent' },
{ label: '💼 持仓', cmd: '/positions' }, { label: '💼 持仓', cmd: '/positions' },
{ label: '💰 余额', cmd: '/balance' }, { label: '💰 余额', cmd: '/balance' },
{ label: '📋 Traders', cmd: '/traders' }, { label: '📋 Traders', cmd: '/traders' },
@@ -588,6 +672,8 @@ export function AgentChatPage() {
{ label: '❓ 帮助', cmd: '/help' }, { 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: '💼 Positions', cmd: '/positions' },
{ label: '💰 Balance', cmd: '/balance' }, { label: '💰 Balance', cmd: '/balance' },
{ label: '📋 Traders', cmd: '/traders' }, { label: '📋 Traders', cmd: '/traders' },
@@ -603,6 +689,18 @@ export function AgentChatPage() {
title: language === 'zh' ? '市场行情' : 'Market', title: language === 'zh' ? '市场行情' : 'Market',
component: <MarketTicker />, 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, key: 'positions' as const,
icon: <Wallet size={14} />, icon: <Wallet size={14} />,