mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-05 12:00:59 +08:00
refactor: optimize codebase encoding
This commit is contained in:
@@ -212,9 +212,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
}
|
||||
|
||||
await toast.promise(api.createTrader(data), {
|
||||
loading: '正在创建…',
|
||||
success: '创建成功',
|
||||
error: '创建失败',
|
||||
loading: t('aiTradersToast.creating', language),
|
||||
success: t('aiTradersToast.created', language),
|
||||
error: t('aiTradersToast.createFailed', language),
|
||||
})
|
||||
setShowCreateModal(false)
|
||||
await mutateTraders()
|
||||
@@ -269,9 +269,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
console.log('🔥 handleSaveEditTrader - request:', request)
|
||||
|
||||
await toast.promise(api.updateTrader(editingTrader.trader_id, request), {
|
||||
loading: '正在保存…',
|
||||
success: '保存成功',
|
||||
error: '保存失败',
|
||||
loading: t('aiTradersToast.saving', language),
|
||||
success: t('aiTradersToast.saved', language),
|
||||
error: t('aiTradersToast.saveFailed', language),
|
||||
})
|
||||
setShowEditModal(false)
|
||||
setEditingTrader(null)
|
||||
@@ -290,9 +290,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
|
||||
try {
|
||||
await toast.promise(api.deleteTrader(traderId), {
|
||||
loading: '正在删除…',
|
||||
success: '删除成功',
|
||||
error: '删除失败',
|
||||
loading: t('aiTradersToast.deleting', language),
|
||||
success: t('aiTradersToast.deleted', language),
|
||||
error: t('aiTradersToast.deleteFailed', language),
|
||||
})
|
||||
|
||||
await mutateTraders()
|
||||
@@ -306,15 +306,15 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
try {
|
||||
if (running) {
|
||||
await toast.promise(api.stopTrader(traderId), {
|
||||
loading: '正在停止…',
|
||||
success: '已停止',
|
||||
error: '停止失败',
|
||||
loading: t('aiTradersToast.stopping', language),
|
||||
success: t('aiTradersToast.stopped', language),
|
||||
error: t('aiTradersToast.stopFailed', language),
|
||||
})
|
||||
} else {
|
||||
await toast.promise(api.startTrader(traderId), {
|
||||
loading: '正在启动…',
|
||||
success: '已启动',
|
||||
error: '启动失败',
|
||||
loading: t('aiTradersToast.starting', language),
|
||||
success: t('aiTradersToast.started', language),
|
||||
error: t('aiTradersToast.startFailed', language),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -329,9 +329,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
try {
|
||||
const newValue = !currentShowInCompetition
|
||||
await toast.promise(api.toggleCompetition(traderId, newValue), {
|
||||
loading: '正在更新…',
|
||||
success: newValue ? '已在竞技场显示' : '已在竞技场隐藏',
|
||||
error: '更新失败',
|
||||
loading: t('aiTradersToast.updating', language),
|
||||
success: newValue ? t('aiTradersToast.showInCompetition', language) : t('aiTradersToast.hideInCompetition', language),
|
||||
error: t('aiTradersToast.updateFailed', language),
|
||||
})
|
||||
|
||||
await mutateTraders()
|
||||
@@ -393,9 +393,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
|
||||
const request = config.buildRequest(updatedItems)
|
||||
await toast.promise(config.updateApi(request), {
|
||||
loading: '正在更新配置…',
|
||||
success: '配置已更新',
|
||||
error: '更新配置失败',
|
||||
loading: t('aiTradersToast.updatingConfig', language),
|
||||
success: t('aiTradersToast.configUpdated', language),
|
||||
error: t('aiTradersToast.configUpdateFailed', language),
|
||||
})
|
||||
|
||||
const refreshedItems = await config.refreshApi()
|
||||
@@ -506,9 +506,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
}
|
||||
|
||||
await toast.promise(api.updateModelConfigs(request), {
|
||||
loading: '正在更新模型配置…',
|
||||
success: '模型配置已更新',
|
||||
error: '更新模型配置失败',
|
||||
loading: t('aiTradersToast.updatingModelConfig', language),
|
||||
success: t('aiTradersToast.modelConfigUpdated', language),
|
||||
error: t('aiTradersToast.modelConfigUpdateFailed', language),
|
||||
})
|
||||
|
||||
const refreshedModels = await api.getModelConfigs()
|
||||
@@ -536,9 +536,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
|
||||
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',
|
||||
loading: t('aiTradersToast.deletingExchange', language),
|
||||
success: t('aiTradersToast.exchangeDeleted', language),
|
||||
error: t('aiTradersToast.exchangeDeleteFailed', language),
|
||||
})
|
||||
|
||||
const refreshedExchanges = await api.getExchangeConfigs()
|
||||
@@ -598,9 +598,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
}
|
||||
|
||||
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',
|
||||
loading: t('aiTradersToast.updatingExchangeConfig', language),
|
||||
success: t('aiTradersToast.exchangeConfigUpdated', language),
|
||||
error: t('aiTradersToast.exchangeConfigUpdateFailed', language),
|
||||
})
|
||||
} else {
|
||||
const createRequest = {
|
||||
@@ -622,9 +622,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
}
|
||||
|
||||
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',
|
||||
loading: t('aiTradersToast.creatingExchange', language),
|
||||
success: t('aiTradersToast.exchangeCreated', language),
|
||||
error: t('aiTradersToast.exchangeCreateFailed', language),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -258,7 +258,7 @@ export function ExchangeConfigModal({
|
||||
toast.success(t('ipCopied', language))
|
||||
}
|
||||
} catch {
|
||||
toast.error(t('copyIPFailed', language) || `复制失败: ${ip}`)
|
||||
toast.error(t('copyIPFailed', language))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,7 +305,7 @@ export function ExchangeConfigModal({
|
||||
|
||||
const trimmedAccountName = accountName.trim()
|
||||
if (!trimmedAccountName) {
|
||||
toast.error(language === 'zh' ? '请输入账户名称' : 'Please enter account name')
|
||||
toast.error(t('exchangeConfig.pleaseEnterAccountName', language))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -338,7 +338,7 @@ export function ExchangeConfigModal({
|
||||
}
|
||||
}
|
||||
|
||||
const stepLabels = language === 'zh' ? ['选择交易所', '配置账户'] : ['Select Exchange', 'Configure']
|
||||
const stepLabels = [t('exchangeConfig.selectExchange', language), t('exchangeConfig.configure', language)]
|
||||
const cexExchanges = SUPPORTED_EXCHANGE_TEMPLATES.filter(t => t.type === 'cex')
|
||||
const dexExchanges = SUPPORTED_EXCHANGE_TEMPLATES.filter(t => t.type === 'dex')
|
||||
|
||||
@@ -412,13 +412,13 @@ export function ExchangeConfigModal({
|
||||
{/* Exchange Grid */}
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||
{language === 'zh' ? '选择您的交易所' : 'Choose Your Exchange'}
|
||||
{t('exchangeConfig.chooseExchange', language)}
|
||||
</div>
|
||||
|
||||
{/* CEX */}
|
||||
<div className="space-y-3">
|
||||
<div className="text-xs font-medium uppercase tracking-wide" style={{ color: '#F0B90B' }}>
|
||||
{language === 'zh' ? '中心化交易所 (CEX)' : 'Centralized Exchanges'}
|
||||
{t('exchangeConfig.centralizedExchanges', language)}
|
||||
</div>
|
||||
<div className="grid grid-cols-3 sm:grid-cols-5 gap-3">
|
||||
{cexExchanges.map((template) => (
|
||||
@@ -436,7 +436,7 @@ export function ExchangeConfigModal({
|
||||
{/* DEX */}
|
||||
<div className="space-y-3">
|
||||
<div className="text-xs font-medium uppercase tracking-wide" style={{ color: '#A78BFA' }}>
|
||||
{language === 'zh' ? '去中心化交易所 (DEX)' : 'Decentralized Exchanges'}
|
||||
{t('exchangeConfig.decentralizedExchanges', language)}
|
||||
</div>
|
||||
<div className="grid grid-cols-3 sm:grid-cols-5 gap-3">
|
||||
{dexExchanges.map((template) => (
|
||||
@@ -477,11 +477,11 @@ export function ExchangeConfigModal({
|
||||
>
|
||||
<UserPlus className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||
<span className="text-sm font-medium" style={{ color: '#F0B90B' }}>
|
||||
{language === 'zh' ? '注册' : 'Register'}
|
||||
{t('exchangeConfig.register', language)}
|
||||
</span>
|
||||
{exchangeRegistrationLinks[currentExchangeType || '']?.hasReferral && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded" style={{ background: 'rgba(14, 203, 129, 0.2)', color: '#0ECB81' }}>
|
||||
{language === 'zh' ? '优惠' : 'Bonus'}
|
||||
{t('exchangeConfig.bonus', language)}
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
@@ -491,13 +491,13 @@ export function ExchangeConfigModal({
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||
<Key className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||
{language === 'zh' ? '账户名称' : 'Account Name'} *
|
||||
{t('exchangeConfig.accountName', language)} *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={accountName}
|
||||
onChange={(e) => setAccountName(e.target.value)}
|
||||
placeholder={language === 'zh' ? '例如:主账户、套利账户' : 'e.g., Main Account'}
|
||||
placeholder={t('exchangeConfig.accountNamePlaceholder', language)}
|
||||
className="w-full px-4 py-3 rounded-xl text-base"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
required
|
||||
@@ -517,7 +517,7 @@ export function ExchangeConfigModal({
|
||||
<div className="flex items-center gap-2">
|
||||
<span style={{ color: '#58a6ff' }}>ℹ️</span>
|
||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
||||
{language === 'zh' ? '币安用户必读:使用「现货与合约交易」API' : 'Use "Spot & Futures Trading" API'}
|
||||
{t('exchangeConfig.useBinanceFuturesApi', language)}
|
||||
</span>
|
||||
</div>
|
||||
<span style={{ color: '#8b949e' }}>{showBinanceGuide ? '▲' : '▼'}</span>
|
||||
@@ -532,7 +532,7 @@ export function ExchangeConfigModal({
|
||||
style={{ color: '#58a6ff' }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{language === 'zh' ? '查看官方教程' : 'View Tutorial'} <ExternalLink className="w-3 h-3" />
|
||||
{t('exchangeConfig.viewTutorial', language)} <ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
@@ -696,10 +696,10 @@ export function ExchangeConfigModal({
|
||||
<span style={{ fontSize: '16px' }}>🔐</span>
|
||||
<div>
|
||||
<div className="text-sm font-semibold mb-1" style={{ color: '#3B82F6' }}>
|
||||
{language === 'zh' ? 'Lighter API Key 配置' : 'Lighter API Key Setup'}
|
||||
{t('exchangeConfig.lighterApiKeySetup', language)}
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{language === 'zh' ? '请在 Lighter 网站生成 API Key' : 'Generate an API Key on Lighter website'}
|
||||
{t('exchangeConfig.lighterApiKeyDesc', language)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -717,8 +717,8 @@ export function ExchangeConfigModal({
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||
{language === 'zh' ? 'API Key 索引' : 'API Key Index'}
|
||||
<Tooltip content={language === 'zh' ? 'API Key 索引从0开始' : 'API Key index starts from 0'}>
|
||||
{t('exchangeConfig.apiKeyIndex', language)}
|
||||
<Tooltip content={t('exchangeConfig.apiKeyIndexTooltip', language)}>
|
||||
<HelpCircle className="w-4 h-4 cursor-help" style={{ color: '#3B82F6' }} />
|
||||
</Tooltip>
|
||||
</label>
|
||||
@@ -730,7 +730,7 @@ export function ExchangeConfigModal({
|
||||
{/* Buttons */}
|
||||
<div className="flex gap-3 pt-4">
|
||||
<button type="button" onClick={handleBack} className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5" style={{ background: '#2B3139', color: '#848E9C' }}>
|
||||
{editingExchangeId ? t('cancel', language) : (language === 'zh' ? '返回' : 'Back')}
|
||||
{editingExchangeId ? t('cancel', language) : t('exchangeConfig.back', language)}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
@@ -738,7 +738,7 @@ export function ExchangeConfigModal({
|
||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style={{ background: '#F0B90B', color: '#000' }}
|
||||
>
|
||||
{isSaving ? (t('saving', language) || '保存中...') : (
|
||||
{isSaving ? t('saving', language) : (
|
||||
<>{t('saveConfig', language)} <ArrowRight className="w-4 h-4" /></>
|
||||
)}
|
||||
</button>
|
||||
|
||||
@@ -79,7 +79,7 @@ export function ModelConfigModal({
|
||||
|
||||
const availableModels = allModels || []
|
||||
const configuredIds = new Set(configuredModels?.map(m => m.id) || [])
|
||||
const stepLabels = language === 'zh' ? ['选择模型', '配置 API'] : ['Select Model', 'Configure API']
|
||||
const stepLabels = [t('modelConfig.selectModel', language), t('modelConfig.configureApi', language)]
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4 overflow-y-auto backdrop-blur-sm">
|
||||
@@ -192,7 +192,7 @@ function ModelSelectionStep({
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||
{language === 'zh' ? '选择 AI 模型提供商' : 'Choose Your AI Provider'}
|
||||
{t('modelConfig.chooseProvider', language)}
|
||||
</div>
|
||||
|
||||
{/* Claw402 Featured Card */}
|
||||
@@ -217,9 +217,7 @@ function ModelSelectionStep({
|
||||
<a href="https://claw402.ai" target="_blank" rel="noopener noreferrer" onClick={(e) => e.stopPropagation()} className="ml-1.5 text-[10px] font-normal px-1.5 py-0.5 rounded" style={{ color: '#60A5FA', background: 'rgba(96, 165, 250, 0.1)' }}>↗ claw402.ai</a>
|
||||
</div>
|
||||
<div className="text-xs mt-0.5" style={{ color: '#A0AEC0' }}>
|
||||
{language === 'zh'
|
||||
? 'USDC 按次付费 · 支持全部 AI 模型 · 无需 API Key'
|
||||
: 'Pay-per-call USDC · All AI Models · No API Key'}
|
||||
{t('modelConfig.payPerCall', language)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -228,7 +226,7 @@ function ModelSelectionStep({
|
||||
<div className="w-2 h-2 rounded-full" style={{ background: '#00E096' }} />
|
||||
)}
|
||||
<div className="px-3 py-1.5 rounded-full text-xs font-bold" style={{ background: 'linear-gradient(135deg, #2563EB, #7C3AED)', color: '#fff' }}>
|
||||
{language === 'zh' ? '🔥 推荐' : '🔥 Best'}
|
||||
{'🔥 ' + t('modelConfig.recommended', language)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -256,7 +254,7 @@ function ModelSelectionStep({
|
||||
<div className="flex items-center gap-3 pt-2">
|
||||
<div className="flex-1 h-px" style={{ background: '#2B3139' }} />
|
||||
<span className="text-xs font-medium px-2" style={{ color: '#848E9C' }}>
|
||||
{language === 'zh' ? '通过钱包支付' : 'Via BlockRun Wallet'}
|
||||
{t('modelConfig.viaBlockrunWallet', language)}
|
||||
</span>
|
||||
<div className="flex-1 h-px" style={{ background: '#2B3139' }} />
|
||||
</div>
|
||||
@@ -274,7 +272,7 @@ function ModelSelectionStep({
|
||||
</>
|
||||
)}
|
||||
<div className="text-xs text-center pt-2" style={{ color: '#848E9C' }}>
|
||||
{language === 'zh' ? '带金色标记的模型已配置' : 'Models with gold badge are already configured'}
|
||||
{t('modelConfig.modelsConfigured', language)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -310,9 +308,7 @@ function Claw402ConfigForm({
|
||||
Claw402 <span className="text-xs font-normal" style={{ color: '#60A5FA' }}>↗</span>
|
||||
</a>
|
||||
<div className="text-sm mt-1" style={{ color: '#A0AEC0' }}>
|
||||
{language === 'zh'
|
||||
? '用 USDC 按次付费,支持所有主流 AI 模型'
|
||||
: 'Pay-per-call with USDC — supports all major AI models'}
|
||||
{t('modelConfig.allModelsClaw', language)}
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-3 mt-3 flex-wrap">
|
||||
{['GPT', 'Claude', 'DeepSeek', 'Gemini', 'Grok', 'Qwen', 'Kimi'].map(name => (
|
||||
@@ -327,12 +323,10 @@ function Claw402ConfigForm({
|
||||
<div className="space-y-3">
|
||||
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||
<Brain className="w-4 h-4" style={{ color: '#2563EB' }} />
|
||||
{language === 'zh' ? '① 选择 AI 模型' : '① Choose AI Model'}
|
||||
{t('modelConfig.selectAiModel', language)}
|
||||
</label>
|
||||
<div className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{language === 'zh'
|
||||
? '所有模型通过 Claw402 统一调用,创建后可随时切换'
|
||||
: 'All models unified via Claw402. Switch anytime after setup.'}
|
||||
{t('modelConfig.allModelsUnified', language)}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
{CLAW402_MODELS.map((m) => {
|
||||
@@ -372,34 +366,28 @@ function Claw402ConfigForm({
|
||||
<svg className="w-4 h-4" style={{ color: '#2563EB' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
|
||||
</svg>
|
||||
{language === 'zh' ? '② 设置钱包' : '② Setup Wallet'}
|
||||
{t('modelConfig.setupWallet', language)}
|
||||
</label>
|
||||
|
||||
<div className="p-3 rounded-xl" style={{ background: 'rgba(37, 99, 235, 0.06)', border: '1px solid rgba(37, 99, 235, 0.15)' }}>
|
||||
<div className="text-xs mb-2" style={{ color: '#A0AEC0' }}>
|
||||
{language === 'zh'
|
||||
? '💡 Claw402 使用 Base 链上的 USDC 付费,你需要一个 EVM 钱包'
|
||||
: '💡 Claw402 uses USDC on Base chain. You need an EVM wallet.'}
|
||||
{t('modelConfig.walletInfo', language)}
|
||||
</div>
|
||||
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span style={{ color: '#00E096' }}>•</span>
|
||||
{language === 'zh'
|
||||
? '可以用 MetaMask、Rabby 等钱包导出私钥'
|
||||
: 'Export private key from MetaMask, Rabby, etc.'}
|
||||
{t('modelConfig.exportKey', language)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span style={{ color: '#00E096' }}>•</span>
|
||||
{language === 'zh'
|
||||
? '建议新建一个专用钱包,充入少量 USDC 即可'
|
||||
: 'Recommended: create a dedicated wallet with a small USDC balance'}
|
||||
{t('modelConfig.dedicatedWallet', language)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-xs font-medium" style={{ color: '#A0AEC0' }}>
|
||||
{language === 'zh' ? '钱包私钥(Base 链 EVM)' : 'Wallet Private Key (Base Chain EVM)'}
|
||||
{t('modelConfig.walletPrivateKey', language)}
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
@@ -413,9 +401,7 @@ function Claw402ConfigForm({
|
||||
<div className="flex items-start gap-1.5 text-[11px]" style={{ color: '#848E9C' }}>
|
||||
<span className="mt-px">🔒</span>
|
||||
<span>
|
||||
{language === 'zh'
|
||||
? '私钥仅在本地签名使用,不会上传或发送交易。无需 ETH,无 Gas 费用。'
|
||||
: 'Private key is only used locally for signing. Never uploaded. No ETH or gas needed.'}
|
||||
{t('modelConfig.privateKeyNote', language)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -424,20 +410,20 @@ function Claw402ConfigForm({
|
||||
{/* USDC Recharge Guide */}
|
||||
<div className="p-4 rounded-xl" style={{ background: 'rgba(0, 224, 150, 0.05)', border: '1px solid rgba(0, 224, 150, 0.15)' }}>
|
||||
<div className="text-sm font-semibold mb-2 flex items-center gap-2" style={{ color: '#00E096' }}>
|
||||
💰 {language === 'zh' ? '如何充值 USDC' : 'How to Fund USDC'}
|
||||
{'💰 ' + t('modelConfig.howToFundUsdc', language)}
|
||||
</div>
|
||||
<div className="text-xs space-y-1.5" style={{ color: '#848E9C' }}>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="font-bold" style={{ color: '#A0AEC0' }}>1.</span>
|
||||
<span>{language === 'zh' ? '从交易所(Binance / OKX / Coinbase)提 USDC 到你的钱包地址' : 'Withdraw USDC from exchange (Binance/OKX/Coinbase) to your wallet'}</span>
|
||||
<span>{t('modelConfig.fundStep1', language)}</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="font-bold" style={{ color: '#A0AEC0' }}>2.</span>
|
||||
<span>{language === 'zh' ? '选择 Base 网络(手续费极低)' : 'Select Base network (very low fees)'}</span>
|
||||
<span>{t('modelConfig.fundStep2', language)}</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="font-bold" style={{ color: '#A0AEC0' }}>3.</span>
|
||||
<span>{language === 'zh' ? '充入 $5-10 USDC 即可使用很长时间(约 $0.003/次调用)' : '$5-10 USDC lasts a long time (~$0.003/call)'}</span>
|
||||
<span>{t('modelConfig.fundStep3', language)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -445,7 +431,7 @@ function Claw402ConfigForm({
|
||||
{/* Buttons */}
|
||||
<div className="flex gap-3 pt-2">
|
||||
<button type="button" onClick={onBack} className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5" style={{ background: '#2B3139', color: '#848E9C' }}>
|
||||
{editingModelId ? t('cancel', language) : (language === 'zh' ? '返回' : 'Back')}
|
||||
{editingModelId ? t('cancel', language) : t('modelConfig.back', language)}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
@@ -453,7 +439,7 @@ function Claw402ConfigForm({
|
||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style={{ background: apiKey.trim() ? 'linear-gradient(135deg, #2563EB, #7C3AED)' : '#2B3139', color: '#fff' }}
|
||||
>
|
||||
{language === 'zh' ? '🚀 开始交易' : '🚀 Start Trading'}
|
||||
{'🚀 ' + t('modelConfig.startTrading', language)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -513,8 +499,8 @@ function StandardProviderConfigForm({
|
||||
<ExternalLink className="w-4 h-4" style={{ color: '#A78BFA' }} />
|
||||
<span className="text-sm font-medium" style={{ color: '#A78BFA' }}>
|
||||
{selectedModel.provider?.startsWith('blockrun')
|
||||
? (language === 'zh' ? '开始使用' : 'Get Started')
|
||||
: (language === 'zh' ? '获取 API Key' : 'Get API Key')}
|
||||
? t('modelConfig.getStarted', language)
|
||||
: t('modelConfig.getApiKey', language)}
|
||||
</span>
|
||||
</a>
|
||||
)}
|
||||
@@ -539,7 +525,7 @@ function StandardProviderConfigForm({
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
</svg>
|
||||
{selectedModel.provider?.startsWith('blockrun')
|
||||
? (language === 'zh' ? '钱包私钥 *' : 'Wallet Private Key *')
|
||||
? t('modelConfig.walletPrivateKeyLabel', language)
|
||||
: 'API Key *'}
|
||||
</label>
|
||||
<input
|
||||
@@ -612,7 +598,7 @@ function StandardProviderConfigForm({
|
||||
<svg className="w-4 h-4" style={{ color: '#A78BFA' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
{language === 'zh' ? '选择模型' : 'Select Model'}
|
||||
{t('modelConfig.selectModelLabel', language)}
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{BLOCKRUN_MODELS.map((m) => {
|
||||
@@ -655,7 +641,7 @@ function StandardProviderConfigForm({
|
||||
{/* Buttons */}
|
||||
<div className="flex gap-3 pt-4">
|
||||
<button type="button" onClick={onBack} className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5" style={{ background: '#2B3139', color: '#848E9C' }}>
|
||||
{editingModelId ? t('cancel', language) : (language === 'zh' ? '返回' : 'Back')}
|
||||
{editingModelId ? t('cancel', language) : t('modelConfig.back', language)}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Check, ChevronLeft, ExternalLink, MessageCircle, Unlink, ArrowRight } f
|
||||
import { toast } from 'sonner'
|
||||
import { api } from '../../lib/api'
|
||||
import type { TelegramConfig, AIModel } from '../../types'
|
||||
import type { Language } from '../../i18n/translations'
|
||||
import { t, type Language } from '../../i18n/translations'
|
||||
|
||||
// Step indicator (reused pattern from ExchangeConfigModal)
|
||||
function StepIndicator({ currentStep, labels }: { currentStep: number; labels: string[] }) {
|
||||
@@ -55,8 +55,6 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isUnbinding, setIsUnbinding] = useState(false)
|
||||
|
||||
const zh = language === 'zh'
|
||||
|
||||
// Load current config and available models
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
@@ -84,20 +82,20 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
|
||||
// Basic format validation: looks like "123456789:ABCdef..."
|
||||
if (!/^\d+:[A-Za-z0-9_-]{35,}$/.test(token.trim())) {
|
||||
toast.error(zh ? 'Bot Token 格式不正确,应为 "数字:字母数字串"' : 'Invalid Bot Token format. Expected "numbers:alphanumeric"')
|
||||
toast.error(t('telegram.invalidTokenFormat', language))
|
||||
return
|
||||
}
|
||||
|
||||
setIsSaving(true)
|
||||
try {
|
||||
await api.updateTelegramConfig(token.trim(), selectedModelId || undefined)
|
||||
toast.success(zh ? 'Bot Token 已保存,等待绑定' : 'Bot Token saved, waiting for binding')
|
||||
toast.success(t('telegram.tokenSaved', language))
|
||||
const updated = await api.getTelegramConfig()
|
||||
setConfig(updated)
|
||||
setToken('')
|
||||
setStep(1)
|
||||
} catch (err) {
|
||||
toast.error(zh ? '保存失败,请检查 Token 是否正确' : 'Save failed, please verify the token')
|
||||
toast.error(t('telegram.saveFailed', language))
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
@@ -108,33 +106,31 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
setIsUnbinding(true)
|
||||
try {
|
||||
await api.unbindTelegram()
|
||||
toast.success(zh ? '已解绑 Telegram 账号' : 'Telegram account unbound')
|
||||
toast.success(t('telegram.unbound', language))
|
||||
const updated = await api.getTelegramConfig()
|
||||
setConfig(updated)
|
||||
setStep(updated.token_masked ? 1 : 0)
|
||||
} catch {
|
||||
toast.error(zh ? '解绑失败' : 'Unbind failed')
|
||||
toast.error(t('telegram.unbindFailed', language))
|
||||
} finally {
|
||||
setIsUnbinding(false)
|
||||
}
|
||||
}
|
||||
|
||||
const stepLabels = zh
|
||||
? ['创建 Bot', '绑定账号', '完成']
|
||||
: ['Create Bot', 'Bind Account', 'Done']
|
||||
const stepLabels = [t('telegram.createBot', language), t('telegram.bindAccount', language), t('telegram.done', language)]
|
||||
|
||||
// Model selector shared between steps
|
||||
const ModelSelector = () => (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||
{zh ? '选择 AI 模型(可选)' : 'Select AI Model (optional)'}
|
||||
{t('telegram.selectAiModel', language)}
|
||||
</label>
|
||||
{models.length === 0 ? (
|
||||
<div
|
||||
className="px-4 py-3 rounded-xl text-xs"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#848E9C' }}
|
||||
>
|
||||
{zh ? '暂无启用的模型,请先在「AI 模型」中配置' : 'No enabled models. Configure one in AI Models first.'}
|
||||
{t('telegram.noEnabledModels', language)}
|
||||
</div>
|
||||
) : (
|
||||
<select
|
||||
@@ -147,7 +143,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
color: selectedModelId ? '#EAECEF' : '#848E9C',
|
||||
}}
|
||||
>
|
||||
<option value="">{zh ? '— 自动选择(推荐)' : '— Auto-select (recommended)'}</option>
|
||||
<option value="">{t('telegram.autoSelect', language)}</option>
|
||||
{models.map((m) => (
|
||||
<option key={m.id} value={m.id}>
|
||||
{m.name} ({m.provider}{m.customModelName ? ` · ${m.customModelName}` : ''})
|
||||
@@ -156,9 +152,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
</select>
|
||||
)}
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{zh
|
||||
? '不选则自动使用已启用的模型'
|
||||
: 'Leave blank to auto-use any enabled model'}
|
||||
{t('telegram.autoUseEnabled', language)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -184,7 +178,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
<div className="flex items-center gap-2">
|
||||
<MessageCircle className="w-6 h-6" style={{ color: '#2AABEE' }} />
|
||||
<h3 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
|
||||
{zh ? 'Telegram Bot 配置' : 'Telegram Bot Setup'}
|
||||
{t('telegram.botSetup', language)}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
@@ -207,7 +201,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
<div className="px-6 pb-6 space-y-5">
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8 text-zinc-500 text-sm font-mono">
|
||||
{zh ? '加载中...' : 'Loading...'}
|
||||
{t('telegram.loading', language)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -222,13 +216,13 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
<span className="text-2xl">🤖</span>
|
||||
<div>
|
||||
<div className="font-semibold mb-1" style={{ color: '#2AABEE' }}>
|
||||
{zh ? '第一步:在 Telegram 创建你的 Bot' : 'Step 1: Create your Bot in Telegram'}
|
||||
{t('telegram.step1Title', language)}
|
||||
</div>
|
||||
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
|
||||
<div>1. {zh ? '打开 Telegram,搜索' : 'Open Telegram, search for'} <code className="text-blue-400">@BotFather</code></div>
|
||||
<div>2. {zh ? '发送' : 'Send'} <code className="text-blue-400">/newbot</code> {zh ? '命令' : 'command'}</div>
|
||||
<div>3. {zh ? '按提示输入 Bot 名称和用户名' : 'Follow prompts to set bot name and username'}</div>
|
||||
<div>4. {zh ? 'BotFather 会返回一个 Token,复制它' : 'BotFather will return a Token, copy it'}</div>
|
||||
<div>1. {t('telegram.step1Desc1', language)} <code className="text-blue-400">@BotFather</code></div>
|
||||
<div>2. {t('telegram.step1Desc2', language)} <code className="text-blue-400">/newbot</code> {t('telegram.step1Desc2Suffix', language)}</div>
|
||||
<div>3. {t('telegram.step1Desc3', language)}</div>
|
||||
<div>4. {t('telegram.step1Desc4', language)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -242,12 +236,12 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
style={{ background: '#2AABEE', color: '#000' }}
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
{zh ? '打开 @BotFather' : 'Open @BotFather'}
|
||||
{t('telegram.openBotFather', language)}
|
||||
</a>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||
{zh ? '粘贴 Bot Token' : 'Paste Bot Token'}
|
||||
{t('telegram.pasteToken', language)}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
@@ -258,7 +252,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
/>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{zh ? 'Token 格式:数字:字母数字串,如 123456789:ABCdef...' : 'Format: numbers:alphanumeric, e.g. 123456789:ABCdef...'}
|
||||
{t('telegram.tokenFormat', language)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -271,8 +265,8 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
style={{ background: '#2AABEE', color: '#000' }}
|
||||
>
|
||||
{isSaving
|
||||
? (zh ? '保存中...' : 'Saving...')
|
||||
: (<>{zh ? '保存并继续' : 'Save & Continue'} <ArrowRight className="w-4 h-4" /></>)
|
||||
? t('telegram.savingToken', language)
|
||||
: (<>{t('telegram.saveAndContinue', language)} <ArrowRight className="w-4 h-4" /></>)
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
@@ -289,12 +283,12 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
<span className="text-2xl">📱</span>
|
||||
<div>
|
||||
<div className="font-semibold mb-1" style={{ color: '#0ECB81' }}>
|
||||
{zh ? '第二步:向你的 Bot 发送 /start' : 'Step 2: Send /start to your Bot'}
|
||||
{t('telegram.step2Title', language)}
|
||||
</div>
|
||||
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
|
||||
<div>1. {zh ? '在 Telegram 中搜索你刚创建的 Bot' : 'Search for your newly created Bot in Telegram'}</div>
|
||||
<div>2. {zh ? '点击 Start 或发送' : 'Click Start or send'} <code className="text-green-400">/start</code></div>
|
||||
<div>3. {zh ? 'Bot 会自动绑定到你的账号' : 'Bot will automatically bind to your account'}</div>
|
||||
<div>1. {t('telegram.step2Desc1', language)}</div>
|
||||
<div>2. {t('telegram.step2Desc2', language)} <code className="text-green-400">/start</code></div>
|
||||
<div>3. {t('telegram.step2Desc3', language)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -308,7 +302,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
<div className="w-2 h-2 rounded-full bg-yellow-500 animate-pulse flex-shrink-0" />
|
||||
<div>
|
||||
<div className="text-xs font-mono" style={{ color: '#848E9C' }}>
|
||||
{zh ? '当前 Token' : 'Current Token'}
|
||||
{t('telegram.currentToken', language)}
|
||||
</div>
|
||||
<div className="text-sm font-mono" style={{ color: '#EAECEF' }}>
|
||||
{config.token_masked}
|
||||
@@ -322,9 +316,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
style={{ background: 'rgba(240, 185, 11, 0.08)', border: '1px solid rgba(240, 185, 11, 0.2)' }}
|
||||
>
|
||||
<div className="text-xs" style={{ color: '#F0B90B' }}>
|
||||
{zh
|
||||
? '⏳ 等待你发送 /start... 发送后刷新页面查看状态'
|
||||
: '⏳ Waiting for you to send /start... Refresh page after sending'}
|
||||
{t('telegram.waitingForStart', language)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -334,7 +326,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5"
|
||||
style={{ background: '#2B3139', color: '#848E9C' }}
|
||||
>
|
||||
{zh ? '重新配置 Token' : 'Reconfigure Token'}
|
||||
{t('telegram.reconfigureToken', language)}
|
||||
</button>
|
||||
<button
|
||||
onClick={async () => {
|
||||
@@ -343,19 +335,19 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
setConfig(updated)
|
||||
if (updated.is_bound) {
|
||||
setStep(2)
|
||||
toast.success(zh ? '绑定成功!' : 'Bound successfully!')
|
||||
toast.success(t('telegram.bindSuccess', language))
|
||||
} else {
|
||||
toast.info(zh ? '尚未收到 /start,请先向 Bot 发送 /start' : 'No /start received yet. Please send /start to your Bot first')
|
||||
toast.info(t('telegram.noStartReceived', language))
|
||||
}
|
||||
} catch {
|
||||
toast.error(zh ? '检查失败' : 'Check failed')
|
||||
toast.error(t('telegram.checkFailed', language))
|
||||
}
|
||||
}}
|
||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02]"
|
||||
style={{ background: '#0ECB81', color: '#000' }}
|
||||
>
|
||||
<Check className="w-4 h-4" />
|
||||
{zh ? '检查绑定状态' : 'Check Status'}
|
||||
{t('telegram.checkStatus', language)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -370,12 +362,10 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
>
|
||||
<div className="text-4xl">🎉</div>
|
||||
<div className="font-bold text-lg" style={{ color: '#0ECB81' }}>
|
||||
{zh ? 'Telegram Bot 已绑定!' : 'Telegram Bot is Active!'}
|
||||
{t('telegram.botActive', language)}
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{zh
|
||||
? '你现在可以通过 Telegram 用自然语言控制交易系统'
|
||||
: 'You can now control the trading system via natural language in Telegram'}
|
||||
{t('telegram.botActiveDesc', language)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -387,7 +377,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
<div className="w-2 h-2 rounded-full bg-green-500 flex-shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs font-mono" style={{ color: '#848E9C' }}>
|
||||
{zh ? 'Bot Token' : 'Bot Token'}
|
||||
Bot Token
|
||||
</div>
|
||||
<div className="text-sm font-mono truncate" style={{ color: '#EAECEF' }}>
|
||||
{config.token_masked}
|
||||
@@ -398,7 +388,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
|
||||
{/* AI Model selector — works on active bot */}
|
||||
<BoundModelSelector
|
||||
zh={zh}
|
||||
language={language}
|
||||
models={models}
|
||||
currentModelId={config?.model_id ?? ''}
|
||||
onSaved={(modelId) => {
|
||||
@@ -412,14 +402,14 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||
>
|
||||
<div className="text-xs font-semibold uppercase tracking-wide mb-2" style={{ color: '#848E9C' }}>
|
||||
{zh ? '支持的命令' : 'Supported Commands'}
|
||||
{t('telegram.supportedCommands', language)}
|
||||
</div>
|
||||
{[
|
||||
{ cmd: '/help', desc: zh ? '查看所有命令' : 'Show all commands' },
|
||||
{ cmd: zh ? '查看交易员状态' : 'Show trader status', desc: zh ? '自然语言查询' : 'Natural language' },
|
||||
{ cmd: zh ? '启动/停止交易员' : 'Start/stop trader', desc: zh ? '自然语言控制' : 'Natural language control' },
|
||||
{ cmd: zh ? '查看持仓' : 'View positions', desc: zh ? '实时持仓查询' : 'Real-time position query' },
|
||||
{ cmd: zh ? '配置策略' : 'Configure strategy', desc: zh ? '修改交易策略' : 'Modify trading strategy' },
|
||||
{ cmd: '/help', desc: t('telegram.cmdHelp', language) },
|
||||
{ cmd: t('telegram.cmdStatus', language), desc: t('telegram.cmdNaturalLang', language) },
|
||||
{ cmd: t('telegram.cmdStartStop', language), desc: t('telegram.cmdControl', language) },
|
||||
{ cmd: t('telegram.cmdPositions', language), desc: t('telegram.cmdPositionsDesc', language) },
|
||||
{ cmd: t('telegram.cmdStrategy', language), desc: t('telegram.cmdStrategyDesc', language) },
|
||||
].map((item, i) => (
|
||||
<div key={i} className="flex items-start gap-2 text-xs">
|
||||
<code className="font-mono px-1.5 py-0.5 rounded flex-shrink-0" style={{ background: '#1E2329', color: '#2AABEE' }}>
|
||||
@@ -438,14 +428,14 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D', border: '1px solid rgba(246, 70, 93, 0.2)' }}
|
||||
>
|
||||
<Unlink className="w-4 h-4" />
|
||||
{isUnbinding ? (zh ? '解绑中...' : 'Unbinding...') : (zh ? '解绑账号' : 'Unbind Account')}
|
||||
{isUnbinding ? t('telegram.unbinding', language) : t('telegram.unbindAccount', language)}
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex-1 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02]"
|
||||
style={{ background: '#2AABEE', color: '#000' }}
|
||||
>
|
||||
{zh ? '完成' : 'Done'}
|
||||
{t('telegram.done', language)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -461,12 +451,12 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
// BoundModelSelector — lets the user change the AI model when the bot is already active.
|
||||
// It updates the model_id without requiring re-entry of the bot token.
|
||||
function BoundModelSelector({
|
||||
zh,
|
||||
language,
|
||||
models,
|
||||
currentModelId,
|
||||
onSaved,
|
||||
}: {
|
||||
zh: boolean
|
||||
language: Language
|
||||
models: AIModel[]
|
||||
currentModelId: string
|
||||
onSaved: (modelId: string) => void
|
||||
@@ -483,9 +473,9 @@ function BoundModelSelector({
|
||||
// POST /api/telegram/model — lightweight endpoint for model-only update
|
||||
await api.updateTelegramModel(modelId)
|
||||
onSaved(modelId)
|
||||
toast.success(zh ? 'AI 模型已更新' : 'AI model updated')
|
||||
toast.success(t('telegram.modelUpdated', language))
|
||||
} catch {
|
||||
toast.error(zh ? '更新失败' : 'Update failed')
|
||||
toast.error(t('telegram.modelUpdateFailed', language))
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
@@ -496,7 +486,7 @@ function BoundModelSelector({
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||
{zh ? 'AI 模型(用于自然语言解析)' : 'AI Model (for natural language)'}
|
||||
{t('telegram.aiModelLabel', language)}
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
@@ -509,7 +499,7 @@ function BoundModelSelector({
|
||||
color: modelId ? '#EAECEF' : '#848E9C',
|
||||
}}
|
||||
>
|
||||
<option value="">{zh ? '— 自动选择' : '— Auto-select'}</option>
|
||||
<option value="">{t('telegram.aiModelAutoSelect', language)}</option>
|
||||
{models.map((m) => (
|
||||
<option key={m.id} value={m.id}>
|
||||
{m.name}{m.customModelName ? ` · ${m.customModelName}` : ''}
|
||||
@@ -522,7 +512,7 @@ function BoundModelSelector({
|
||||
className="px-4 py-2.5 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
style={{ background: '#F0B90B', color: '#000', whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{isSaving ? '...' : (zh ? '保存' : 'Save')}
|
||||
{isSaving ? '...' : t('telegram.save', language)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { TraderConfigData } from '../../types'
|
||||
import { t } from '../../i18n/translations'
|
||||
import { useLanguage } from '../../contexts/LanguageContext'
|
||||
import { PunkAvatar, getTraderAvatar } from '../common/PunkAvatar'
|
||||
|
||||
// 提取下划线后面的名称部分
|
||||
// Extract the name part after the last underscore
|
||||
function getShortName(fullName: string): string {
|
||||
const parts = fullName.split('_')
|
||||
return parts.length > 1 ? parts[parts.length - 1] : fullName
|
||||
@@ -18,6 +20,7 @@ export function TraderConfigViewModal({
|
||||
onClose,
|
||||
traderData,
|
||||
}: TraderConfigViewModalProps) {
|
||||
const { language } = useLanguage()
|
||||
if (!isOpen || !traderData) return null
|
||||
|
||||
const InfoRow = ({
|
||||
@@ -30,7 +33,7 @@ export function TraderConfigViewModal({
|
||||
<div className="flex justify-between items-start py-2 border-b border-[#2B3139] last:border-b-0">
|
||||
<span className="text-sm text-[#848E9C] font-medium">{label}</span>
|
||||
<span className="text-sm text-[#EAECEF] font-mono text-right">
|
||||
{typeof value === 'boolean' ? (value ? '是' : '否') : value}
|
||||
{typeof value === 'boolean' ? (value ? t('traderConfigView.yes', language) : t('traderConfigView.no', language)) : value}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
@@ -50,9 +53,9 @@ export function TraderConfigViewModal({
|
||||
className="rounded-lg"
|
||||
/>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-[#EAECEF]">交易员配置</h2>
|
||||
<h2 className="text-xl font-bold text-[#EAECEF]">{t('traderConfigView.traderConfig', language)}</h2>
|
||||
<p className="text-sm text-[#848E9C] mt-1">
|
||||
{traderData.trader_name} 的配置信息
|
||||
{t('traderConfigView.configInfo', language, { name: traderData.trader_name })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,7 +70,7 @@ export function TraderConfigViewModal({
|
||||
}
|
||||
>
|
||||
<span>{traderData.is_running ? '●' : '○'}</span>
|
||||
{traderData.is_running ? '运行中' : '已停止'}
|
||||
{traderData.is_running ? t('traderConfigView.running', language) : t('traderConfigView.stopped', language)}
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
@@ -83,32 +86,32 @@ export function TraderConfigViewModal({
|
||||
{/* Basic Info */}
|
||||
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
|
||||
<h3 className="text-lg font-semibold text-[#EAECEF] mb-4 flex items-center gap-2">
|
||||
🤖 基础信息
|
||||
{'🤖 ' + t('traderConfigView.basicInfo', language)}
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<InfoRow
|
||||
label="交易员名称"
|
||||
label={t('traderConfigView.traderName', language)}
|
||||
value={traderData.trader_name}
|
||||
/>
|
||||
<InfoRow
|
||||
label="AI模型"
|
||||
label={t('traderConfigView.aiModel', language)}
|
||||
value={getShortName(traderData.ai_model).toUpperCase()}
|
||||
/>
|
||||
<InfoRow
|
||||
label="交易所"
|
||||
label={t('traderConfigView.exchange', language)}
|
||||
value={getShortName(traderData.exchange_id).toUpperCase()}
|
||||
/>
|
||||
<InfoRow
|
||||
label="初始余额"
|
||||
label={t('traderConfigView.initialBalance', language)}
|
||||
value={`$${traderData.initial_balance.toLocaleString()}`}
|
||||
/>
|
||||
<InfoRow
|
||||
label="保证金模式"
|
||||
value={traderData.is_cross_margin ? '全仓' : '逐仓'}
|
||||
label={t('traderConfigView.marginMode', language)}
|
||||
value={traderData.is_cross_margin ? t('traderConfigView.crossMargin', language) : t('traderConfigView.isolatedMargin', language)}
|
||||
/>
|
||||
<InfoRow
|
||||
label="扫描间隔"
|
||||
value={`${traderData.scan_interval_minutes || 3} 分钟`}
|
||||
label={t('traderConfigView.scanIntervalLabel', language)}
|
||||
value={t('traderConfigView.scanInterval', language, { minutes: traderData.scan_interval_minutes || 3 })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -117,11 +120,11 @@ export function TraderConfigViewModal({
|
||||
{traderData.strategy_id && (
|
||||
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
|
||||
<h3 className="text-lg font-semibold text-[#EAECEF] mb-4 flex items-center gap-2">
|
||||
📋 使用策略
|
||||
{'📋 ' + t('traderConfigView.strategyUsed', language)}
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<InfoRow
|
||||
label="策略名称"
|
||||
label={t('traderConfigView.strategyName', language)}
|
||||
value={traderData.strategy_name || traderData.strategy_id}
|
||||
/>
|
||||
</div>
|
||||
@@ -135,7 +138,7 @@ export function TraderConfigViewModal({
|
||||
onClick={onClose}
|
||||
className="px-6 py-3 bg-[#2B3139] text-[#EAECEF] rounded-lg hover:bg-[#404750] transition-all duration-200 border border-[#404750]"
|
||||
>
|
||||
关闭
|
||||
{t('traderConfigView.close', language)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user