import React, { useState, useEffect } from 'react' import { useNavigate } from 'react-router-dom' import useSWR from 'swr' import { api } from '../lib/api' import type { TraderInfo, CreateTraderRequest, AIModel, Exchange, } from '../types' import { useLanguage } from '../contexts/LanguageContext' import { t, type Language } from '../i18n/translations' import { useAuth } from '../contexts/AuthContext' import { getExchangeIcon } from './ExchangeIcons' import { getModelIcon } from './ModelIcons' import { TraderConfigModal } from './TraderConfigModal' import { DeepVoidBackground } from './DeepVoidBackground' import { ExchangeConfigModal } from './traders/ExchangeConfigModal' import { TelegramConfigModal } from './traders/TelegramConfigModal' import { PunkAvatar, getTraderAvatar } from './PunkAvatar' import { Bot, Brain, Landmark, BarChart3, Trash2, Plus, Users, Pencil, Eye, EyeOff, ExternalLink, Copy, Check, MessageCircle, } from 'lucide-react' import { confirmToast } from '../lib/notify' import { toast } from 'sonner' // 获取友好的AI模型名称 function getModelDisplayName(modelId: string): string { switch (modelId.toLowerCase()) { case 'deepseek': return 'DeepSeek' case 'qwen': return 'Qwen' case 'claude': return 'Claude' default: return modelId.toUpperCase() } } // 提取下划线后面的名称部分 function getShortName(fullName: string): string { const parts = fullName.split('_') return parts.length > 1 ? parts[parts.length - 1] : fullName } // Top models available through BlockRun wallet providers const BLOCKRUN_MODELS = [ { id: 'gpt-5.4', name: 'GPT-5.4', desc: 'OpenAI · Flagship' }, { id: 'claude-opus-4.6', name: 'Claude Opus 4.6', desc: 'Anthropic · Flagship' }, { id: 'gemini-3.1-pro', name: 'Gemini 3.1 Pro', desc: 'Google · Flagship' }, { id: 'grok-3', name: 'Grok 3', desc: 'xAI · Flagship' }, { id: 'deepseek-chat', name: 'DeepSeek Chat', desc: 'DeepSeek · Flagship' }, { id: 'minimax-m2.5', name: 'MiniMax M2.5', desc: 'MiniMax · Flagship' }, ] // Models available through Claw402 (x402 USDC payment protocol) const CLAW402_MODELS = [ { id: 'gpt-5.4', name: 'GPT-5.4', provider: 'OpenAI', desc: 'Flagship · Fast', icon: '⚡' }, { id: 'gpt-5.4-pro', name: 'GPT-5.4 Pro', provider: 'OpenAI', desc: 'Reasoning · Pro', icon: '🧠' }, { id: 'gpt-5.3', name: 'GPT-5.3', provider: 'OpenAI', desc: 'Balanced', icon: '💡' }, { id: 'gpt-5-mini', name: 'GPT-5 Mini', provider: 'OpenAI', desc: 'Fast · Cheap', icon: '🚀' }, { id: 'claude-opus', name: 'Claude Opus', provider: 'Anthropic', desc: 'Flagship · Deep', icon: '🎯' }, { id: 'deepseek', name: 'DeepSeek V3', provider: 'DeepSeek', desc: 'Best Value', icon: '🔥' }, { id: 'deepseek-reasoner', name: 'DeepSeek R1', provider: 'DeepSeek', desc: 'Reasoning', icon: '🤔' }, { id: 'qwen-max', name: 'Qwen Max', provider: 'Alibaba', desc: 'Flagship', icon: '🌟' }, { id: 'qwen-plus', name: 'Qwen Plus', provider: 'Alibaba', desc: 'Balanced', icon: '✨' }, { id: 'grok-4.1', name: 'Grok 4.1', provider: 'xAI', desc: 'Flagship', icon: '⚡' }, { id: 'gemini-3.1-pro', name: 'Gemini 3.1 Pro', provider: 'Google', desc: 'Flagship', icon: '💎' }, { id: 'kimi-k2.5', name: 'Kimi K2.5', provider: 'Moonshot', desc: 'Balanced', icon: '🌙' }, ] // AI Provider configuration - default models and API links const AI_PROVIDER_CONFIG: Record = { deepseek: { defaultModel: 'deepseek-chat', apiUrl: 'https://platform.deepseek.com/api_keys', apiName: 'DeepSeek', }, qwen: { defaultModel: 'qwen3-max', apiUrl: 'https://dashscope.console.aliyun.com/apiKey', apiName: 'Alibaba Cloud', }, openai: { defaultModel: 'gpt-5.2', apiUrl: 'https://platform.openai.com/api-keys', apiName: 'OpenAI', }, claude: { defaultModel: 'claude-opus-4-6', apiUrl: 'https://console.anthropic.com/settings/keys', apiName: 'Anthropic', }, gemini: { defaultModel: 'gemini-3-pro-preview', apiUrl: 'https://aistudio.google.com/app/apikey', apiName: 'Google AI Studio', }, grok: { defaultModel: 'grok-3-latest', apiUrl: 'https://console.x.ai/', apiName: 'xAI', }, kimi: { defaultModel: 'moonshot-v1-auto', apiUrl: 'https://platform.moonshot.ai/console/api-keys', apiName: 'Moonshot', }, minimax: { defaultModel: 'MiniMax-M2.5', apiUrl: 'https://platform.minimax.io', apiName: 'MiniMax', }, claw402: { defaultModel: 'deepseek', apiUrl: 'https://claw402.ai', apiName: 'Claw402', }, 'blockrun-base': { defaultModel: 'gpt-5.4', apiUrl: 'https://blockrun.ai', apiName: 'BlockRun', }, 'blockrun-sol': { defaultModel: 'gpt-5.4', apiUrl: 'https://sol.blockrun.ai', apiName: 'BlockRun', }, } interface AITradersPageProps { onTraderSelect?: (traderId: string) => void } // Helper function to get exchange display name from exchange ID (UUID) function getExchangeDisplayName(exchangeId: string | undefined, exchanges: Exchange[]): string { if (!exchangeId) return 'Unknown' const exchange = exchanges.find(e => e.id === exchangeId) if (!exchange) return exchangeId.substring(0, 8).toUpperCase() + '...' // Show truncated UUID if not found const typeName = exchange.exchange_type?.toUpperCase() || exchange.name return exchange.account_name ? `${typeName} - ${exchange.account_name}` : typeName } // Helper function to check if exchange is a perp-dex type (wallet-based) function isPerpDexExchange(exchangeType: string | undefined): boolean { if (!exchangeType) return false const perpDexTypes = ['hyperliquid', 'lighter', 'aster'] return perpDexTypes.includes(exchangeType.toLowerCase()) } // Helper function to get wallet address for perp-dex exchanges function getWalletAddress(exchange: Exchange | undefined): string | undefined { if (!exchange) return undefined const type = exchange.exchange_type?.toLowerCase() switch (type) { case 'hyperliquid': return exchange.hyperliquidWalletAddr case 'lighter': return exchange.lighterWalletAddr case 'aster': return exchange.asterSigner default: return undefined } } // Helper function to truncate wallet address for display function truncateAddress(address: string, startLen = 6, endLen = 4): string { if (address.length <= startLen + endLen + 3) return address return `${address.slice(0, startLen)}...${address.slice(-endLen)}` } export function AITradersPage({ onTraderSelect }: AITradersPageProps) { const { language } = useLanguage() const { user, token } = useAuth() const navigate = useNavigate() const [showCreateModal, setShowCreateModal] = useState(false) const [showEditModal, setShowEditModal] = useState(false) const [showModelModal, setShowModelModal] = useState(false) const [showExchangeModal, setShowExchangeModal] = useState(false) const [showTelegramModal, setShowTelegramModal] = useState(false) const [editingModel, setEditingModel] = useState(null) const [editingExchange, setEditingExchange] = useState(null) const [editingTrader, setEditingTrader] = useState(null) const [allModels, setAllModels] = useState([]) const [allExchanges, setAllExchanges] = useState([]) const [supportedModels, setSupportedModels] = useState([]) 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 toggleTraderAddressVisibility = (traderId: string) => { setVisibleTraderAddresses(prev => { const next = new Set(prev) if (next.has(traderId)) { next.delete(traderId) } else { next.add(traderId) } return next }) } // 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 (id: string, address: string) => { try { await navigator.clipboard.writeText(address) setCopiedId(id) setTimeout(() => setCopiedId(null), 2000) } catch (err) { console.error('Failed to copy address:', err) } } const { data: traders, mutate: mutateTraders, isLoading: isTradersLoading } = useSWR( user && token ? 'traders' : null, api.getTraders, { refreshInterval: 5000 } ) // 加载AI模型和交易所配置 useEffect(() => { const loadConfigs = async () => { if (!user || !token) { // 未登录时只加载公开的支持模型 try { const supportedModels = await api.getSupportedModels() setSupportedModels(supportedModels) } catch (err) { console.error('Failed to load supported configs:', err) } return } try { const [ modelConfigs, exchangeConfigs, supportedModels, ] = await Promise.all([ api.getModelConfigs(), api.getExchangeConfigs(), api.getSupportedModels(), ]) setAllModels(modelConfigs) setAllExchanges(exchangeConfigs) setSupportedModels(supportedModels) } catch (error) { console.error('Failed to load configs:', error) } } loadConfigs() }, [user, token]) // 只显示已配置的模型和交易所 // 注意:后端返回的数据不包含敏感信息(apiKey等),所以通过其他字段判断是否已配置 const configuredModels = allModels?.filter((m) => { // 如果模型已启用,说明已配置 // 或者有自定义API URL,也说明已配置 return m.enabled || (m.customApiUrl && m.customApiUrl.trim() !== '') }) || [] const configuredExchanges = allExchanges?.filter((e) => { // Aster 交易所检查特殊字段 if (e.id === 'aster') { return e.asterUser && e.asterUser.trim() !== '' } // Hyperliquid 需要检查钱包地址(后端会返回这个字段) if (e.id === 'hyperliquid') { return e.hyperliquidWalletAddr && e.hyperliquidWalletAddr.trim() !== '' } // 其他交易所:如果已启用,说明已配置(后端返回的已配置交易所会有 enabled: true) return e.enabled }) || [] // 只在创建交易员时使用已启用且配置完整的 // 注意:后端返回的数据不包含敏感信息,所以只检查 enabled 状态和必要的非敏感字段 const enabledModels = allModels?.filter((m) => m.enabled) || [] const enabledExchanges = allExchanges?.filter((e) => { if (!e.enabled) return false // Aster 交易所需要特殊字段(后端会返回这些非敏感字段) if (e.id === 'aster') { return ( e.asterUser && e.asterUser.trim() !== '' && e.asterSigner && e.asterSigner.trim() !== '' ) } // Hyperliquid 需要钱包地址(后端会返回这个字段) if (e.id === 'hyperliquid') { return e.hyperliquidWalletAddr && e.hyperliquidWalletAddr.trim() !== '' } // 其他交易所:如果已启用,说明已配置完整(后端只返回已配置的交易所) return true }) || [] // 检查模型是否正在被运行中的交易员使用(用于UI禁用) const isModelInUse = (modelId: string) => { 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 } // 检查交易所是否被任何交易员使用(包括停止状态的) const isExchangeUsedByAnyTrader = (exchangeId: string) => { return traders?.some((t) => t.exchange_id === exchangeId) || false } // 获取使用特定模型的交易员列表 const getTradersUsingModel = (modelId: string) => { return traders?.filter((t) => t.ai_model === modelId) || [] } // 获取使用特定交易所的交易员列表 const getTradersUsingExchange = (exchangeId: string) => { return traders?.filter((t) => t.exchange_id === exchangeId) || [] } const handleCreateTrader = async (data: CreateTraderRequest) => { try { const model = allModels?.find((m) => m.id === data.ai_model_id) const exchange = allExchanges?.find((e) => e.id === data.exchange_id) if (!model?.enabled) { toast.error(t('modelNotConfigured', language)) return } if (!exchange?.enabled) { toast.error(t('exchangeNotConfigured', language)) return } await toast.promise(api.createTrader(data), { loading: '正在创建…', success: '创建成功', error: '创建失败', }) setShowCreateModal(false) // Immediately refresh traders list for better UX await mutateTraders() } catch (error) { console.error('Failed to create trader:', error) toast.error(t('createTraderFailed', language)) } } const handleEditTrader = async (traderId: string) => { try { const traderConfig = await api.getTraderConfig(traderId) setEditingTrader(traderConfig) setShowEditModal(true) } catch (error) { console.error('Failed to fetch trader config:', error) toast.error(t('getTraderConfigFailed', language)) } } const handleSaveEditTrader = async (data: CreateTraderRequest) => { console.log('🔥🔥🔥 handleSaveEditTrader CALLED with data:', data) if (!editingTrader) return try { const model = enabledModels?.find((m) => m.id === data.ai_model_id) const exchange = enabledExchanges?.find((e) => e.id === data.exchange_id) if (!model) { toast.error(t('modelConfigNotExist', language)) return } if (!exchange) { toast.error(t('exchangeConfigNotExist', language)) return } const request = { name: data.name, ai_model_id: data.ai_model_id, exchange_id: data.exchange_id, strategy_id: data.strategy_id, initial_balance: data.initial_balance, scan_interval_minutes: data.scan_interval_minutes, is_cross_margin: data.is_cross_margin, show_in_competition: data.show_in_competition, } console.log('🔥 handleSaveEditTrader - data:', data) console.log('🔥 handleSaveEditTrader - data.strategy_id:', data.strategy_id) console.log('🔥 handleSaveEditTrader - request:', request) await toast.promise(api.updateTrader(editingTrader.trader_id, request), { loading: '正在保存…', success: '保存成功', error: '保存失败', }) setShowEditModal(false) setEditingTrader(null) // Immediately refresh traders list for better UX await mutateTraders() } catch (error) { console.error('Failed to update trader:', error) toast.error(t('updateTraderFailed', language)) } } const handleDeleteTrader = async (traderId: string) => { { const ok = await confirmToast(t('confirmDeleteTrader', language)) if (!ok) return } try { await toast.promise(api.deleteTrader(traderId), { loading: '正在删除…', success: '删除成功', error: '删除失败', }) // Immediately refresh traders list for better UX await mutateTraders() } catch (error) { console.error('Failed to delete trader:', error) toast.error(t('deleteTraderFailed', language)) } } const handleToggleTrader = async (traderId: string, running: boolean) => { try { if (running) { await toast.promise(api.stopTrader(traderId), { loading: '正在停止…', success: '已停止', error: '停止失败', }) } else { await toast.promise(api.startTrader(traderId), { loading: '正在启动…', success: '已启动', error: '启动失败', }) } // Immediately refresh traders list to update running status await mutateTraders() } catch (error) { console.error('Failed to toggle trader:', error) toast.error(t('operationFailed', language)) } } const handleToggleCompetition = async (traderId: string, currentShowInCompetition: boolean) => { try { const newValue = !currentShowInCompetition await toast.promise(api.toggleCompetition(traderId, newValue), { loading: '正在更新…', success: newValue ? '已在竞技场显示' : '已在竞技场隐藏', error: '更新失败', }) // Immediately refresh traders list to update status await mutateTraders() } catch (error) { console.error('Failed to toggle competition visibility:', error) toast.error(t('operationFailed', language)) } } const handleModelClick = (modelId: string) => { if (!isModelInUse(modelId)) { setEditingModel(modelId) setShowModelModal(true) } } const handleExchangeClick = (exchangeId: string) => { if (!isExchangeInUse(exchangeId)) { setEditingExchange(exchangeId) setShowExchangeModal(true) } } // 通用删除配置处理函数 const handleDeleteConfig = async (config: { id: string type: 'model' | 'exchange' checkInUse: (id: string) => boolean getUsingTraders: (id: string) => any[] cannotDeleteKey: string confirmDeleteKey: string allItems: T[] | undefined clearFields: (item: T) => T buildRequest: (items: T[]) => any updateApi: (request: any) => Promise refreshApi: () => Promise setItems: (items: T[]) => void closeModal: () => void errorKey: string }) => { // 检查是否有交易员正在使用 if (config.checkInUse(config.id)) { const usingTraders = config.getUsingTraders(config.id) const traderNames = usingTraders.map((t) => t.trader_name).join(', ') toast.error( `${t(config.cannotDeleteKey, language)} · ${t('tradersUsing', language)}: ${traderNames} · ${t('pleaseDeleteTradersFirst', language)}` ) return } { const ok = await confirmToast(t(config.confirmDeleteKey, language)) if (!ok) return } try { const updatedItems = config.allItems?.map((item) => item.id === config.id ? config.clearFields(item) : item ) || [] const request = config.buildRequest(updatedItems) await toast.promise(config.updateApi(request), { loading: '正在更新配置…', success: '配置已更新', error: '更新配置失败', }) // 重新获取用户配置以确保数据同步 const refreshedItems = await config.refreshApi() config.setItems(refreshedItems) config.closeModal() } catch (error) { console.error(`Failed to delete ${config.type} config:`, error) toast.error(t(config.errorKey, language)) } } const handleDeleteModelConfig = async (modelId: string) => { await handleDeleteConfig({ id: modelId, type: 'model', checkInUse: isModelUsedByAnyTrader, getUsingTraders: getTradersUsingModel, cannotDeleteKey: 'cannotDeleteModelInUse', confirmDeleteKey: 'confirmDeleteModel', allItems: allModels, clearFields: (m) => ({ ...m, apiKey: '', customApiUrl: '', customModelName: '', enabled: false, }), buildRequest: (models) => ({ models: Object.fromEntries( models.map((model) => [ model.provider, { enabled: model.enabled, api_key: model.apiKey || '', custom_api_url: model.customApiUrl || '', custom_model_name: model.customModelName || '', }, ]) ), }), updateApi: api.updateModelConfigs, refreshApi: api.getModelConfigs, setItems: (items) => { // 使用函数式更新确保状态正确更新 setAllModels([...items]) }, closeModal: () => { setShowModelModal(false) setEditingModel(null) }, errorKey: 'deleteConfigFailed', }) } const handleSaveModelConfig = async ( modelId: string, apiKey: string, customApiUrl?: string, customModelName?: string ) => { try { // 创建或更新用户的模型配置 const existingModel = allModels?.find((m) => m.id === modelId) let updatedModels // 找到要配置的模型(优先从已配置列表,其次从支持列表) const modelToUpdate = existingModel || supportedModels?.find((m) => m.id === modelId) if (!modelToUpdate) { toast.error(t('modelNotExist', language)) return } if (existingModel) { // 更新现有配置 updatedModels = allModels?.map((m) => m.id === modelId ? { ...m, apiKey, customApiUrl: customApiUrl || '', customModelName: customModelName || '', enabled: true, } : m ) || [] } else { // 添加新配置 const newModel = { ...modelToUpdate, apiKey, customApiUrl: customApiUrl || '', customModelName: customModelName || '', enabled: true, } updatedModels = [...(allModels || []), newModel] } const request = { models: Object.fromEntries( updatedModels.map((model) => [ model.provider, // 使用 provider 而不是 id { enabled: model.enabled, api_key: model.apiKey || '', custom_api_url: model.customApiUrl || '', custom_model_name: model.customModelName || '', }, ]) ), } await toast.promise(api.updateModelConfigs(request), { loading: '正在更新模型配置…', success: '模型配置已更新', error: '更新模型配置失败', }) // 重新获取用户配置以确保数据同步 const refreshedModels = await api.getModelConfigs() setAllModels(refreshedModels) setShowModelModal(false) setEditingModel(null) } catch (error) { console.error('Failed to save model config:', error) toast.error(t('saveConfigFailed', language)) } } const handleDeleteExchangeConfig = async (exchangeId: string) => { // 检查是否有trader在使用此交易所账户 if (isExchangeUsedByAnyTrader(exchangeId)) { const tradersUsing = getTradersUsingExchange(exchangeId) toast.error( `${t('cannotDeleteExchangeInUse', language)}: ${tradersUsing.join(', ')}` ) return } // 确认删除 const ok = await confirmToast(t('confirmDeleteExchange', language)) if (!ok) return try { await toast.promise(api.deleteExchange(exchangeId), { loading: language === 'zh' ? '正在删除交易所账户…' : 'Deleting exchange account...', success: language === 'zh' ? '交易所账户已删除' : 'Exchange account deleted', error: language === 'zh' ? '删除交易所账户失败' : 'Failed to delete exchange account', }) // 重新获取用户配置以确保数据同步 const refreshedExchanges = await api.getExchangeConfigs() setAllExchanges(refreshedExchanges) setShowExchangeModal(false) setEditingExchange(null) } catch (error) { console.error('Failed to delete exchange config:', error) toast.error(t('deleteExchangeConfigFailed', language)) } } const handleSaveExchangeConfig = async ( exchangeId: string | null, // null for creating new account exchangeType: string, accountName: string, apiKey: string, secretKey?: string, passphrase?: string, testnet?: boolean, hyperliquidWalletAddr?: string, asterUser?: string, asterSigner?: string, asterPrivateKey?: string, lighterWalletAddr?: string, lighterPrivateKey?: string, lighterApiKeyPrivateKey?: string, lighterApiKeyIndex?: number ) => { try { if (exchangeId) { // 更新现有账户配置 const existingExchange = allExchanges?.find((e) => e.id === exchangeId) if (!existingExchange) { toast.error(t('exchangeNotExist', language)) return } const request = { exchanges: { [exchangeId]: { enabled: true, api_key: apiKey || '', secret_key: secretKey || '', passphrase: passphrase || '', testnet: testnet || false, hyperliquid_wallet_addr: hyperliquidWalletAddr || '', aster_user: asterUser || '', aster_signer: asterSigner || '', aster_private_key: asterPrivateKey || '', lighter_wallet_addr: lighterWalletAddr || '', lighter_private_key: lighterPrivateKey || '', lighter_api_key_private_key: lighterApiKeyPrivateKey || '', lighter_api_key_index: lighterApiKeyIndex || 0, }, }, } await toast.promise(api.updateExchangeConfigsEncrypted(request), { loading: language === 'zh' ? '正在更新交易所配置…' : 'Updating exchange config...', success: language === 'zh' ? '交易所配置已更新' : 'Exchange config updated', error: language === 'zh' ? '更新交易所配置失败' : 'Failed to update exchange config', }) } else { // 创建新账户 const createRequest = { exchange_type: exchangeType, account_name: accountName, enabled: true, api_key: apiKey || '', secret_key: secretKey || '', passphrase: passphrase || '', testnet: testnet || false, hyperliquid_wallet_addr: hyperliquidWalletAddr || '', aster_user: asterUser || '', aster_signer: asterSigner || '', aster_private_key: asterPrivateKey || '', lighter_wallet_addr: lighterWalletAddr || '', lighter_private_key: lighterPrivateKey || '', lighter_api_key_private_key: lighterApiKeyPrivateKey || '', lighter_api_key_index: lighterApiKeyIndex || 0, } await toast.promise(api.createExchangeEncrypted(createRequest), { loading: language === 'zh' ? '正在创建交易所账户…' : 'Creating exchange account...', success: language === 'zh' ? '交易所账户已创建' : 'Exchange account created', error: language === 'zh' ? '创建交易所账户失败' : 'Failed to create exchange account', }) } // 重新获取用户配置以确保数据同步 const refreshedExchanges = await api.getExchangeConfigs() setAllExchanges(refreshedExchanges) setShowExchangeModal(false) setEditingExchange(null) } catch (error) { console.error('Failed to save exchange config:', error) toast.error(t('saveConfigFailed', language)) } } const handleAddModel = () => { setEditingModel(null) setShowModelModal(true) } const handleAddExchange = () => { setEditingExchange(null) setShowExchangeModal(true) } return (
{/* Header - Terminal Style */}

