refactor: optimize codebase encoding

This commit is contained in:
tinkle-community
2026-03-12 16:12:08 +08:00
parent 2314ece9d1
commit 736d2d385d
61 changed files with 2301 additions and 1533 deletions

View File

@@ -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),
})
}

View File

@@ -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>

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>