fix(trader): resolve conflict-related ModelConfigModal UI issues (#1493)

Co-authored-by: Dean <afei.wuhao@gmail.com>
This commit is contained in:
deanokk
2026-04-25 00:51:14 +08:00
committed by GitHub
parent a20a71b88d
commit 5dbe32d884

View File

@@ -4,11 +4,12 @@ import { Trash2, Brain, ExternalLink } from 'lucide-react'
import type { AIModel } from '../../types' import type { AIModel } from '../../types'
import type { Language } from '../../i18n/translations' import type { Language } from '../../i18n/translations'
import { t } from '../../i18n/translations' import { t } from '../../i18n/translations'
import { api } from '../../lib/api'
import { getBeginnerWalletAddress } from '../../lib/onboarding'
import { getModelIcon } from '../common/ModelIcons' import { getModelIcon } from '../common/ModelIcons'
import { ModelStepIndicator } from './ModelStepIndicator' import { ModelStepIndicator } from './ModelStepIndicator'
import { ModelCard } from './ModelCard' import { ModelCard } from './ModelCard'
import { import {
BLOCKRUN_MODELS,
CLAW402_MODELS, CLAW402_MODELS,
AI_PROVIDER_CONFIG, AI_PROVIDER_CONFIG,
DEFAULT_CLAW402_MODEL, DEFAULT_CLAW402_MODEL,
@@ -50,6 +51,8 @@ export function ModelConfigModal({
const selectedModel = const selectedModel =
allModels?.find((m) => m.id === selectedModelId) || allModels?.find((m) => m.id === selectedModelId) ||
configuredModels?.find((m) => m.id === selectedModelId) configuredModels?.find((m) => m.id === selectedModelId)
const configuredModel =
configuredModels?.find((m) => m.id === selectedModelId) || null
useEffect(() => { useEffect(() => {
if (editingModelId && selectedModel) { if (editingModelId && selectedModel) {
@@ -91,8 +94,6 @@ export function ModelConfigModal({
const configuredIds = new Set(configuredModels?.map((m) => m.id) || []) const configuredIds = new Set(configuredModels?.map((m) => m.id) || [])
const isClaw402Selected = const isClaw402Selected =
selectedModel?.provider === 'claw402' || selectedModel?.id === 'claw402' selectedModel?.provider === 'claw402' || selectedModel?.id === 'claw402'
const isBeginnerDefaultModel =
isClaw402Selected && getUserMode() === 'beginner'
const stepLabels = [ const stepLabels = [
t('modelConfig.selectModel', language), t('modelConfig.selectModel', language),
t( t(
@@ -248,6 +249,14 @@ function ModelSelectionStep({
onSelectModel: (modelId: string) => void onSelectModel: (modelId: string) => void
language: Language language: Language
}) { }) {
const [showOtherProviders, setShowOtherProviders] = useState(false)
const claw402Model =
availableModels.find((model) => model.provider === 'claw402') || null
const otherProviders = availableModels.filter(
(model) =>
model.provider !== 'claw402' && !model.provider?.startsWith('blockrun')
)
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="text-sm font-semibold" style={{ color: '#EAECEF' }}> <div className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
@@ -255,12 +264,11 @@ function ModelSelectionStep({
</div> </div>
{/* Claw402 Featured Card */} {/* Claw402 Featured Card */}
{availableModels.some(m => m.provider === 'claw402') && ( {claw402Model && (
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
const claw = availableModels.find(m => m.provider === 'claw402') onSelectModel(claw402Model.id)
if (claw) onSelectModel(claw.id)
}} }}
className="w-full p-5 rounded-xl text-left transition-all hover:scale-[1.01]" className="w-full p-5 rounded-xl text-left transition-all hover:scale-[1.01]"
style={{ style={{
@@ -407,6 +415,7 @@ function ModelSelectionStep({
function Claw402ConfigForm({ function Claw402ConfigForm({
apiKey, apiKey,
modelName, modelName,
configuredModel,
editingModelId, editingModelId,
onApiKeyChange, onApiKeyChange,
onModelNameChange, onModelNameChange,
@@ -416,6 +425,7 @@ function Claw402ConfigForm({
}: { }: {
apiKey: string apiKey: string
modelName: string modelName: string
configuredModel: AIModel | null
editingModelId: string | null editingModelId: string | null
onApiKeyChange: (value: string) => void onApiKeyChange: (value: string) => void
onModelNameChange: (value: string) => void onModelNameChange: (value: string) => void
@@ -497,7 +507,10 @@ function Claw402ConfigForm({
.catch(() => { .catch(() => {
// Ignore silently: this is a best-effort fallback for showing the current wallet. // 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 // Debounced validation when apiKey changes
useEffect(() => { useEffect(() => {
@@ -839,7 +852,13 @@ function Claw402ConfigForm({
type="password" type="password"
value={apiKey} value={apiKey}
onChange={(e) => onApiKeyChange(e.target.value)} onChange={(e) => onApiKeyChange(e.target.value)}
placeholder="0x..." placeholder={
hasExistingWallet
? language === 'zh'
? '已保存,如需更换钱包请重新输入私钥'
: 'Already saved. Enter a new private key only to replace the wallet.'
: '0x...'
}
className="flex-1 px-4 py-3 rounded-xl font-mono text-sm" className="flex-1 px-4 py-3 rounded-xl font-mono text-sm"
style={{ style={{
background: '#0B0E11', background: '#0B0E11',
@@ -850,24 +869,33 @@ function Claw402ConfigForm({
: '1px solid #2B3139', : '1px solid #2B3139',
color: '#EAECEF', color: '#EAECEF',
}} }}
required required={!hasExistingWallet}
/> />
{!apiKey && ( {!apiKey && (
<button <button
type="button" type="button"
onClick={async () => { onClick={async () => {
try { try {
const res = await fetch('/api/wallet/generate', { method: 'POST' }) const res = await fetch('/api/wallet/generate', {
method: 'POST',
})
const data = await res.json() const data = await res.json()
if (data.private_key) { if (data.private_key) {
onApiKeyChange(data.private_key) onApiKeyChange(data.private_key)
setShowNewWalletBackup(true) setShowNewWalletBackup(true)
setNewWalletKey(data.private_key) setNewWalletKey(data.private_key)
} }
} catch { /* ignore */ } } catch {
/* ignore */
}
}} }}
className="shrink-0 px-3 py-3 rounded-xl text-xs font-semibold transition-all hover:scale-[1.02]" className="shrink-0 px-3 py-3 rounded-xl text-xs font-semibold transition-all hover:scale-[1.02]"
style={{ background: 'linear-gradient(135deg, #2563EB, #7C3AED)', color: '#fff', border: 'none', cursor: 'pointer' }} style={{
background: 'linear-gradient(135deg, #2563EB, #7C3AED)',
color: '#fff',
border: 'none',
cursor: 'pointer',
}}
> >
{language === 'zh' ? '🔑 创建钱包' : '🔑 Create Wallet'} {language === 'zh' ? '🔑 创建钱包' : '🔑 Create Wallet'}
</button> </button>
@@ -876,9 +904,21 @@ function Claw402ConfigForm({
{/* New wallet backup warning */} {/* New wallet backup warning */}
{showNewWalletBackup && newWalletKey && ( {showNewWalletBackup && newWalletKey && (
<div className="p-3 rounded-xl" style={{ background: 'rgba(239, 68, 68, 0.08)', border: '1px solid rgba(239, 68, 68, 0.3)' }}> <div
<div className="text-xs font-bold mb-2" style={{ color: '#EF4444' }}> className="p-3 rounded-xl"
🚨 {language === 'zh' ? '重要:请立即备份私钥!' : 'Important: Backup your private key NOW!'} style={{
background: 'rgba(239, 68, 68, 0.08)',
border: '1px solid rgba(239, 68, 68, 0.3)',
}}
>
<div
className="text-xs font-bold mb-2"
style={{ color: '#EF4444' }}
>
🚨{' '}
{language === 'zh'
? '重要:请立即备份私钥!'
: 'Important: Backup your private key NOW!'}
</div> </div>
<div className="text-[11px] mb-2" style={{ color: '#F87171' }}> <div className="text-[11px] mb-2" style={{ color: '#F87171' }}>
{language === 'zh' {language === 'zh'
@@ -886,7 +926,10 @@ function Claw402ConfigForm({
: 'This is your wallet private key. If lost, it cannot be recovered and all assets will be permanently lost. Copy and save it securely.'} : 'This is your wallet private key. If lost, it cannot be recovered and all assets will be permanently lost. Copy and save it securely.'}
</div> </div>
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<code className="text-[10px] font-mono break-all select-all flex-1 p-2 rounded" style={{ background: '#0B0E11', color: '#F87171' }}> <code
className="text-[10px] font-mono break-all select-all flex-1 p-2 rounded"
style={{ background: '#0B0E11', color: '#F87171' }}
>
{newWalletKey} {newWalletKey}
</code> </code>
<button <button
@@ -897,15 +940,38 @@ function Claw402ConfigForm({
setTimeout(() => setCopiedAddr(false), 2000) setTimeout(() => setCopiedAddr(false), 2000)
}} }}
className="shrink-0 text-[10px] px-2 py-1 rounded" className="shrink-0 text-[10px] px-2 py-1 rounded"
style={{ background: 'rgba(239,68,68,0.15)', color: '#F87171', border: 'none', cursor: 'pointer' }} style={{
background: 'rgba(239,68,68,0.15)',
color: '#F87171',
border: 'none',
cursor: 'pointer',
}}
> >
{copiedAddr ? '✅ Copied' : '📋 Copy Key'} {copiedAddr ? '✅ Copied' : '📋 Copy Key'}
</button> </button>
</div> </div>
<div className="text-[10px] space-y-1" style={{ color: '#848E9C' }}> <div
<div> {language === 'zh' ? '建议保存到密码管理器1Password / Bitwarden' : 'Save to a password manager (1Password / Bitwarden)'}</div> className="text-[10px] space-y-1"
<div> {language === 'zh' ? '或抄在纸上放安全的地方' : 'Or write it down and store it safely'}</div> style={{ color: '#848E9C' }}
<div> {language === 'zh' ? '不要截图发给别人' : 'Do NOT screenshot or share with anyone'}</div> >
<div>
{' '}
{language === 'zh'
? '建议保存到密码管理器1Password / Bitwarden'
: 'Save to a password manager (1Password / Bitwarden)'}
</div>
<div>
{' '}
{language === 'zh'
? '或抄在纸上放安全的地方'
: 'Or write it down and store it safely'}
</div>
<div>
{' '}
{language === 'zh'
? '不要截图发给别人'
: 'Do NOT screenshot or share with anyone'}
</div>
</div> </div>
</div> </div>
)} )}
@@ -1138,7 +1204,7 @@ function Claw402ConfigForm({
)} )}
{/* Test Connection button */} {/* Test Connection button */}
{isKeyValid && !validating && ( {(isKeyValid || hasExistingWallet) && !validating && (
<button <button
type="button" type="button"
onClick={handleTestConnection} onClick={handleTestConnection}
@@ -1223,7 +1289,7 @@ function Claw402ConfigForm({
</button> </button>
<button <button
type="submit" type="submit"
disabled={!isKeyValid} disabled={!isKeyValid && !hasExistingWallet}
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" 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={{ style={{
background: background:
@@ -1335,9 +1401,14 @@ function StandardProviderConfigForm({
{editingModelId && selectedModel && 'has_api_key' in selectedModel && ( {editingModelId && selectedModel && 'has_api_key' in selectedModel && (
<div <div
className="p-3 rounded-xl text-xs" className="p-3 rounded-xl text-xs"
style={{ background: 'rgba(14, 203, 129, 0.08)', border: '1px solid rgba(14, 203, 129, 0.2)', color: '#9FE8C5' }} style={{
background: 'rgba(14, 203, 129, 0.08)',
border: '1px solid rgba(14, 203, 129, 0.2)',
color: '#9FE8C5',
}}
> >
{selectedModel.has_api_key ? '已配置 API Key' : '未配置 API Key'}
{selectedModel.has_api_key ? '已配置 API Key' : '未配置 API Key'}
</div> </div>
)} )}
@@ -1372,10 +1443,10 @@ function StandardProviderConfigForm({
editingModelId && selectedModel.has_api_key editingModelId && selectedModel.has_api_key
? '已保存,如需更换请重新输入' ? '已保存,如需更换请重新输入'
: selectedModel.provider === 'blockrun-base' : selectedModel.provider === 'blockrun-base'
? '0x... (EVM private key)' ? '0x... (EVM private key)'
: selectedModel.provider === 'blockrun-sol' : selectedModel.provider === 'blockrun-sol'
? 'bs58 encoded key (Solana)' ? 'bs58 encoded key (Solana)'
: t('enterAPIKey', language) : t('enterAPIKey', language)
} }
className="w-full px-4 py-3 rounded-xl" className="w-full px-4 py-3 rounded-xl"
style={{ style={{
@@ -1388,80 +1459,84 @@ function StandardProviderConfigForm({
</div> </div>
{/* Custom Base URL */} {/* Custom Base URL */}
<div className="space-y-2"> {!selectedModel.provider?.startsWith('blockrun') && (
<label <div className="space-y-2">
className="flex items-center gap-2 text-sm font-semibold" <label
style={{ color: '#EAECEF' }} className="flex items-center gap-2 text-sm font-semibold"
> style={{ color: '#EAECEF' }}
<svg
className="w-4 h-4"
style={{ color: '#A78BFA' }}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
> >
<path <svg
strokeLinecap="round" className="w-4 h-4"
strokeLinejoin="round" style={{ color: '#A78BFA' }}
strokeWidth={2} fill="none"
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" stroke="currentColor"
/> viewBox="0 0 24 24"
</svg> >
{t('customBaseURL', language)} <path
</label> strokeLinecap="round"
<input strokeLinejoin="round"
type="url" strokeWidth={2}
value={baseUrl} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
onChange={(e) => onBaseUrlChange(e.target.value)} />
placeholder={t('customBaseURLPlaceholder', language)} </svg>
className="w-full px-4 py-3 rounded-xl" {t('customBaseURL', language)}
style={{ </label>
background: '#0B0E11', <input
border: '1px solid #2B3139', type="url"
color: '#EAECEF', value={baseUrl}
}} onChange={(e) => onBaseUrlChange(e.target.value)}
/> placeholder={t('customBaseURLPlaceholder', language)}
<div className="text-xs" style={{ color: '#848E9C' }}> className="w-full px-4 py-3 rounded-xl"
{t('leaveBlankForDefault', language)} style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
}}
/>
<div className="text-xs" style={{ color: '#848E9C' }}>
{t('leaveBlankForDefault', language)}
</div>
</div> </div>
)} )}
{/* Custom Model Name */} {/* Custom Model Name */}
<div className="space-y-2"> {!selectedModel.provider?.startsWith('blockrun') && (
<label <div className="space-y-2">
className="flex items-center gap-2 text-sm font-semibold" <label
style={{ color: '#EAECEF' }} className="flex items-center gap-2 text-sm font-semibold"
> style={{ color: '#EAECEF' }}
<svg
className="w-4 h-4"
style={{ color: '#A78BFA' }}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
> >
<path <svg
strokeLinecap="round" className="w-4 h-4"
strokeLinejoin="round" style={{ color: '#A78BFA' }}
strokeWidth={2} fill="none"
d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" stroke="currentColor"
/> viewBox="0 0 24 24"
</svg> >
{t('customModelName', language)} <path
</label> strokeLinecap="round"
<input strokeLinejoin="round"
type="text" strokeWidth={2}
value={modelName} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"
onChange={(e) => onModelNameChange(e.target.value)} />
placeholder={t('customModelNamePlaceholder', language)} </svg>
className="w-full px-4 py-3 rounded-xl" {t('customModelName', language)}
style={{ </label>
background: '#0B0E11', <input
border: '1px solid #2B3139', type="text"
color: '#EAECEF', value={modelName}
}} onChange={(e) => onModelNameChange(e.target.value)}
/> placeholder={t('customModelNamePlaceholder', language)}
<div className="text-xs" style={{ color: '#848E9C' }}> className="w-full px-4 py-3 rounded-xl"
{t('leaveBlankForDefaultModel', language)} style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
}}
/>
<div className="text-xs" style={{ color: '#848E9C' }}>
{t('leaveBlankForDefaultModel', language)}
</div>
</div> </div>
)} )}