From 8e57fb986f9d791cd5fff98d4241203269b0498f Mon Sep 17 00:00:00 2001 From: tinkle-community Date: Sat, 20 Dec 2025 15:53:43 +0800 Subject: [PATCH] fix: improve exchange/model edit and usage status display - Fixed ExchangeConfigModal to use allExchanges for editing - Improved wallet address display layout (moved to right side) - Added privacy toggle and copy button for addresses - Enhanced usage status for both AI models and exchanges: - Shows '2/3 Running' when traders are active (green badge) - Shows '2 Traders' when assigned but stopped (yellow badge) - Shows 'Idle' when enabled but not used - Shows 'Configured' when not enabled --- web/src/components/AITradersPage.tsx | 299 ++++++++++++++++++--------- 1 file changed, 202 insertions(+), 97 deletions(-) diff --git a/web/src/components/AITradersPage.tsx b/web/src/components/AITradersPage.tsx index 314a0fc6..7ce1f1ed 100644 --- a/web/src/components/AITradersPage.tsx +++ b/web/src/components/AITradersPage.tsx @@ -153,13 +153,13 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { const [allModels, setAllModels] = useState([]) const [allExchanges, setAllExchanges] = useState([]) const [supportedModels, setSupportedModels] = useState([]) - const [supportedExchanges, setSupportedExchanges] = useState([]) - const [visibleAddresses, setVisibleAddresses] = useState>(new Set()) - const [copiedAddressId, setCopiedAddressId] = useState(null) + const [visibleTraderAddresses, setVisibleTraderAddresses] = useState>(new Set()) + const [visibleExchangeAddresses, setVisibleExchangeAddresses] = useState>(new Set()) + const [copiedId, setCopiedId] = useState(null) // Toggle wallet address visibility for a trader - const toggleAddressVisibility = (traderId: string) => { - setVisibleAddresses(prev => { + const toggleTraderAddressVisibility = (traderId: string) => { + setVisibleTraderAddresses(prev => { const next = new Set(prev) if (next.has(traderId)) { next.delete(traderId) @@ -170,12 +170,25 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { }) } + // Toggle wallet address visibility for an exchange + const toggleExchangeAddressVisibility = (exchangeId: string) => { + setVisibleExchangeAddresses(prev => { + const next = new Set(prev) + if (next.has(exchangeId)) { + next.delete(exchangeId) + } else { + next.add(exchangeId) + } + return next + }) + } + // Copy wallet address to clipboard - const handleCopyAddress = async (traderId: string, address: string) => { + const handleCopyAddress = async (id: string, address: string) => { try { await navigator.clipboard.writeText(address) - setCopiedAddressId(traderId) - setTimeout(() => setCopiedAddressId(null), 2000) + setCopiedId(id) + setTimeout(() => setCopiedId(null), 2000) } catch (err) { console.error('Failed to copy address:', err) } @@ -191,14 +204,10 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { useEffect(() => { const loadConfigs = async () => { if (!user || !token) { - // 未登录时只加载公开的支持模型和交易所 + // 未登录时只加载公开的支持模型 try { - const [supportedModels, supportedExchanges] = await Promise.all([ - api.getSupportedModels(), - api.getSupportedExchanges(), - ]) + const supportedModels = await api.getSupportedModels() setSupportedModels(supportedModels) - setSupportedExchanges(supportedExchanges) } catch (err) { console.error('Failed to load supported configs:', err) } @@ -210,17 +219,14 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { modelConfigs, exchangeConfigs, supportedModels, - supportedExchanges, ] = await Promise.all([ api.getModelConfigs(), api.getExchangeConfigs(), api.getSupportedModels(), - api.getSupportedExchanges(), ]) setAllModels(modelConfigs) setAllExchanges(exchangeConfigs) setSupportedModels(supportedModels) - setSupportedExchanges(supportedExchanges) } catch (error) { console.error('Failed to load configs:', error) } @@ -281,11 +287,27 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { return traders?.some((t) => t.ai_model === modelId && t.is_running) } + // 检查模型被哪些交易员使用 + const getModelUsageInfo = (modelId: string) => { + const usingTraders = traders?.filter((t) => t.ai_model === modelId) || [] + const runningCount = usingTraders.filter((t) => t.is_running).length + const totalCount = usingTraders.length + return { runningCount, totalCount, usingTraders } + } + // 检查交易所是否正在被运行中的交易员使用(用于UI禁用) const isExchangeInUse = (exchangeId: string) => { return traders?.some((t) => t.exchange_id === exchangeId && t.is_running) } + // 检查交易所被哪些交易员使用 + const getExchangeUsageInfo = (exchangeId: string) => { + const usingTraders = traders?.filter((t) => t.exchange_id === exchangeId) || [] + const runningCount = usingTraders.filter((t) => t.is_running).length + const totalCount = usingTraders.length + return { runningCount, totalCount, usingTraders } + } + // 检查模型是否被任何交易员使用(包括停止状态的) const isModelUsedByAnyTrader = (modelId: string) => { return traders?.some((t) => t.ai_model === modelId) || false @@ -882,6 +904,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
{configuredModels.map((model) => { const inUse = isModelInUse(model.id) + const usageInfo = getModelUsageInfo(model.id) return (
{model.customModelName || AI_PROVIDER_CONFIG[model.provider]?.defaultModel || ''}
-
- {inUse - ? t('inUse', language) - : model.enabled - ? t('enabled', language) - : t('configured', language)} +
+ {usageInfo.totalCount > 0 ? ( + 0 + ? { background: 'rgba(14, 203, 129, 0.15)', color: '#0ECB81' } + : { background: 'rgba(240, 185, 11, 0.15)', color: '#F0B90B' } + } + > + {usageInfo.runningCount > 0 + ? `${usageInfo.runningCount}/${usageInfo.totalCount} ${language === 'zh' ? '运行中' : 'Running'}` + : `${usageInfo.totalCount} ${language === 'zh' ? '个交易员' : usageInfo.totalCount === 1 ? 'Trader' : 'Traders'}` + } + + ) : ( + + {model.enabled + ? (language === 'zh' ? '空闲' : 'Idle') + : (language === 'zh' ? '已配置' : 'Configured')} + + )}
@@ -965,6 +1004,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
{configuredExchanges.map((exchange) => { const inUse = isExchangeInUse(exchange.id) + const usageInfo = getExchangeUsageInfo(exchange.id) return (
handleExchangeClick(exchange.id)} > -
+ {/* Left: Icon + Name + Type */} +
{getExchangeIcon(exchange.exchange_type || exchange.id, { width: 28, height: 28 })}
@@ -990,19 +1031,92 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { - {exchange.account_name || 'Default'}
-
- {exchange.type?.toUpperCase() || 'CEX'} •{' '} - {inUse - ? t('inUse', language) - : exchange.enabled - ? t('enabled', language) - : t('configured', language)} +
+ {exchange.type?.toUpperCase() || 'CEX'} + + {usageInfo.totalCount > 0 ? ( + 0 + ? { background: 'rgba(14, 203, 129, 0.15)', color: '#0ECB81' } + : { background: 'rgba(240, 185, 11, 0.15)', color: '#F0B90B' } + } + > + {usageInfo.runningCount > 0 + ? `${usageInfo.runningCount}/${usageInfo.totalCount} ${language === 'zh' ? '运行中' : 'Running'}` + : `${usageInfo.totalCount} ${language === 'zh' ? '个交易员' : usageInfo.totalCount === 1 ? 'Trader' : 'Traders'}` + } + + ) : ( + + {exchange.enabled + ? (language === 'zh' ? '空闲' : 'Idle') + : (language === 'zh' ? '已配置' : 'Configured')} + + )}
-
+ {/* Right: Wallet Address + Status Dot */} +
+ {/* Wallet address for DEX exchanges */} + {(() => { + const walletAddr = exchange.hyperliquidWalletAddr || exchange.asterUser || exchange.lighterWalletAddr + if (exchange.type !== 'dex' || !walletAddr) return null + + const isVisible = visibleExchangeAddresses.has(exchange.id) + const isCopied = copiedId === `exchange-${exchange.id}` + + return ( +
e.stopPropagation()} + > + + {isVisible ? walletAddr : truncateAddress(walletAddr)} + + + +
+ ) + })()} +
+
) })} @@ -1104,70 +1218,61 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
- {/* Wallet Address for Perp-DEX */} - {(() => { - const exchange = allExchanges.find(e => e.id === trader.exchange_id) - const walletAddr = getWalletAddress(exchange) - const isPerpDex = isPerpDexExchange(exchange?.exchange_type) - if (!isPerpDex) return null - - const isVisible = visibleAddresses.has(trader.trader_id) - const isCopied = copiedAddressId === trader.trader_id - - return ( -
- {walletAddr ? ( - <> - - {isVisible ? walletAddr : truncateAddress(walletAddr)} - - - - - ) : ( - - {language === 'zh' ? '未配置地址' : 'No address'} - - )} -
- ) - })()} -
+ {/* Wallet Address for Perp-DEX - placed before status for alignment */} + {(() => { + const exchange = allExchanges.find(e => e.id === trader.exchange_id) + const walletAddr = getWalletAddress(exchange) + const isPerpDex = isPerpDexExchange(exchange?.exchange_type) + if (!isPerpDex || !walletAddr) return null + + const isVisible = visibleTraderAddresses.has(trader.trader_id) + const isCopied = copiedId === trader.trader_id + + return ( +
+ + {isVisible ? walletAddr : truncateAddress(walletAddr)} + + + +
+ ) + })()} {/* Status */}
{/*