refactor(web): restructure AITradersPage into modular architecture (#1023)

* refactor(web): restructure AITradersPage into modular architecture
Refactored the massive 2652-line AITradersPage.tsx into a clean, modular architecture following React best practices.
**Changes:**
- Decomposed 2652-line component into 12 focused modules
- Introduced Zustand stores for config and modal state management
- Extracted all business logic into useTraderActions custom hook (633 lines)
- Created reusable section components (PageHeader, TradersGrid, etc.)
- Separated complex modal logic into dedicated components
- Added TraderConfig type, eliminated all any types
- Fixed critical bugs in configuredExchanges logic and getState() usage
**File Structure:**
- Main page reduced from 2652 → 234 lines (91% reduction)
- components/traders/: 7 UI components + 5 section components
- stores/: tradersConfigStore, tradersModalStore
- hooks/: useTraderActions (all business logic)
Co-Authored-By: tinkle-community <tinklefund@gmail.com>
* chore: ignore PR_DESCRIPTION.md
* fix(web): restore trader dashboard navigation functionality
Fixed missing navigation logic in refactored AITradersPage. The "查看" (View) button now correctly navigates to the trader dashboard.
**Root Cause:**
During refactoring, the `useNavigate` hook and default navigation logic were inadvertently omitted from the main page component.
**Changes:**
- Added `useNavigate` import from react-router-dom
- Implemented `handleTraderSelect` function with fallback navigation
- Restored original behavior: use `onTraderSelect` prop if provided, otherwise navigate to `/dashboard?trader=${traderId}`
**Testing:**
-  Click "查看" button navigates to trader dashboard
-  Query parameter correctly passed to dashboard
Co-Authored-By: tinkle-community <tinklefund@gmail.com>
* fix(web): correct type definitions for trader configuration
Fixed TypeScript build errors by using the correct `TraderConfigData` type instead of the incorrect `TraderConfig` type.
**Root Cause:**
During refactoring, a new `TraderConfig` type was incorrectly created that extended `CreateTraderRequest` (with fields like `name`, `ai_model_id`). However, the `TraderConfigModal` component and API responses actually use `TraderConfigData` (with fields like `trader_name`, `ai_model`).
**Changes:**
- Replaced all `TraderConfig` references with `TraderConfigData`:
  - stores/tradersModalStore.ts
  - hooks/useTraderActions.ts
  - lib/api.ts
- Removed incorrect `TraderConfig` type definition from types.ts
- Added null check for `editingTrader.trader_id` to satisfy TypeScript
**Build Status:**
-  TypeScript compilation: PASS
-  Vite production build: PASS
Co-Authored-By: tinkle-community <tinklefund@gmail.com>
---------
Co-authored-by: tinkle-community <tinklefund@gmail.com>
This commit is contained in:
Ember
2025-11-15 12:33:48 +08:00
committed by tangmengqiu
parent 7eab056394
commit c64d4ff549
19 changed files with 3050 additions and 21 deletions

View File

@@ -0,0 +1,926 @@
import React, { useState, useEffect } from 'react'
import type { Exchange } from '../../types'
import { t, type Language } from '../../i18n/translations'
import { api } from '../../lib/api'
import { getExchangeIcon } from '../ExchangeIcons'
import {
TwoStageKeyModal,
type TwoStageKeyModalResult,
} from '../TwoStageKeyModal'
import {
WebCryptoEnvironmentCheck,
type WebCryptoCheckStatus,
} from '../WebCryptoEnvironmentCheck'
import { BookOpen, Trash2, HelpCircle } from 'lucide-react'
import { toast } from 'sonner'
import { Tooltip } from './Tooltip'
import { getShortName } from './utils'
interface ExchangeConfigModalProps {
allExchanges: Exchange[]
editingExchangeId: string | null
onSave: (
exchangeId: string,
apiKey: string,
secretKey?: string,
testnet?: boolean,
hyperliquidWalletAddr?: string,
asterUser?: string,
asterSigner?: string,
asterPrivateKey?: string
) => Promise<void>
onDelete: (exchangeId: string) => void
onClose: () => void
language: Language
}
export function ExchangeConfigModal({
allExchanges,
editingExchangeId,
onSave,
onDelete,
onClose,
language,
}: ExchangeConfigModalProps) {
const [selectedExchangeId, setSelectedExchangeId] = useState(
editingExchangeId || ''
)
const [apiKey, setApiKey] = useState('')
const [secretKey, setSecretKey] = useState('')
const [passphrase, setPassphrase] = useState('')
const [testnet, setTestnet] = useState(false)
const [showGuide, setShowGuide] = useState(false)
const [serverIP, setServerIP] = useState<{
public_ip: string
message: string
} | null>(null)
const [loadingIP, setLoadingIP] = useState(false)
const [copiedIP, setCopiedIP] = useState(false)
const [webCryptoStatus, setWebCryptoStatus] =
useState<WebCryptoCheckStatus>('idle')
// 币安配置指南展开状态
const [showBinanceGuide, setShowBinanceGuide] = useState(false)
// Aster 特定字段
const [asterUser, setAsterUser] = useState('')
const [asterSigner, setAsterSigner] = useState('')
const [asterPrivateKey, setAsterPrivateKey] = useState('')
// Hyperliquid 特定字段
const [hyperliquidWalletAddr, setHyperliquidWalletAddr] = useState('')
// 安全输入状态
const [secureInputTarget, setSecureInputTarget] = useState<
null | 'hyperliquid' | 'aster'
>(null)
// 获取当前编辑的交易所信息
const selectedExchange = allExchanges?.find(
(e) => e.id === selectedExchangeId
)
// 如果是编辑现有交易所,初始化表单数据
useEffect(() => {
if (editingExchangeId && selectedExchange) {
setApiKey(selectedExchange.apiKey || '')
setSecretKey(selectedExchange.secretKey || '')
setPassphrase('') // Don't load existing passphrase for security
setTestnet(selectedExchange.testnet || false)
// Aster 字段
setAsterUser(selectedExchange.asterUser || '')
setAsterSigner(selectedExchange.asterSigner || '')
setAsterPrivateKey('') // Don't load existing private key for security
// Hyperliquid 字段
setHyperliquidWalletAddr(selectedExchange.hyperliquidWalletAddr || '')
}
}, [editingExchangeId, selectedExchange])
// 加载服务器IP当选择binance时
useEffect(() => {
if (selectedExchangeId === 'binance' && !serverIP) {
setLoadingIP(true)
api
.getServerIP()
.then((data) => {
setServerIP(data)
})
.catch((err) => {
console.error('Failed to load server IP:', err)
})
.finally(() => {
setLoadingIP(false)
})
}
}, [selectedExchangeId])
const handleCopyIP = async (ip: string) => {
try {
// 优先使用现代 Clipboard API
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(ip)
setCopiedIP(true)
setTimeout(() => setCopiedIP(false), 2000)
toast.success(t('ipCopied', language))
} else {
// 降级方案: 使用传统的 execCommand 方法
const textArea = document.createElement('textarea')
textArea.value = ip
textArea.style.position = 'fixed'
textArea.style.left = '-999999px'
textArea.style.top = '-999999px'
document.body.appendChild(textArea)
textArea.focus()
textArea.select()
try {
const successful = document.execCommand('copy')
if (successful) {
setCopiedIP(true)
setTimeout(() => setCopiedIP(false), 2000)
toast.success(t('ipCopied', language))
} else {
throw new Error('复制命令执行失败')
}
} finally {
document.body.removeChild(textArea)
}
}
} catch (err) {
console.error('复制失败:', err)
// 显示错误提示
toast.error(
t('copyIPFailed', language) || `复制失败: ${ip}\n请手动复制此IP地址`
)
}
}
// 安全输入处理函数
const secureInputContextLabel =
secureInputTarget === 'aster'
? t('asterExchangeName', language)
: secureInputTarget === 'hyperliquid'
? t('hyperliquidExchangeName', language)
: undefined
const handleSecureInputCancel = () => {
setSecureInputTarget(null)
}
const handleSecureInputComplete = ({
value,
obfuscationLog,
}: TwoStageKeyModalResult) => {
const trimmed = value.trim()
if (secureInputTarget === 'hyperliquid') {
setApiKey(trimmed)
}
if (secureInputTarget === 'aster') {
setAsterPrivateKey(trimmed)
}
console.log('Secure input obfuscation log:', obfuscationLog)
setSecureInputTarget(null)
}
// 掩盖敏感数据显示
const maskSecret = (secret: string) => {
if (!secret || secret.length === 0) return ''
if (secret.length <= 8) return '*'.repeat(secret.length)
return (
secret.slice(0, 4) +
'*'.repeat(Math.max(secret.length - 8, 4)) +
secret.slice(-4)
)
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!selectedExchangeId) return
// 根据交易所类型验证不同字段
if (selectedExchange?.id === 'binance') {
if (!apiKey.trim() || !secretKey.trim()) return
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet)
} else if (selectedExchange?.id === 'hyperliquid') {
if (!apiKey.trim() || !hyperliquidWalletAddr.trim()) return // 验证私钥和钱包地址
await onSave(
selectedExchangeId,
apiKey.trim(),
'',
testnet,
hyperliquidWalletAddr.trim()
)
} else if (selectedExchange?.id === 'aster') {
if (!asterUser.trim() || !asterSigner.trim() || !asterPrivateKey.trim())
return
await onSave(
selectedExchangeId,
'',
'',
testnet,
undefined,
asterUser.trim(),
asterSigner.trim(),
asterPrivateKey.trim()
)
} else if (selectedExchange?.id === 'okx') {
if (!apiKey.trim() || !secretKey.trim() || !passphrase.trim()) return
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet)
} else {
// 默认情况其他CEX交易所
if (!apiKey.trim() || !secretKey.trim()) return
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet)
}
}
// 可选择的交易所列表(所有支持的交易所)
const availableExchanges = allExchanges || []
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4 overflow-y-auto">
<div
className="bg-gray-800 rounded-lg w-full max-w-lg relative my-8"
style={{
background: '#1E2329',
maxHeight: 'calc(100vh - 4rem)',
}}
>
<div
className="flex items-center justify-between p-6 pb-4 sticky top-0 z-10"
style={{ background: '#1E2329' }}
>
<h3 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
{editingExchangeId
? t('editExchange', language)
: t('addExchange', language)}
</h3>
<div className="flex items-center gap-2">
{selectedExchange?.id === 'binance' && (
<button
type="button"
onClick={() => setShowGuide(true)}
className="px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105 flex items-center gap-2"
style={{
background: 'rgba(240, 185, 11, 0.1)',
color: '#F0B90B',
}}
>
<BookOpen className="w-4 h-4" />
{t('viewGuide', language)}
</button>
)}
{editingExchangeId && (
<button
type="button"
onClick={() => onDelete(editingExchangeId)}
className="p-2 rounded hover:bg-red-100 transition-colors"
style={{
background: 'rgba(246, 70, 93, 0.1)',
color: '#F6465D',
}}
title={t('delete', language)}
>
<Trash2 className="w-4 h-4" />
</button>
)}
</div>
</div>
<form onSubmit={handleSubmit} className="px-6 pb-6">
<div
className="space-y-4 overflow-y-auto"
style={{ maxHeight: 'calc(100vh - 16rem)' }}
>
{!editingExchangeId && (
<div className="space-y-3">
<div className="space-y-2">
<div
className="text-xs font-semibold uppercase tracking-wide"
style={{ color: '#F0B90B' }}
>
{t('environmentSteps.checkTitle', language)}
</div>
<WebCryptoEnvironmentCheck
language={language}
variant="card"
onStatusChange={setWebCryptoStatus}
/>
</div>
<div className="space-y-2">
<div
className="text-xs font-semibold uppercase tracking-wide"
style={{ color: '#F0B90B' }}
>
{t('environmentSteps.selectTitle', language)}
</div>
<select
value={selectedExchangeId}
onChange={(e) => setSelectedExchangeId(e.target.value)}
className="w-full px-3 py-2 rounded"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
}}
aria-label={t('selectExchange', language)}
disabled={webCryptoStatus !== 'secure'}
required
>
<option value="">
{t('pleaseSelectExchange', language)}
</option>
{availableExchanges.map((exchange) => (
<option key={exchange.id} value={exchange.id}>
{getShortName(exchange.name)} (
{exchange.type.toUpperCase()})
</option>
))}
</select>
</div>
</div>
)}
{selectedExchange && (
<div
className="p-4 rounded"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
>
<div className="flex items-center gap-3 mb-3">
<div className="w-8 h-8 flex items-center justify-center">
{getExchangeIcon(selectedExchange.id, {
width: 32,
height: 32,
})}
</div>
<div>
<div className="font-semibold" style={{ color: '#EAECEF' }}>
{getShortName(selectedExchange.name)}
</div>
<div className="text-xs" style={{ color: '#848E9C' }}>
{selectedExchange.type.toUpperCase()} {' '}
{selectedExchange.id}
</div>
</div>
</div>
</div>
)}
{selectedExchange && (
<>
{/* Binance 和其他 CEX 交易所的字段 */}
{(selectedExchange.id === 'binance' ||
selectedExchange.type === 'cex') &&
selectedExchange.id !== 'hyperliquid' &&
selectedExchange.id !== 'aster' && (
<>
{/* 币安用户配置提示 (D1 方案) */}
{selectedExchange.id === 'binance' && (
<div
className="mb-4 p-3 rounded cursor-pointer transition-colors"
style={{
background: '#1a3a52',
border: '1px solid #2b5278',
}}
onClick={() => setShowBinanceGuide(!showBinanceGuide)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span style={{ color: '#58a6ff' }}></span>
<span
className="text-sm font-medium"
style={{ color: '#EAECEF' }}
>
<strong></strong>
使API
API
</span>
</div>
<span style={{ color: '#8b949e' }}>
{showBinanceGuide ? '▲' : '▼'}
</span>
</div>
{/* 展开的详细说明 */}
{showBinanceGuide && (
<div
className="mt-3 pt-3"
style={{
borderTop: '1px solid #2b5278',
fontSize: '0.875rem',
color: '#c9d1d9',
}}
onClick={(e) => e.stopPropagation()}
>
<p className="mb-2" style={{ color: '#8b949e' }}>
<strong></strong> API
</p>
<p
className="font-semibold mb-1"
style={{ color: '#EAECEF' }}
>
</p>
<ol
className="list-decimal list-inside space-y-1 mb-3"
style={{ paddingLeft: '0.5rem' }}
>
<li>
{' '}
<strong>API </strong>
</li>
<li>
API
<strong> API </strong>
</li>
<li>
<strong></strong>
<span style={{ color: '#f85149' }}>
</span>
</li>
<li>
IP <strong></strong>
IP
</li>
</ol>
<p
className="mb-2 p-2 rounded"
style={{
background: '#3d2a00',
border: '1px solid #9e6a03',
}}
>
💡 <strong></strong>
使
</p>
<a
href="https://www.binance.com/zh-CN/support/faq/how-to-create-api-keys-on-binance-360002502072"
target="_blank"
rel="noopener noreferrer"
className="inline-block text-sm hover:underline"
style={{ color: '#58a6ff' }}
>
📖
</a>
</div>
)}
</div>
)}
<div>
<label
className="block text-sm font-semibold mb-2"
style={{ color: '#EAECEF' }}
>
{t('apiKey', language)}
</label>
<input
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder={t('enterAPIKey', language)}
className="w-full px-3 py-2 rounded"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
}}
required
/>
</div>
<div>
<label
className="block text-sm font-semibold mb-2"
style={{ color: '#EAECEF' }}
>
{t('secretKey', language)}
</label>
<input
type="password"
value={secretKey}
onChange={(e) => setSecretKey(e.target.value)}
placeholder={t('enterSecretKey', language)}
className="w-full px-3 py-2 rounded"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
}}
required
/>
</div>
{selectedExchange.id === 'okx' && (
<div>
<label
className="block text-sm font-semibold mb-2"
style={{ color: '#EAECEF' }}
>
{t('passphrase', language)}
</label>
<input
type="password"
value={passphrase}
onChange={(e) => setPassphrase(e.target.value)}
placeholder={t('enterPassphrase', language)}
className="w-full px-3 py-2 rounded"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
}}
required
/>
</div>
)}
{/* Binance 白名单IP提示 */}
{selectedExchange.id === 'binance' && (
<div
className="p-4 rounded"
style={{
background: 'rgba(240, 185, 11, 0.1)',
border: '1px solid rgba(240, 185, 11, 0.2)',
}}
>
<div
className="text-sm font-semibold mb-2"
style={{ color: '#F0B90B' }}
>
{t('whitelistIP', language)}
</div>
<div
className="text-xs mb-3"
style={{ color: '#848E9C' }}
>
{t('whitelistIPDesc', language)}
</div>
{loadingIP ? (
<div
className="text-xs"
style={{ color: '#848E9C' }}
>
{t('loadingServerIP', language)}
</div>
) : serverIP && serverIP.public_ip ? (
<div
className="flex items-center gap-2 p-2 rounded"
style={{ background: '#0B0E11' }}
>
<code
className="flex-1 text-sm font-mono"
style={{ color: '#F0B90B' }}
>
{serverIP.public_ip}
</code>
<button
type="button"
onClick={() => handleCopyIP(serverIP.public_ip)}
className="px-3 py-1 rounded text-xs font-semibold transition-all hover:scale-105"
style={{
background: 'rgba(240, 185, 11, 0.2)',
color: '#F0B90B',
}}
>
{copiedIP
? t('ipCopied', language)
: t('copyIP', language)}
</button>
</div>
) : null}
</div>
)}
</>
)}
{/* Aster 交易所的字段 */}
{selectedExchange.id === 'aster' && (
<>
<div>
<label
className="block text-sm font-semibold mb-2 flex items-center gap-2"
style={{ color: '#EAECEF' }}
>
{t('user', language)}
<Tooltip content={t('asterUserDesc', language)}>
<HelpCircle
className="w-4 h-4 cursor-help"
style={{ color: '#F0B90B' }}
/>
</Tooltip>
</label>
<input
type="text"
value={asterUser}
onChange={(e) => setAsterUser(e.target.value)}
placeholder={t('enterUser', language)}
className="w-full px-3 py-2 rounded"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
}}
required
/>
</div>
<div>
<label
className="block text-sm font-semibold mb-2 flex items-center gap-2"
style={{ color: '#EAECEF' }}
>
{t('signer', language)}
<Tooltip content={t('asterSignerDesc', language)}>
<HelpCircle
className="w-4 h-4 cursor-help"
style={{ color: '#F0B90B' }}
/>
</Tooltip>
</label>
<input
type="text"
value={asterSigner}
onChange={(e) => setAsterSigner(e.target.value)}
placeholder={t('enterSigner', language)}
className="w-full px-3 py-2 rounded"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
}}
required
/>
</div>
<div>
<label
className="block text-sm font-semibold mb-2 flex items-center gap-2"
style={{ color: '#EAECEF' }}
>
{t('privateKey', language)}
<Tooltip content={t('asterPrivateKeyDesc', language)}>
<HelpCircle
className="w-4 h-4 cursor-help"
style={{ color: '#F0B90B' }}
/>
</Tooltip>
</label>
<input
type="password"
value={asterPrivateKey}
onChange={(e) => setAsterPrivateKey(e.target.value)}
placeholder={t('enterPrivateKey', language)}
className="w-full px-3 py-2 rounded"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
}}
required
/>
</div>
</>
)}
{/* Hyperliquid 交易所的字段 */}
{selectedExchange.id === 'hyperliquid' && (
<>
{/* 安全提示 banner */}
<div
className="p-3 rounded mb-4"
style={{
background: 'rgba(240, 185, 11, 0.1)',
border: '1px solid rgba(240, 185, 11, 0.3)',
}}
>
<div className="flex items-start gap-2">
<span style={{ color: '#F0B90B', fontSize: '16px' }}>
🔐
</span>
<div className="flex-1">
<div
className="text-sm font-semibold mb-1"
style={{ color: '#F0B90B' }}
>
{t('hyperliquidAgentWalletTitle', language)}
</div>
<div
className="text-xs"
style={{ color: '#848E9C', lineHeight: '1.5' }}
>
{t('hyperliquidAgentWalletDesc', language)}
</div>
</div>
</div>
</div>
{/* Agent Private Key 字段 */}
<div>
<label
className="block text-sm font-semibold mb-2"
style={{ color: '#EAECEF' }}
>
{t('hyperliquidAgentPrivateKey', language)}
</label>
<div className="flex flex-col gap-2">
<div className="flex gap-2">
<input
type="text"
value={maskSecret(apiKey)}
readOnly
placeholder={t(
'enterHyperliquidAgentPrivateKey',
language
)}
className="w-full px-3 py-2 rounded"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
}}
/>
<button
type="button"
onClick={() => setSecureInputTarget('hyperliquid')}
className="px-3 py-2 rounded text-xs font-semibold transition-all hover:scale-105"
style={{
background: '#F0B90B',
color: '#000',
whiteSpace: 'nowrap',
}}
>
{apiKey
? t('secureInputReenter', language)
: t('secureInputButton', language)}
</button>
{apiKey && (
<button
type="button"
onClick={() => setApiKey('')}
className="px-3 py-2 rounded text-xs font-semibold transition-all hover:scale-105"
style={{
background: '#1B1F2B',
color: '#848E9C',
whiteSpace: 'nowrap',
}}
>
{t('secureInputClear', language)}
</button>
)}
</div>
{apiKey && (
<div className="text-xs" style={{ color: '#848E9C' }}>
{t('secureInputHint', language)}
</div>
)}
</div>
<div
className="text-xs mt-1"
style={{ color: '#848E9C' }}
>
{t('hyperliquidAgentPrivateKeyDesc', language)}
</div>
</div>
{/* Main Wallet Address 字段 */}
<div>
<label
className="block text-sm font-semibold mb-2"
style={{ color: '#EAECEF' }}
>
{t('hyperliquidMainWalletAddress', language)}
</label>
<input
type="text"
value={hyperliquidWalletAddr}
onChange={(e) =>
setHyperliquidWalletAddr(e.target.value)
}
placeholder={t(
'enterHyperliquidMainWalletAddress',
language
)}
className="w-full px-3 py-2 rounded"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
}}
required
/>
<div
className="text-xs mt-1"
style={{ color: '#848E9C' }}
>
{t('hyperliquidMainWalletAddressDesc', language)}
</div>
</div>
</>
)}
</>
)}
</div>
<div
className="flex gap-3 mt-6 pt-4 sticky bottom-0"
style={{ background: '#1E2329' }}
>
<button
type="button"
onClick={onClose}
className="flex-1 px-4 py-2 rounded text-sm font-semibold"
style={{ background: '#2B3139', color: '#848E9C' }}
>
{t('cancel', language)}
</button>
<button
type="submit"
disabled={
!selectedExchange ||
(selectedExchange.id === 'binance' &&
(!apiKey.trim() || !secretKey.trim())) ||
(selectedExchange.id === 'okx' &&
(!apiKey.trim() ||
!secretKey.trim() ||
!passphrase.trim())) ||
(selectedExchange.id === 'hyperliquid' &&
(!apiKey.trim() || !hyperliquidWalletAddr.trim())) || // 验证私钥和钱包地址
(selectedExchange.id === 'aster' &&
(!asterUser.trim() ||
!asterSigner.trim() ||
!asterPrivateKey.trim())) ||
(selectedExchange.type === 'cex' &&
selectedExchange.id !== 'hyperliquid' &&
selectedExchange.id !== 'aster' &&
selectedExchange.id !== 'binance' &&
selectedExchange.id !== 'okx' &&
(!apiKey.trim() || !secretKey.trim()))
}
className="flex-1 px-4 py-2 rounded text-sm font-semibold disabled:opacity-50"
style={{ background: '#F0B90B', color: '#000' }}
>
{t('saveConfig', language)}
</button>
</div>
</form>
</div>
{/* Binance Setup Guide Modal */}
{showGuide && (
<div
className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 p-4"
onClick={() => setShowGuide(false)}
>
<div
className="bg-gray-800 rounded-lg p-6 w-full max-w-4xl relative"
style={{ background: '#1E2329' }}
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between mb-4">
<h3
className="text-xl font-bold flex items-center gap-2"
style={{ color: '#EAECEF' }}
>
<BookOpen className="w-6 h-6" style={{ color: '#F0B90B' }} />
{t('binanceSetupGuide', language)}
</h3>
<button
onClick={() => setShowGuide(false)}
className="px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
style={{ background: '#2B3139', color: '#848E9C' }}
>
{t('closeGuide', language)}
</button>
</div>
<div className="overflow-y-auto max-h-[80vh]">
<img
src="/images/guide.png"
alt={t('binanceSetupGuide', language)}
className="w-full h-auto rounded"
/>
</div>
</div>
</div>
)}
{/* Two Stage Key Modal */}
<TwoStageKeyModal
isOpen={secureInputTarget !== null}
language={language}
contextLabel={secureInputContextLabel}
expectedLength={64}
onCancel={handleSecureInputCancel}
onComplete={handleSecureInputComplete}
/>
</div>
)
}