{t('aiTraders', language)} {traders?.length || 0} ACTIVE_NODES

SYSTEM_READY

{/* Configuration Status Grid */}
{/* AI Models Card */}

{t('aiModels', language)}

{configuredModels.map((model) => { const inUse = isModelInUse(model.id) const usageInfo = getModelUsageInfo(model.id) return (
handleModelClick(model.id)} >
{getModelIcon(model.provider || model.id, { width: 20, height: 20 }) || ( {getShortName(model.name)[0]} )}
{getShortName(model.name)}
{model.customModelName || AI_PROVIDER_CONFIG[model.provider]?.defaultModel || ''}
{usageInfo.totalCount > 0 ? ( 0 ? 'bg-green-500/10 border-green-500/30 text-green-400' : 'bg-yellow-500/10 border-yellow-500/30 text-yellow-400' }`}> {usageInfo.runningCount}/{usageInfo.totalCount} ACTIVE ) : ( {language === 'zh' ? '就绪' : 'STANDBY'} )}
) })} {configuredModels.length === 0 && (
{t('noModelsConfigured', language)}
)}
{/* Exchanges Card */}

{t('exchanges', language)}

{configuredExchanges.map((exchange) => { const inUse = isExchangeInUse(exchange.id) const usageInfo = getExchangeUsageInfo(exchange.id) return (
handleExchangeClick(exchange.id)} >
{getExchangeIcon(exchange.exchange_type || exchange.id, { width: 20, height: 20 })}
{exchange.exchange_type?.toUpperCase() || getShortName(exchange.name)} {exchange.account_name || 'DEFAULT'}
{exchange.type?.toUpperCase() || 'CEX'}
{/* Wallet Address Display Logic */} {(() => { 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)}
) })()} {usageInfo.totalCount > 0 ? ( 0 ? 'bg-green-500/10 border-green-500/30 text-green-400' : 'bg-yellow-500/10 border-yellow-500/30 text-yellow-400' }`}> {usageInfo.runningCount}/{usageInfo.totalCount} ACTIVE ) : ( {language === 'zh' ? '就绪' : 'STANDBY'} )}
) })} {configuredExchanges.length === 0 && (
{t('noExchangesConfigured', language)}
)}
{/* Traders List */}

