diff --git a/api/handler_ai_model.go b/api/handler_ai_model.go index 91178dde..15ffce3d 100644 --- a/api/handler_ai_model.go +++ b/api/handler_ai_model.go @@ -220,10 +220,8 @@ func (s *Server) handleGetSupportedModels(c *gin.Context) { {"id": "gemini", "name": "Google Gemini", "provider": "gemini", "defaultModel": "gemini-3-pro-preview"}, {"id": "grok", "name": "Grok (xAI)", "provider": "grok", "defaultModel": "grok-3-latest"}, {"id": "kimi", "name": "Kimi (Moonshot)", "provider": "kimi", "defaultModel": "moonshot-v1-auto"}, - {"id": "minimax", "name": "MiniMax", "provider": "minimax", "defaultModel": "MiniMax-M2.5"}, - {"id": "blockrun-base", "name": "BlockRun (Base Wallet)", "provider": "blockrun-base", "defaultModel": "auto"}, - {"id": "blockrun-sol", "name": "BlockRun (Solana Wallet)", "provider": "blockrun-sol", "defaultModel": "auto"}, - {"id": "claw402", "name": "Claw402 (Base USDC)", "provider": "claw402", "defaultModel": "deepseek"}, + {"id": "minimax", "name": "MiniMax", "provider": "minimax", "defaultModel": "MiniMax-M2.7"}, + {"id": "claw402", "name": "Claw402 (Base USDC)", "provider": "claw402", "defaultModel": "deepseek-v4-flash"}, } c.JSON(http.StatusOK, supportedModels) diff --git a/api/handler_onboarding.go b/api/handler_onboarding.go index 7989ed56..27151bb4 100644 --- a/api/handler_onboarding.go +++ b/api/handler_onboarding.go @@ -10,6 +10,7 @@ import ( "strings" "nofx/logger" + "nofx/mcp/payment" "nofx/wallet" gethcrypto "github.com/ethereum/go-ethereum/crypto" @@ -54,7 +55,7 @@ func (s *Server) handleBeginnerOnboarding(c *gin.Context) { } if !reusedExisting { - if err := s.store.AIModel().Update(userID, "claw402", true, privateKey, "", "glm-5"); err != nil { + if err := s.store.AIModel().Update(userID, "claw402", true, privateKey, "", payment.DefaultClaw402Model); err != nil { logger.Errorf("Failed to save beginner claw402 config for user %s: %v", userID, err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save beginner model configuration"}) return @@ -68,7 +69,7 @@ func (s *Server) handleBeginnerOnboarding(c *gin.Context) { os.Setenv("CLAW402_WALLET_KEY", privateKey) os.Setenv("CLAW402_WALLET_ADDRESS", address) - os.Setenv("CLAW402_DEFAULT_MODEL", "glm-5") + os.Setenv("CLAW402_DEFAULT_MODEL", payment.DefaultClaw402Model) envSaved, envPath, envErr := persistBeginnerWalletEnv(privateKey, address) resp := beginnerOnboardingResponse{ @@ -77,7 +78,7 @@ func (s *Server) handleBeginnerOnboarding(c *gin.Context) { Chain: "base", Asset: "USDC", Provider: "claw402", - DefaultModel: "glm-5", + DefaultModel: payment.DefaultClaw402Model, ConfiguredModelID: configuredModelID, BalanceUSDC: wallet.QueryUSDCBalanceStr(address), EnvSaved: envSaved, @@ -253,7 +254,7 @@ func persistBeginnerWalletEnv(privateKey string, address string) (bool, string, if err := upsertEnvFile(path, map[string]string{ "CLAW402_WALLET_KEY": privateKey, "CLAW402_WALLET_ADDRESS": address, - "CLAW402_DEFAULT_MODEL": "glm-5", + "CLAW402_DEFAULT_MODEL": payment.DefaultClaw402Model, }); err != nil { lastErr = err continue diff --git a/mcp/payment/claw402.go b/mcp/payment/claw402.go index 9d628f54..871cc765 100644 --- a/mcp/payment/claw402.go +++ b/mcp/payment/claw402.go @@ -50,7 +50,7 @@ func shortAddr(addr string) string { const ( DefaultClaw402URL = "https://claw402.ai" - DefaultClaw402Model = "glm-5" + DefaultClaw402Model = "deepseek-v4-flash" ) // claw402ModelEndpoints maps user-friendly model names to claw402 API paths. diff --git a/web/src/components/trader/ModelConfigModal.tsx b/web/src/components/trader/ModelConfigModal.tsx index d13bdcb6..6e8ea9f9 100644 --- a/web/src/components/trader/ModelConfigModal.tsx +++ b/web/src/components/trader/ModelConfigModal.tsx @@ -11,6 +11,7 @@ import { BLOCKRUN_MODELS, CLAW402_MODELS, AI_PROVIDER_CONFIG, + DEFAULT_CLAW402_MODEL, getShortName, } from './model-constants' @@ -74,32 +75,74 @@ export function ModelConfigModal({ const handleSubmit = (e: React.FormEvent) => { e.preventDefault() - if (!selectedModelId || !apiKey.trim()) return - onSave(selectedModelId, apiKey.trim(), baseUrl.trim() || undefined, modelName.trim() || undefined) + if (!selectedModelId) return + const key = apiKey.trim() + // Allow empty key when editing an existing model (backend preserves existing key) + if (!key && !editingModelId) return + const trimmedModelName = modelName.trim() + const resolvedModelName = + selectedModel?.provider === 'claw402' + ? trimmedModelName || DEFAULT_CLAW402_MODEL + : trimmedModelName || undefined + onSave(selectedModelId, key, baseUrl.trim() || undefined, resolvedModelName) } const availableModels = allModels || [] - const configuredIds = new Set(configuredModels?.map(m => m.id) || []) - const stepLabels = [t('modelConfig.selectModel', language), t('modelConfig.configureApi', language)] + 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 && ( -
@@ -113,7 +156,12 @@ export function ModelConfigModal({ )} -
@@ -127,7 +175,10 @@ export function ModelConfigModal({ )} {/* Content */} -
+
{/* Step 0: Select Model */} {currentStep === 0 && !editingModelId && ( - )} + {(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' && ( - - )} + {(currentStep === 1 || editingModelId) && + selectedModel && + selectedModel.provider !== 'claw402' && + selectedModel.id !== 'claw402' && ( + + )}
@@ -205,17 +263,41 @@ function ModelSelectionStep({ if (claw) onSelectModel(claw.id) }} className="w-full p-5 rounded-xl text-left transition-all hover:scale-[1.01]" - style={{ background: 'linear-gradient(135deg, rgba(37, 99, 235, 0.15) 0%, rgba(139, 92, 246, 0.15) 100%)', border: '1.5px solid rgba(37, 99, 235, 0.4)' }} + style={{ + background: + 'linear-gradient(135deg, rgba(37, 99, 235, 0.15) 0%, rgba(139, 92, 246, 0.15) 100%)', + border: '1.5px solid rgba(37, 99, 235, 0.4)', + }} >
- Claw402 + Claw402
-
- {configuredIds.has(availableModels.find(m => m.provider === 'claw402')?.id || '') && ( -
+ {configuredIds.has(claw402Model.id) && ( +
)} -
+
{'🔥 ' + t('modelConfig.recommended', language)}
- + GPT · Claude · DeepSeek · Gemini · Grok · Qwen · Kimi
+
+ {t('modelConfig.claw402EntryDesc', language)} +
)} -
- {availableModels.filter(m => !m.provider?.startsWith('blockrun') && m.provider !== 'claw402').map((model) => ( - onSelectModel(model.id)} - configured={configuredIds.has(model.id)} - /> - ))} -
- {availableModels.some(m => m.provider?.startsWith('blockrun')) && ( - <> -
-
- - {t('modelConfig.viaBlockrunWallet', language)} - -
-
-
- {availableModels.filter(m => m.provider?.startsWith('blockrun')).map((model) => ( - onSelectModel(model.id)} - configured={configuredIds.has(model.id)} - /> - ))} -
- + {otherProviders.length > 0 && ( +
+ + + {showOtherProviders && ( +
+
+ {otherProviders.map((model) => ( + onSelectModel(model.id)} + configured={configuredIds.has(model.id)} + /> + ))} +
+
+ {t('modelConfig.modelsConfigured', language)} +
+
+ )} +
)}
{t('modelConfig.modelsConfigured', language)} @@ -307,21 +432,71 @@ function Claw402ConfigForm({ 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 [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) + 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) + const isKeyValid = + apiKey.length === 66 && + apiKey.startsWith('0x') && + /^0x[0-9a-fA-F]{64}$/.test(apiKey) - // Truncate address for display + 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. + }) // Debounced validation when apiKey changes @@ -370,6 +545,24 @@ function Claw402ConfigForm({ 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' }, @@ -383,15 +576,19 @@ function Claw402ConfigForm({ 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), + 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) }) + setTestResult({ + status: 'error', + message: t('modelConfig.claw402Unreachable', language), + }) } finally { setTesting(false) } @@ -402,28 +599,84 @@ function Claw402ConfigForm({ return (
{/* Claw402 Hero Header */} -
+
Claw402
- - Claw402 + + Claw402{' '} + + ↗ +
{t('modelConfig.allModelsClaw', language)}
- {['GPT', 'Claude', 'DeepSeek', 'Gemini', 'Grok', 'Qwen', 'Kimi'].map(name => ( - - {name} - - ))} + {['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 */}
-
{CLAW402_MODELS.map((m) => { - const isSelected = (modelName || 'deepseek') === m.id + const isSelected = (modelName || DEFAULT_CLAW402_MODEL) === m.id return ( ) @@ -467,14 +747,34 @@ function Claw402ConfigForm({ {/* Step 2: Wallet Setup */}
-