import React, { useState, useEffect } from 'react' import { QRCodeSVG } from 'qrcode.react' import { Trash2, Brain, ExternalLink } from 'lucide-react' import type { AIModel } from '../../types' import type { Language } from '../../i18n/translations' import { t } from '../../i18n/translations' import { api } from '../../lib/api' import { getModelIcon } from '../common/ModelIcons' import { ModelStepIndicator } from './ModelStepIndicator' import { ModelCard } from './ModelCard' import { CLAW402_MODELS, AI_PROVIDER_CONFIG, getShortName, } from './model-constants' import { getBeginnerWalletAddress, getUserMode } from '../../lib/onboarding' interface ModelConfigModalProps { allModels: AIModel[] configuredModels: AIModel[] editingModelId: string | null onSave: ( modelId: string, apiKey: string, baseUrl?: string, modelName?: string ) => void onDelete: (modelId: string) => void onClose: () => void language: Language } export function ModelConfigModal({ allModels, configuredModels, editingModelId, onSave, onDelete, onClose, language, }: ModelConfigModalProps) { const [currentStep, setCurrentStep] = useState(editingModelId ? 1 : 0) const [selectedModelId, setSelectedModelId] = useState(editingModelId || '') const [apiKey, setApiKey] = useState('') const [baseUrl, setBaseUrl] = useState('') const [modelName, setModelName] = useState('') const configuredModel = configuredModels?.find((model) => model.id === selectedModelId) || null // Always prefer allModels (supportedModels) for provider/id lookup; // fall back to configuredModels for edit mode details (apiKey etc.) const selectedModel = allModels?.find((m) => m.id === selectedModelId) || configuredModel useEffect(() => { const modelDetails = configuredModel || selectedModel if (editingModelId && modelDetails) { setApiKey(modelDetails.apiKey || '') setBaseUrl(modelDetails.customApiUrl || '') setModelName(modelDetails.customModelName || '') } }, [editingModelId, configuredModel, selectedModel]) const handleSelectModel = (modelId: string) => { setSelectedModelId(modelId) setCurrentStep(1) } const handleBack = () => { if (editingModelId) { onClose() } else { setCurrentStep(0) setSelectedModelId('') } } const handleSubmit = (e: React.FormEvent) => { e.preventDefault() if (!selectedModelId || !apiKey.trim()) return onSave(selectedModelId, apiKey.trim(), baseUrl.trim() || undefined, modelName.trim() || undefined) } const availableModels = allModels || [] const configuredIds = new Set(configuredModels?.map(m => m.id) || []) const isClaw402Selected = selectedModel?.provider === 'claw402' || selectedModel?.id === 'claw402' const isBeginnerDefaultModel = isClaw402Selected && getUserMode() === 'beginner' const stepLabels = [ t('modelConfig.selectModel', language), t( !selectedModel ? 'modelConfig.configure' : isClaw402Selected ? 'modelConfig.configureWallet' : 'modelConfig.configure', language ), ] return (
{/* Header */}
{currentStep > 0 && !editingModelId && ( )}

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

{editingModelId && !isBeginnerDefaultModel && ( )}
{/* Step Indicator */} {!editingModelId && (
)} {/* Content */}
{/* Step 0: Select Model */} {currentStep === 0 && !editingModelId && ( )} {/* Step 1: Configure — Claw402 Dedicated UI */} {(currentStep === 1 || editingModelId) && selectedModel && (selectedModel.provider === 'claw402' || selectedModel.id === 'claw402') && ( )} {/* Step 1: Configure — Standard Providers (non-claw402) */} {(currentStep === 1 || editingModelId) && selectedModel && selectedModel.provider !== 'claw402' && selectedModel.id !== 'claw402' && ( )}
) } // --- Sub-components for ModelConfigModal --- function ModelSelectionStep({ availableModels, configuredIds, selectedModelId, onSelectModel, language, }: { availableModels: AIModel[] configuredIds: Set selectedModelId: string onSelectModel: (modelId: string) => void language: Language }) { const [showOtherProviders, setShowOtherProviders] = useState(false) const claw402Model = availableModels.find((m) => m.provider === 'claw402') const otherProviders = availableModels.filter((m) => m.provider !== 'claw402') return (
{t('modelConfig.chooseProvider', language)}
{/* Claw402 Featured Card */} {claw402Model && ( )} {otherProviders.length > 0 && (
{showOtherProviders && (
{otherProviders.map((model) => ( onSelectModel(model.id)} configured={configuredIds.has(model.id)} /> ))}
{t('modelConfig.modelsConfigured', language)}
)}
)}
) } function Claw402ConfigForm({ apiKey, modelName, configuredModel, editingModelId, onApiKeyChange, onModelNameChange, onBack, onSubmit, language, }: { apiKey: string modelName: string configuredModel: AIModel | null editingModelId: string | null onApiKeyChange: (value: string) => void onModelNameChange: (value: string) => void onBack: () => void onSubmit: (e: React.FormEvent) => void language: Language }) { const [walletAddress, setWalletAddress] = useState('') const [copiedAddr, setCopiedAddr] = useState(false) const [showDeposit, setShowDeposit] = useState(false) const [usdcBalance, setUsdcBalance] = useState(null) const [keyError, setKeyError] = useState('') const [validating, setValidating] = useState(false) const [claw402Status, setClaw402Status] = useState(null) const [testResult, setTestResult] = useState<{ status: string; message: string } | null>(null) const [testing, setTesting] = useState(false) const [serverWalletAddress, setServerWalletAddress] = useState('') const [serverWalletBalance, setServerWalletBalance] = useState(null) const localWalletAddress = getBeginnerWalletAddress()?.trim() || '' const configuredWalletAddress = configuredModel?.walletAddress?.trim() || localWalletAddress || serverWalletAddress const resolvedWalletAddress = walletAddress || configuredWalletAddress const resolvedUsdcBalance = usdcBalance ?? configuredModel?.balanceUsdc ?? serverWalletBalance ?? null const hasExistingWallet = Boolean(configuredWalletAddress) // Client-side validation helper const getClientError = (key: string): string => { if (!key) return '' if (!key.startsWith('0x')) return t('modelConfig.invalidKeyPrefix', language) if (key.length !== 66) return `${t('modelConfig.invalidKeyLength', language)} ${key.length}` if (!/^0x[0-9a-fA-F]{64}$/.test(key)) return t('modelConfig.invalidKeyChars', language) return '' } const isKeyValid = apiKey.length === 66 && apiKey.startsWith('0x') && /^0x[0-9a-fA-F]{64}$/.test(apiKey) useEffect(() => { if (hasExistingWallet) { setShowDeposit(true) } }, [hasExistingWallet]) useEffect(() => { if (configuredModel?.walletAddress || localWalletAddress || serverWalletAddress) { return } let cancelled = false void api .getCurrentBeginnerWallet() .then((result) => { setClaw402Status(result.claw402_status || 'unknown') if (cancelled || !result.found || !result.address) { return } setServerWalletAddress(result.address) setServerWalletBalance(result.balance_usdc || null) }) .catch(() => { // Ignore silently: this is a best-effort fallback for showing the current wallet. }) return () => { cancelled = true } }, [configuredModel?.walletAddress, localWalletAddress, serverWalletAddress]) // Debounced validation when apiKey changes useEffect(() => { setWalletAddress('') setUsdcBalance(null) setClaw402Status(null) setTestResult(null) const clientErr = getClientError(apiKey) setKeyError(clientErr) if (clientErr || !apiKey) { setValidating(false) return } setValidating(true) const timer = setTimeout(async () => { try { const res = await fetch('/api/wallet/validate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ private_key: apiKey }), }) const data = await res.json() if (data.valid) { setWalletAddress(data.address || '') setUsdcBalance(data.balance_usdc || '0.00') setClaw402Status(data.claw402_status || 'unknown') setKeyError('') } else { setKeyError(data.error || 'Invalid key') } } catch { setKeyError('Validation request failed') } finally { setValidating(false) } }, 500) return () => clearTimeout(timer) }, [apiKey]) const handleTestConnection = async () => { setTesting(true) setTestResult(null) try { if (!apiKey && hasExistingWallet) { const result = await api.getCurrentBeginnerWallet() setClaw402Status(result.claw402_status || 'unknown') if (result.found && result.address) { setWalletAddress(result.address) setUsdcBalance(result.balance_usdc || '0.00') setShowDeposit(true) } setTestResult({ status: result.claw402_status === 'ok' ? 'ok' : 'error', message: result.claw402_status === 'ok' ? t('modelConfig.claw402Connected', language) : t('modelConfig.claw402Unreachable', language), }) return } const res = await fetch('/api/wallet/validate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ private_key: apiKey }), }) const data = await res.json() if (data.valid) { setWalletAddress(data.address || '') setUsdcBalance(data.balance_usdc || '0.00') setClaw402Status(data.claw402_status || 'unknown') if (parseFloat(data.balance_usdc || '0') === 0) setShowDeposit(true) setTestResult({ status: data.claw402_status === 'ok' ? 'ok' : 'error', message: data.claw402_status === 'ok' ? t('modelConfig.claw402Connected', language) : t('modelConfig.claw402Unreachable', language), }) } else { setTestResult({ status: 'error', message: data.error || 'Invalid key' }) } } catch { setTestResult({ status: 'error', message: t('modelConfig.claw402Unreachable', language) }) } finally { setTesting(false) } } const balanceNum = resolvedUsdcBalance ? parseFloat(resolvedUsdcBalance) : 0 return (
{/* Claw402 Hero Header */}
Claw402
Claw402
{t('modelConfig.allModelsClaw', language)}
{['GPT', 'Claude', 'DeepSeek', 'Gemini', 'Grok', 'Qwen', 'Kimi'].map(name => ( {name} ))}
{claw402Status ? (
{claw402Status === 'ok' ? t('modelConfig.claw402Connected', language) : t('modelConfig.claw402Unreachable', language)}
) : null}
{/* Step 1: Select AI Model */}
{t('modelConfig.allModelsUnified', language)}
{CLAW402_MODELS.map((m) => { const isSelected = (modelName || 'glm-5') === m.id return ( ) })}
{/* Step 2: Wallet Setup */}
{t('modelConfig.walletInfo', language)}
{t('modelConfig.exportKey', language)}
{t('modelConfig.dedicatedWallet', language)}
{hasExistingWallet && (
{language === 'zh' ? '已自动提取当前钱包' : 'Current wallet loaded automatically'}
{language === 'zh' ? '你现在可以直接查看当前钱包地址、余额和充值二维码。只有在想更换钱包时,才需要重新输入新的私钥。' : 'You can view the current wallet address, balance, and deposit QR code right away. Only enter a new private key if you want to replace this wallet.'}
{!configuredModel?.walletAddress && localWalletAddress ? (
{language === 'zh' ? '当前地址来自本地已保存的新手钱包。' : 'This address comes from the locally saved beginner wallet.'}
) : null} {!configuredModel?.walletAddress && !localWalletAddress && serverWalletAddress ? (
{language === 'zh' ? '当前地址来自后端保存的钱包配置。' : 'This address comes from the wallet saved on the server.'}
) : null}
)}
{t('modelConfig.walletPrivateKey', language)}
onApiKeyChange(e.target.value)} placeholder={ hasExistingWallet ? language === 'zh' ? '如需切换钱包,请手动输入新的私钥' : 'Enter a new private key only if you want to switch wallets' : '0x...' } className="flex-1 px-4 py-3 rounded-xl font-mono text-sm" style={{ background: '#0B0E11', border: keyError ? '1px solid #EF4444' : walletAddress ? '1px solid #00E096' : '1px solid #2B3139', color: '#EAECEF', }} required={!hasExistingWallet} />
{hasExistingWallet && !apiKey ? (
{language === 'zh' ? '后续这里只使用你第一次创建并保存的钱包;如果你要换钱包,请手动填写新的私钥。' : 'This screen keeps using the wallet created and saved the first time. Enter a new private key manually only if you want to switch wallets.'}
) : null}
🔒 {t('modelConfig.privateKeyNote', language)}
{/* Wallet Validation Results */} {(apiKey || hasExistingWallet) && (
{/* Validating spinner */} {validating && (
{t('modelConfig.validating', language)}
)} {/* Error message */} {keyError && !validating && (
{keyError}
)} {/* Success: address + balance + status */} {resolvedWalletAddress && !validating && !keyError && ( <>
{t('modelConfig.walletAddress', language)}:
{resolvedWalletAddress}
⚠️ {language === 'zh' ? '请确认这是你的钱包地址(可在 MetaMask 中核对)' : 'Please confirm this is your wallet address (verify in MetaMask)'}
{resolvedUsdcBalance !== null && (
💰 0 ? '#00E096' : '#F59E0B' }}> {t('modelConfig.usdcBalance', language)}: ${resolvedUsdcBalance}
)} {showDeposit && (
💳 {language === 'zh' ? '充值 USDC (Base 链)' : 'Deposit USDC (Base Chain)'}
{language === 'zh' ? '扫码或复制地址转账' : 'Scan QR or copy address to transfer'}
{resolvedWalletAddress}
📱 {language === 'zh' ? '用交易所 App 扫描二维码直接转账' : 'Scan QR with exchange app to transfer'}
• {language === 'zh' ? '提币时网络选择 Base' : 'Choose Base network when withdrawing'}
• {language === 'zh' ? '或跨链桥: ' : 'Or bridge: '}bridge.base.org
• {language === 'zh' ? '最低充值 $1 USDC 即可开始' : 'Min $1 USDC to start'}
)} {!apiKey && hasExistingWallet && (
{language === 'zh' ? '当前正在使用这个钱包充值。若要切换钱包,再输入新的私钥并保存即可。' : 'This wallet is currently used for funding. Enter a new private key only if you want to switch wallets.'}
)} {claw402Status && (
{claw402Status === 'ok' ? '🟢' : '🔴'} {claw402Status === 'ok' ? t('modelConfig.claw402Connected', language) : t('modelConfig.claw402Unreachable', language)}
)} )} {/* Test Connection button */} {(isKeyValid || hasExistingWallet) && !validating && ( )} {/* Test result */} {testResult && !testing && (
{testResult.status === 'ok' ? '✅' : '❌'} {testResult.message}
)}
)}
{/* USDC Recharge Guide */}
{'💰 ' + t('modelConfig.howToFundUsdc', language)}
1. {t('modelConfig.fundStep1', language)}
2. {t('modelConfig.fundStep2', language)}
3. {t('modelConfig.fundStep3', language)}
{/* Buttons */}
) } function StandardProviderConfigForm({ selectedModel, apiKey, baseUrl, modelName, editingModelId, onApiKeyChange, onBaseUrlChange, onModelNameChange, onBack, onSubmit, language, }: { selectedModel: AIModel apiKey: string baseUrl: string modelName: string editingModelId: string | null onApiKeyChange: (value: string) => void onBaseUrlChange: (value: string) => void onModelNameChange: (value: string) => void onBack: () => void onSubmit: (e: React.FormEvent) => void language: Language }) { return (
{/* Selected Model Header */}
{getModelIcon(selectedModel.provider || selectedModel.id, { width: 32, height: 32 }) || ( {selectedModel.name[0]} )}
{getShortName(selectedModel.name)}
{selectedModel.provider} • {AI_PROVIDER_CONFIG[selectedModel.provider]?.defaultModel || selectedModel.id}
{AI_PROVIDER_CONFIG[selectedModel.provider] && ( {t('modelConfig.getApiKey', language)} )}
{/* Kimi Warning */} {selectedModel.provider === 'kimi' && (
⚠️
{t('kimiApiNote', language)}
)} {/* API Key / Wallet Private Key */}
onApiKeyChange(e.target.value)} placeholder={t('enterAPIKey', language)} className="w-full px-4 py-3 rounded-xl" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} required />
{/* Custom Base URL */}
onBaseUrlChange(e.target.value)} placeholder={t('customBaseURLPlaceholder', language)} className="w-full px-4 py-3 rounded-xl" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} />
{t('leaveBlankForDefault', language)}
{/* Custom Model Name */}
onModelNameChange(e.target.value)} placeholder={t('customModelNamePlaceholder', language)} className="w-full px-4 py-3 rounded-xl" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} />
{t('leaveBlankForDefaultModel', language)}
{/* Info Box */}
{t('information', language)}
• {t('modelConfigInfo1', language)}
• {t('modelConfigInfo2', language)}
• {t('modelConfigInfo3', language)}
{/* Buttons */}
) }