{t('currentTraders', language)}

{isTradersLoading ? ( /* Loading Skeleton */
{[1, 2, 3].map((i) => (
))}
) : traders && traders.length > 0 ? (
{traders.map((trader) => (
{trader.trader_name}
{getModelDisplayName( trader.ai_model.split('_').pop() || trader.ai_model )}{' '} Model • {getExchangeDisplayName(trader.exchange_id, allExchanges)}
{/* 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 */}
{/*
{t('status', language)}
*/}
{trader.is_running ? t('running', language) : t('stopped', language)}
{/* Actions: 禁止换行,超出横向滚动 */}
))}
) : (
{t('noTraders', language)}
{t('createFirstTrader', language)}
{(configuredModels.length === 0 || configuredExchanges.length === 0) && (
{configuredModels.length === 0 && configuredExchanges.length === 0 ? t('configureModelsAndExchangesFirst', language) : configuredModels.length === 0 ? t('configureModelsFirst', language) : t('configureExchangesFirst', language)}
)}
)}
{/* Create Trader Modal */} {showCreateModal && ( setShowCreateModal(false)} /> )} {/* Edit Trader Modal */} {showEditModal && editingTrader && ( { setShowEditModal(false) setEditingTrader(null) }} /> )} {/* Model Configuration Modal */} {showModelModal && ( { setShowModelModal(false) setEditingModel(null) }} language={language} /> )} {/* Exchange Configuration Modal */} {showExchangeModal && ( { setShowExchangeModal(false) setEditingExchange(null) }} language={language} /> )} {/* Telegram Bot Modal */} {showTelegramModal && ( setShowTelegramModal(false)} language={language} /> )}
) } // Step indicator component for Model Config function ModelStepIndicator({ currentStep, labels }: { currentStep: number; labels: string[] }) { return (
{labels.map((label, index) => (
{index < currentStep ? : index + 1}
{label}
{index < labels.length - 1 && (
)} ))}
) } // Model card component function ModelCard({ model, selected, onClick, configured, }: { model: AIModel selected: boolean onClick: () => void configured?: boolean }) { return ( ) } // Model Configuration Modal Component export function ModelConfigModal({ allModels, configuredModels, editingModelId, onSave, onDelete, onClose, language, }: { allModels: AIModel[] configuredModels: AIModel[] editingModelId: string | null onSave: ( modelId: string, apiKey: string, baseUrl?: string, modelName?: string ) => void onDelete: (modelId: string) => void onClose: () => void language: Language }) { const [currentStep, setCurrentStep] = useState(editingModelId ? 1 : 0) const [selectedModelId, setSelectedModelId] = useState(editingModelId || '') const [apiKey, setApiKey] = useState('') const [baseUrl, setBaseUrl] = useState('') const [modelName, setModelName] = useState('') // Always prefer allModels (supportedModels) for provider/id lookup; // fall back to configuredModels for edit mode details (apiKey etc.) const selectedModel = allModels?.find((m) => m.id === selectedModelId) || configuredModels?.find((m) => m.id === selectedModelId) useEffect(() => { if (editingModelId && selectedModel) { setApiKey(selectedModel.apiKey || '') setBaseUrl(selectedModel.customApiUrl || '') setModelName(selectedModel.customModelName || '') } }, [editingModelId, selectedModel]) const handleSelectModel = (modelId: string) => { setSelectedModelId(modelId) setCurrentStep(1) } const handleBack = () => { if (editingModelId) { onClose() } else { setCurrentStep(0) setSelectedModelId('') } } const handleSubmit = (e: React.FormEvent) => { e.preventDefault() if (!selectedModelId || !apiKey.trim()) return onSave(selectedModelId, apiKey.trim(), baseUrl.trim() || undefined, modelName.trim() || undefined) } const availableModels = allModels || [] const configuredIds = new Set(configuredModels?.map(m => m.id) || []) const stepLabels = language === 'zh' ? ['选择模型', '配置 API'] : ['Select Model', 'Configure API'] return (
{/* Header */}
{currentStep > 0 && !editingModelId && ( )}

{editingModelId ? t('editAIModel', language) : t('addAIModel', language)}

{editingModelId && ( )}
{/* Step Indicator */} {!editingModelId && (
)} {/* Content */}
{/* Step 0: Select Model */} {currentStep === 0 && !editingModelId && (
{language === 'zh' ? '选择 AI 模型提供商' : 'Choose Your AI Provider'}
{/* Claw402 Featured Card — always first, always prominent */} {availableModels.some(m => m.provider === 'claw402') && ( )}
{availableModels.filter(m => !m.provider?.startsWith('blockrun') && m.provider !== 'claw402').map((model) => ( handleSelectModel(model.id)} configured={configuredIds.has(model.id)} /> ))}
{availableModels.some(m => m.provider?.startsWith('blockrun')) && ( <>
{language === 'zh' ? '通过钱包支付' : 'Via BlockRun Wallet'}
{availableModels.filter(m => m.provider?.startsWith('blockrun')).map((model) => ( handleSelectModel(model.id)} configured={configuredIds.has(model.id)} /> ))}
)}
{language === 'zh' ? '带金色标记的模型已配置' : 'Models with gold badge are already configured'}
)} {/* Step 1: Configure — Claw402 Dedicated UI */} {(currentStep === 1 || editingModelId) && selectedModel && (selectedModel.provider === 'claw402' || selectedModel.id === 'claw402') && (
{/* Claw402 Hero Header */}
Claw402
Claw402
{language === 'zh' ? '用 USDC 按次付费,支持所有主流 AI 模型' : 'Pay-per-call with USDC — supports all major AI models'}
{['GPT', 'Claude', 'DeepSeek', 'Gemini', 'Grok', 'Qwen', 'Kimi'].map(name => ( {name} ))}
{/* Step 1: Select AI Model */}
{language === 'zh' ? '所有模型通过 Claw402 统一调用,创建后可随时切换' : 'All models unified via Claw402. Switch anytime after setup.'}
{CLAW402_MODELS.map((m) => { const isSelected = (modelName || 'deepseek') === m.id return ( ) })}
{/* Step 2: Wallet Setup */}
{language === 'zh' ? '💡 Claw402 使用 Base 链上的 USDC 付费,你需要一个 EVM 钱包' : '💡 Claw402 uses USDC on Base chain. You need an EVM wallet.'}
{language === 'zh' ? '可以用 MetaMask、Rabby 等钱包导出私钥' : 'Export private key from MetaMask, Rabby, etc.'}
{language === 'zh' ? '建议新建一个专用钱包,充入少量 USDC 即可' : 'Recommended: create a dedicated wallet with a small USDC balance'}
{language === 'zh' ? '钱包私钥(Base 链 EVM)' : 'Wallet Private Key (Base Chain EVM)'}
setApiKey(e.target.value)} placeholder="0x..." className="w-full px-4 py-3 rounded-xl font-mono text-sm" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} required />
🔒 {language === 'zh' ? '私钥仅在本地签名使用,不会上传或发送交易。无需 ETH,无 Gas 费用。' : 'Private key is only used locally for signing. Never uploaded. No ETH or gas needed.'}
{/* USDC Recharge Guide */}
💰 {language === 'zh' ? '如何充值 USDC' : 'How to Fund USDC'}
1. {language === 'zh' ? '从交易所(Binance / OKX / Coinbase)提 USDC 到你的钱包地址' : 'Withdraw USDC from exchange (Binance/OKX/Coinbase) to your wallet'}
2. {language === 'zh' ? '选择 Base 网络(手续费极低)' : 'Select Base network (very low fees)'}
3. {language === 'zh' ? '充入 $5-10 USDC 即可使用很长时间(约 $0.003/次调用)' : '$5-10 USDC lasts a long time (~$0.003/call)'}
{/* Buttons */}
)} {/* Step 1: Configure — Standard Providers (non-claw402) */} {(currentStep === 1 || editingModelId) && selectedModel && selectedModel.provider !== 'claw402' && selectedModel.id !== 'claw402' && (
{/* Selected Model Header */}
{getModelIcon(selectedModel.provider || selectedModel.id, { width: 32, height: 32 }) || ( {selectedModel.name[0]} )}
{getShortName(selectedModel.name)}
{selectedModel.provider} • {AI_PROVIDER_CONFIG[selectedModel.provider]?.defaultModel || selectedModel.id}
{AI_PROVIDER_CONFIG[selectedModel.provider] && ( {selectedModel.provider?.startsWith('blockrun') ? (language === 'zh' ? '开始使用' : 'Get Started') : (language === 'zh' ? '获取 API Key' : 'Get API Key')} )}
{/* Kimi Warning */} {selectedModel.provider === 'kimi' && (
⚠️
{t('kimiApiNote', language)}
)} {/* API Key / Wallet Private Key */}
setApiKey(e.target.value)} placeholder={ selectedModel.provider === 'blockrun-base' ? '0x... (EVM private key)' : selectedModel.provider === 'blockrun-sol' ? 'bs58 encoded key (Solana)' : t('enterAPIKey', language) } className="w-full px-4 py-3 rounded-xl" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} required />
{/* Custom Base URL (hidden for BlockRun) */} {!selectedModel.provider?.startsWith('blockrun') && (
setBaseUrl(e.target.value)} placeholder={t('customBaseURLPlaceholder', language)} className="w-full px-4 py-3 rounded-xl" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} />
{t('leaveBlankForDefault', language)}
)} {/* Custom Model Name (hidden for BlockRun) */} {!selectedModel.provider?.startsWith('blockrun') && (
setModelName(e.target.value)} placeholder={t('customModelNamePlaceholder', language)} className="w-full px-4 py-3 rounded-xl" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} />
{t('leaveBlankForDefaultModel', language)}
)} {/* BlockRun Model Selector */} {selectedModel.provider?.startsWith('blockrun') && (
{BLOCKRUN_MODELS.map((m) => { const isSelected = (modelName || BLOCKRUN_MODELS[0].id) === m.id return ( ) })}
)} {/* Info Box */}
{t('information', language)}
• {t('modelConfigInfo1', language)}
• {t('modelConfigInfo2', language)}
• {t('modelConfigInfo3', language)}
{/* Buttons */}
)}
) }