mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-09 14:00:57 +08:00
feat: improve user onboarding and setup UX (#1436)
* feat: add beginner onboarding and mode switching flow * chore: ignore local gh auth config * fix: restore kline fallback and align onboarding language --------- Co-authored-by: zavier-bin <zhaobbbhhh@gmail.com>
This commit is contained in:
@@ -5,6 +5,8 @@ import { useAuth } from '../../contexts/AuthContext'
|
||||
import { useLanguage } from '../../contexts/LanguageContext'
|
||||
import { t } from '../../i18n/translations'
|
||||
import { DeepVoidBackground } from '../common/DeepVoidBackground'
|
||||
import { OnboardingModeSelector } from './OnboardingModeSelector'
|
||||
import type { UserMode } from '../../lib/onboarding'
|
||||
|
||||
export function LoginPage() {
|
||||
const { language } = useLanguage()
|
||||
@@ -15,6 +17,7 @@ export function LoginPage() {
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [expiredToastId, setExpiredToastId] = useState<string | number | null>(null)
|
||||
const [mode, setMode] = useState<UserMode>('beginner')
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionStorage.getItem('from401') === 'true') {
|
||||
@@ -28,7 +31,7 @@ export function LoginPage() {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
setLoading(true)
|
||||
const result = await login(email, password)
|
||||
const result = await login(email, password, mode)
|
||||
setLoading(false)
|
||||
if (result.success) {
|
||||
if (expiredToastId) toast.dismiss(expiredToastId)
|
||||
@@ -109,6 +112,12 @@ export function LoginPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<OnboardingModeSelector
|
||||
language={language}
|
||||
mode={mode}
|
||||
onChange={setMode}
|
||||
/>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<p className="text-xs text-red-400 bg-red-500/10 border border-red-500/20 rounded-lg px-3 py-2">
|
||||
|
||||
75
web/src/components/auth/OnboardingModeSelector.tsx
Normal file
75
web/src/components/auth/OnboardingModeSelector.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import type { UserMode } from '../../lib/onboarding'
|
||||
|
||||
interface OnboardingModeSelectorProps {
|
||||
language: string
|
||||
mode: UserMode
|
||||
onChange: (mode: UserMode) => void
|
||||
}
|
||||
|
||||
export function OnboardingModeSelector({
|
||||
language,
|
||||
mode,
|
||||
onChange,
|
||||
}: OnboardingModeSelectorProps) {
|
||||
const isZh = language === 'zh'
|
||||
|
||||
const options: Array<{
|
||||
id: UserMode
|
||||
title: string
|
||||
badge?: string
|
||||
description: string
|
||||
}> = [
|
||||
{
|
||||
id: 'beginner',
|
||||
title: isZh ? '新手模式' : 'Beginner Mode',
|
||||
badge: isZh ? '推荐' : 'Recommended',
|
||||
description: isZh
|
||||
? '自动生成 Base 钱包,默认接入 Claw402 + DeepSeek,最快完成首次启动。'
|
||||
: 'Generate a Base wallet automatically and start with Claw402 + DeepSeek by default.',
|
||||
},
|
||||
{
|
||||
id: 'advanced',
|
||||
title: isZh ? '老手模式' : 'Advanced Mode',
|
||||
description: isZh
|
||||
? '保持现在的完整配置流程,你自己决定模型、钱包和交易所。'
|
||||
: 'Keep the full manual flow and configure models, wallets, and exchanges yourself.',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs font-medium text-zinc-400">
|
||||
{isZh ? '使用模式' : 'Experience'}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{options.map((option) => {
|
||||
const selected = option.id === mode
|
||||
return (
|
||||
<button
|
||||
key={option.id}
|
||||
type="button"
|
||||
onClick={() => onChange(option.id)}
|
||||
className={`w-full rounded-xl border px-4 py-3 text-left transition-all ${
|
||||
selected
|
||||
? 'border-nofx-gold/60 bg-nofx-gold/10 shadow-[0_0_0_1px_rgba(240,185,11,0.15)]'
|
||||
: 'border-zinc-800 bg-zinc-950/60 hover:border-zinc-700'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 text-sm font-semibold text-white">
|
||||
<span>{option.title}</span>
|
||||
{option.badge ? (
|
||||
<span className="rounded-full bg-nofx-gold px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-black">
|
||||
{option.badge}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="mt-1 text-xs leading-5 text-zinc-400">
|
||||
{option.description}
|
||||
</p>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -4,6 +4,12 @@ import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { Menu, X, ChevronDown, Settings } from 'lucide-react'
|
||||
import { t, type Language } from '../../i18n/translations'
|
||||
import { OFFICIAL_LINKS } from '../../constants/branding'
|
||||
import {
|
||||
getPostAuthPath,
|
||||
getUserMode,
|
||||
setUserMode,
|
||||
type UserMode,
|
||||
} from '../../lib/onboarding'
|
||||
|
||||
type Page =
|
||||
| 'competition'
|
||||
@@ -44,8 +50,21 @@ export default function HeaderBar({
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||
const [languageDropdownOpen, setLanguageDropdownOpen] = useState(false)
|
||||
const [userDropdownOpen, setUserDropdownOpen] = useState(false)
|
||||
const [userMode, setUserModeState] = useState<UserMode>(() => getUserMode() ?? 'advanced')
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
const userDropdownRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const navigateInApp = (path: string) => {
|
||||
navigate(path)
|
||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||
}
|
||||
|
||||
const handleSwitchMode = (nextMode: UserMode) => {
|
||||
setUserMode(nextMode)
|
||||
setUserModeState(nextMode)
|
||||
setUserDropdownOpen(false)
|
||||
navigateInApp(getPostAuthPath(nextMode))
|
||||
}
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
@@ -216,6 +235,15 @@ export default function HeaderBar({
|
||||
<Settings className="w-3.5 h-3.5" />
|
||||
Settings
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSwitchMode(userMode === 'beginner' ? 'advanced' : 'beginner')}
|
||||
className="w-full flex items-center gap-2 px-3 py-2 text-sm transition-colors hover:bg-white/5 text-nofx-text-muted hover:text-white"
|
||||
>
|
||||
<Settings className="w-3.5 h-3.5" />
|
||||
{userMode === 'beginner'
|
||||
? language === 'zh' ? '切到老手模式' : 'Switch to Advanced'
|
||||
: language === 'zh' ? '切到新手模式' : 'Switch to Beginner'}
|
||||
</button>
|
||||
{onLogout && (
|
||||
<button
|
||||
onClick={() => {
|
||||
|
||||
@@ -3,14 +3,19 @@ import { Eye, EyeOff } from 'lucide-react'
|
||||
import { useAuth } from '../../contexts/AuthContext'
|
||||
import { DeepVoidBackground } from '../common/DeepVoidBackground'
|
||||
import { invalidateSystemConfig } from '../../lib/config'
|
||||
import { OnboardingModeSelector } from '../auth/OnboardingModeSelector'
|
||||
import type { UserMode } from '../../lib/onboarding'
|
||||
import { useLanguage } from '../../contexts/LanguageContext'
|
||||
|
||||
export function SetupPage() {
|
||||
const { language } = useLanguage()
|
||||
const { register } = useAuth()
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [mode, setMode] = useState<UserMode>('beginner')
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
@@ -20,11 +25,10 @@ export function SetupPage() {
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
const result = await register(email, password)
|
||||
const result = await register(email, password, undefined, mode)
|
||||
setLoading(false)
|
||||
if (result.success) {
|
||||
invalidateSystemConfig()
|
||||
window.location.href = '/traders'
|
||||
} else {
|
||||
setError(result.message || 'Setup failed, please try again')
|
||||
}
|
||||
@@ -87,6 +91,12 @@ export function SetupPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<OnboardingModeSelector
|
||||
language={language}
|
||||
mode={mode}
|
||||
onChange={setMode}
|
||||
/>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<p className="text-xs text-red-400 bg-red-500/10 border border-red-500/20 rounded-lg px-3 py-2">
|
||||
|
||||
@@ -18,6 +18,7 @@ import { TelegramConfigModal } from './TelegramConfigModal'
|
||||
import { ModelConfigModal } from './ModelConfigModal'
|
||||
import { ConfigStatusGrid } from './ConfigStatusGrid'
|
||||
import { TradersList } from './TradersList'
|
||||
import { BeginnerGuideCards } from './BeginnerGuideCards'
|
||||
import {
|
||||
Bot,
|
||||
Plus,
|
||||
@@ -25,6 +26,12 @@ import {
|
||||
} from 'lucide-react'
|
||||
import { confirmToast } from '../../lib/notify'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
getBeginnerWalletAddress,
|
||||
getUserMode,
|
||||
setBeginnerWalletAddress as persistBeginnerWalletAddress,
|
||||
} from '../../lib/onboarding'
|
||||
import type { Strategy } from '../../types'
|
||||
|
||||
interface AITradersPageProps {
|
||||
onTraderSelect?: (traderId: string) => void
|
||||
@@ -48,6 +55,14 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
const [visibleTraderAddresses, setVisibleTraderAddresses] = useState<Set<string>>(new Set())
|
||||
const [visibleExchangeAddresses, setVisibleExchangeAddresses] = useState<Set<string>>(new Set())
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null)
|
||||
const [quickSetupLoading, setQuickSetupLoading] = useState(false)
|
||||
const [beginnerWalletAddress, setBeginnerWalletAddress] = useState<string | null>(() => getBeginnerWalletAddress())
|
||||
const isBeginnerMode = getUserMode() === 'beginner'
|
||||
|
||||
const navigateInApp = (path: string) => {
|
||||
navigate(path)
|
||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||
}
|
||||
|
||||
// Toggle wallet address visibility for a trader
|
||||
const toggleTraderAddressVisibility = (traderId: string) => {
|
||||
@@ -91,6 +106,11 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
api.getTraders,
|
||||
{ refreshInterval: 5000 }
|
||||
)
|
||||
const { data: strategies } = useSWR<Strategy[]>(
|
||||
user && token ? 'strategies' : null,
|
||||
api.getStrategies,
|
||||
{ refreshInterval: 30000 }
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const loadConfigs = async () => {
|
||||
@@ -115,6 +135,12 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
api.getSupportedModels(),
|
||||
])
|
||||
setAllModels(modelConfigs)
|
||||
const clawWalletAddress =
|
||||
modelConfigs.find((model) => model.provider === 'claw402')?.walletAddress || null
|
||||
if (clawWalletAddress) {
|
||||
setBeginnerWalletAddress(clawWalletAddress)
|
||||
persistBeginnerWalletAddress(clawWalletAddress)
|
||||
}
|
||||
setAllExchanges(exchangeConfigs)
|
||||
setSupportedModels(models)
|
||||
} catch (error) {
|
||||
@@ -616,6 +642,36 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
setShowExchangeModal(true)
|
||||
}
|
||||
|
||||
const handleQuickSetupClaw402 = async () => {
|
||||
if (quickSetupLoading) return
|
||||
|
||||
try {
|
||||
setQuickSetupLoading(true)
|
||||
const result = await api.prepareBeginnerOnboarding()
|
||||
setBeginnerWalletAddress(result.address)
|
||||
const refreshedModels = await api.getModelConfigs()
|
||||
setAllModels(refreshedModels)
|
||||
toast.success(
|
||||
language === 'zh'
|
||||
? 'Claw402 已默认配置为 DeepSeek'
|
||||
: 'Claw402 is configured with DeepSeek by default'
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Failed to quick setup claw402:', error)
|
||||
toast.error(
|
||||
language === 'zh'
|
||||
? '一键配置 Claw402 失败'
|
||||
: 'Failed to quick setup Claw402'
|
||||
)
|
||||
} finally {
|
||||
setQuickSetupLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const claw402Configured = configuredModels.some((model) => model.provider === 'claw402')
|
||||
const hasStrategies = (strategies?.length || 0) > 0
|
||||
const canCreateTrader = configuredModels.length > 0 && configuredExchanges.length > 0
|
||||
|
||||
return (
|
||||
<DeepVoidBackground className="py-8" disableAnimation>
|
||||
<div className="w-full px-4 md:px-8 space-y-8 animate-fade-in">
|
||||
@@ -687,6 +743,21 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isBeginnerMode ? (
|
||||
<BeginnerGuideCards
|
||||
language={language}
|
||||
claw402Ready={claw402Configured}
|
||||
exchangeReady={configuredExchanges.length > 0}
|
||||
strategyReady={hasStrategies}
|
||||
canCreateTrader={canCreateTrader}
|
||||
walletAddress={beginnerWalletAddress}
|
||||
onQuickSetupClaw402={handleQuickSetupClaw402}
|
||||
onOpenExchange={handleAddExchange}
|
||||
onOpenStrategy={() => navigateInApp('/strategy')}
|
||||
onCreateTrader={() => setShowCreateModal(true)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{/* Configuration Status Grid */}
|
||||
<ConfigStatusGrid
|
||||
configuredModels={configuredModels}
|
||||
@@ -715,7 +786,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
copiedId={copiedId}
|
||||
language={language}
|
||||
onTraderSelect={onTraderSelect}
|
||||
onNavigate={(path) => navigate(path)}
|
||||
onNavigate={navigateInApp}
|
||||
onEditTrader={handleEditTrader}
|
||||
onToggleTrader={handleToggleTrader}
|
||||
onToggleCompetition={handleToggleCompetition}
|
||||
|
||||
169
web/src/components/trader/BeginnerGuideCards.tsx
Normal file
169
web/src/components/trader/BeginnerGuideCards.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import { Brain, Landmark, Rocket, Sparkles } from 'lucide-react'
|
||||
|
||||
interface BeginnerGuideCardsProps {
|
||||
language: string
|
||||
claw402Ready: boolean
|
||||
exchangeReady: boolean
|
||||
strategyReady: boolean
|
||||
canCreateTrader: boolean
|
||||
walletAddress?: string | null
|
||||
onQuickSetupClaw402: () => void
|
||||
onOpenExchange: () => void
|
||||
onOpenStrategy: () => void
|
||||
onCreateTrader: () => void
|
||||
}
|
||||
|
||||
function truncateAddress(address: string) {
|
||||
if (address.length <= 12) return address
|
||||
return `${address.slice(0, 6)}...${address.slice(-4)}`
|
||||
}
|
||||
|
||||
export function BeginnerGuideCards({
|
||||
language,
|
||||
claw402Ready,
|
||||
exchangeReady,
|
||||
strategyReady,
|
||||
canCreateTrader,
|
||||
walletAddress,
|
||||
onQuickSetupClaw402,
|
||||
onOpenExchange,
|
||||
onOpenStrategy,
|
||||
onCreateTrader,
|
||||
}: BeginnerGuideCardsProps) {
|
||||
const isZh = language === 'zh'
|
||||
|
||||
const cards = [
|
||||
{
|
||||
key: 'model',
|
||||
icon: Brain,
|
||||
title: isZh ? '1. 极速模型' : '1. Fast AI',
|
||||
desc: isZh
|
||||
? '默认就是 Claw402 + DeepSeek。第一次不用挑模型,先跑起来。'
|
||||
: 'Start with Claw402 + DeepSeek. No model picking needed for the first run.',
|
||||
meta: walletAddress
|
||||
? isZh
|
||||
? `钱包 ${truncateAddress(walletAddress)}`
|
||||
: `Wallet ${truncateAddress(walletAddress)}`
|
||||
: isZh
|
||||
? 'Base 链 USDC 按次付费'
|
||||
: 'Pay per call with Base USDC',
|
||||
ready: claw402Ready,
|
||||
actionLabel: claw402Ready
|
||||
? isZh ? '已配置' : 'Configured'
|
||||
: isZh ? '一键配置' : 'One-click setup',
|
||||
onAction: onQuickSetupClaw402,
|
||||
disabled: claw402Ready,
|
||||
},
|
||||
{
|
||||
key: 'exchange',
|
||||
icon: Landmark,
|
||||
title: isZh ? '2. 连接交易所' : '2. Add Exchange',
|
||||
desc: isZh
|
||||
? '交易所接好以后,AI 才能真正下单。'
|
||||
: 'Connect an exchange so the AI can actually place trades.',
|
||||
meta: exchangeReady
|
||||
? isZh ? '已准备好' : 'Ready'
|
||||
: isZh ? 'Binance / OKX / Bybit / Hyperliquid' : 'Binance / OKX / Bybit / Hyperliquid',
|
||||
ready: exchangeReady,
|
||||
actionLabel: exchangeReady
|
||||
? isZh ? '继续管理' : 'Manage'
|
||||
: isZh ? '去配置' : 'Configure',
|
||||
onAction: onOpenExchange,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
key: 'strategy',
|
||||
icon: Sparkles,
|
||||
title: isZh ? '3. 选择策略' : '3. Pick Strategy',
|
||||
desc: isZh
|
||||
? '先用默认策略也可以,后面再慢慢细调。'
|
||||
: 'You can start with a default strategy and fine-tune later.',
|
||||
meta: strategyReady
|
||||
? isZh ? '已有策略可用' : 'Strategy ready'
|
||||
: isZh ? '可选,但建议提前看一眼' : 'Optional, but worth a quick look',
|
||||
ready: strategyReady,
|
||||
actionLabel: isZh ? '打开策略页' : 'Open strategy',
|
||||
onAction: onOpenStrategy,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
key: 'trader',
|
||||
icon: Rocket,
|
||||
title: isZh ? '4. 创建 Trader' : '4. Create Trader',
|
||||
desc: isZh
|
||||
? '最后一步,把模型和交易所绑在一起,就能开始运行。'
|
||||
: 'Last step: bind your model and exchange, then start running.',
|
||||
meta: canCreateTrader
|
||||
? isZh ? '已经可以创建' : 'Ready to create'
|
||||
: isZh ? '先完成前两步' : 'Finish the first two steps first',
|
||||
ready: canCreateTrader,
|
||||
actionLabel: isZh ? '立即创建' : 'Create now',
|
||||
onAction: onCreateTrader,
|
||||
disabled: !canCreateTrader,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<section className="space-y-4 rounded-[28px] border border-white/10 bg-zinc-950/60 p-5 backdrop-blur-xl">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.3em] text-nofx-gold/80">
|
||||
{isZh ? '新手引导' : 'Quickstart'}
|
||||
</div>
|
||||
<h2 className="mt-1 text-xl font-bold text-white">
|
||||
{isZh ? '先按这 4 步走,最快上手' : 'Follow these 4 steps to get started fast'}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="rounded-full border border-white/10 bg-white/5 px-3 py-1 text-xs text-zinc-400">
|
||||
{isZh ? '老手模式不会看到这块' : 'Hidden in advanced mode'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
{cards.map((card) => {
|
||||
const Icon = card.icon
|
||||
return (
|
||||
<div
|
||||
key={card.key}
|
||||
className="rounded-[22px] border border-white/8 bg-black/25 p-4"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-white/6 text-nofx-gold">
|
||||
<Icon className="h-5 w-5" />
|
||||
</div>
|
||||
<span
|
||||
className={`rounded-full px-2.5 py-1 text-[10px] font-bold uppercase tracking-[0.22em] ${
|
||||
card.ready
|
||||
? 'bg-emerald-500/15 text-emerald-300'
|
||||
: 'bg-zinc-800 text-zinc-400'
|
||||
}`}
|
||||
>
|
||||
{card.ready ? (isZh ? '已就绪' : 'Ready') : (isZh ? '待完成' : 'Pending')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 className="mt-4 text-base font-semibold text-white">{card.title}</h3>
|
||||
<p className="mt-2 min-h-[72px] text-sm leading-6 text-zinc-400">
|
||||
{card.desc}
|
||||
</p>
|
||||
<div className="mt-3 text-xs text-zinc-500">{card.meta}</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={card.onAction}
|
||||
disabled={card.disabled}
|
||||
className={`mt-5 w-full rounded-2xl px-4 py-3 text-sm font-semibold transition ${
|
||||
card.disabled
|
||||
? 'cursor-not-allowed bg-zinc-900 text-zinc-500'
|
||||
: 'bg-nofx-gold text-black hover:bg-yellow-400'
|
||||
}`}
|
||||
>
|
||||
{card.actionLabel}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -92,6 +92,20 @@ export function ConfigStatusGrid({
|
||||
<div className="text-[10px] text-zinc-500 font-mono flex items-center gap-2">
|
||||
{model.customModelName || AI_PROVIDER_CONFIG[model.provider]?.defaultModel || ''}
|
||||
</div>
|
||||
{model.provider === 'claw402' && (model.balanceUsdc || model.walletAddress) ? (
|
||||
<div className="mt-1.5 flex flex-wrap items-center gap-2 text-[10px] font-mono">
|
||||
{model.balanceUsdc ? (
|
||||
<span className="rounded border border-emerald-500/20 bg-emerald-500/10 px-1.5 py-0.5 text-emerald-400">
|
||||
{model.balanceUsdc} USDC
|
||||
</span>
|
||||
) : null}
|
||||
{model.walletAddress ? (
|
||||
<span className="rounded border border-sky-500/20 bg-sky-500/10 px-1.5 py-0.5 text-sky-400">
|
||||
{truncateAddress(model.walletAddress)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ 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'
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
AI_PROVIDER_CONFIG,
|
||||
getShortName,
|
||||
} from './model-constants'
|
||||
import { getBeginnerWalletAddress } from '../../lib/onboarding'
|
||||
|
||||
interface ModelConfigModalProps {
|
||||
allModels: AIModel[]
|
||||
@@ -42,20 +44,22 @@ export function ModelConfigModal({
|
||||
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) ||
|
||||
configuredModels?.find((m) => m.id === selectedModelId)
|
||||
allModels?.find((m) => m.id === selectedModelId) || configuredModel
|
||||
|
||||
useEffect(() => {
|
||||
if (editingModelId && selectedModel) {
|
||||
setApiKey(selectedModel.apiKey || '')
|
||||
setBaseUrl(selectedModel.customApiUrl || '')
|
||||
setModelName(selectedModel.customModelName || '')
|
||||
const modelDetails = configuredModel || selectedModel
|
||||
if (editingModelId && modelDetails) {
|
||||
setApiKey(modelDetails.apiKey || '')
|
||||
setBaseUrl(modelDetails.customApiUrl || '')
|
||||
setModelName(modelDetails.customModelName || '')
|
||||
}
|
||||
}, [editingModelId, selectedModel])
|
||||
}, [editingModelId, configuredModel, selectedModel])
|
||||
|
||||
const handleSelectModel = (modelId: string) => {
|
||||
setSelectedModelId(modelId)
|
||||
@@ -79,7 +83,18 @@ export function ModelConfigModal({
|
||||
|
||||
const availableModels = allModels || []
|
||||
const configuredIds = new Set(configuredModels?.map(m => m.id) || [])
|
||||
const stepLabels = [t('modelConfig.selectModel', language), t('modelConfig.configureApi', language)]
|
||||
const isClaw402Selected = selectedModel?.provider === 'claw402' || selectedModel?.id === 'claw402'
|
||||
const stepLabels = [
|
||||
t('modelConfig.selectModel', language),
|
||||
t(
|
||||
!selectedModel
|
||||
? 'modelConfig.configure'
|
||||
: isClaw402Selected
|
||||
? 'modelConfig.configureWallet'
|
||||
: 'modelConfig.configure',
|
||||
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">
|
||||
@@ -143,6 +158,7 @@ export function ModelConfigModal({
|
||||
<Claw402ConfigForm
|
||||
apiKey={apiKey}
|
||||
modelName={modelName}
|
||||
configuredModel={configuredModel}
|
||||
editingModelId={editingModelId}
|
||||
onApiKeyChange={setApiKey}
|
||||
onModelNameChange={setModelName}
|
||||
@@ -189,6 +205,10 @@ function ModelSelectionStep({
|
||||
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 (
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||
@@ -196,12 +216,11 @@ function ModelSelectionStep({
|
||||
</div>
|
||||
|
||||
{/* Claw402 Featured Card */}
|
||||
{availableModels.some(m => m.provider === 'claw402') && (
|
||||
{claw402Model && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const claw = availableModels.find(m => m.provider === 'claw402')
|
||||
if (claw) onSelectModel(claw.id)
|
||||
onSelectModel(claw402Model.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)' }}
|
||||
@@ -222,7 +241,7 @@ function ModelSelectionStep({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{configuredIds.has(availableModels.find(m => m.provider === 'claw402')?.id || '') && (
|
||||
{configuredIds.has(claw402Model.id) && (
|
||||
<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' }}>
|
||||
@@ -235,23 +254,57 @@ function ModelSelectionStep({
|
||||
GPT · Claude · DeepSeek · Gemini · Grok · Qwen · Kimi
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-4 ml-[52px] text-[11px]" style={{ color: '#A0AEC0' }}>
|
||||
{t('modelConfig.claw402EntryDesc', language)}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-3 sm:grid-cols-4 gap-3">
|
||||
{availableModels.filter(m => m.provider !== 'claw402').map((model) => (
|
||||
<ModelCard
|
||||
key={model.id}
|
||||
model={model}
|
||||
selected={selectedModelId === model.id}
|
||||
onClick={() => onSelectModel(model.id)}
|
||||
configured={configuredIds.has(model.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-xs text-center pt-2" style={{ color: '#848E9C' }}>
|
||||
{t('modelConfig.modelsConfigured', language)}
|
||||
</div>
|
||||
{otherProviders.length > 0 && (
|
||||
<div className="rounded-xl border border-white/10 bg-black/20 overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowOtherProviders((prev) => !prev)}
|
||||
className="w-full flex items-center justify-between px-4 py-4 text-left transition-all hover:bg-white/5"
|
||||
>
|
||||
<div>
|
||||
<div className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||
{t('modelConfig.otherApiEntry', language)}
|
||||
</div>
|
||||
<div className="mt-1 text-xs" style={{ color: '#848E9C' }}>
|
||||
{t('modelConfig.otherApiEntryDesc', language)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="rounded-full border border-white/10 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.2em]" style={{ color: '#A0AEC0' }}>
|
||||
{otherProviders.length} API
|
||||
</span>
|
||||
<span className="text-sm" style={{ color: '#60A5FA' }}>
|
||||
{showOtherProviders ? '−' : '+'}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{showOtherProviders && (
|
||||
<div className="border-t border-white/5 px-4 py-4">
|
||||
<div className="grid grid-cols-3 sm:grid-cols-4 gap-3">
|
||||
{otherProviders.map((model) => (
|
||||
<ModelCard
|
||||
key={model.id}
|
||||
model={model}
|
||||
selected={selectedModelId === model.id}
|
||||
onClick={() => onSelectModel(model.id)}
|
||||
configured={configuredIds.has(model.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-xs text-center pt-3" style={{ color: '#848E9C' }}>
|
||||
{t('modelConfig.modelsConfigured', language)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -259,6 +312,7 @@ function ModelSelectionStep({
|
||||
function Claw402ConfigForm({
|
||||
apiKey,
|
||||
modelName,
|
||||
configuredModel,
|
||||
editingModelId,
|
||||
onApiKeyChange,
|
||||
onModelNameChange,
|
||||
@@ -268,6 +322,7 @@ function Claw402ConfigForm({
|
||||
}: {
|
||||
apiKey: string
|
||||
modelName: string
|
||||
configuredModel: AIModel | null
|
||||
editingModelId: string | null
|
||||
onApiKeyChange: (value: string) => void
|
||||
onModelNameChange: (value: string) => void
|
||||
@@ -278,14 +333,21 @@ function Claw402ConfigForm({
|
||||
const [walletAddress, setWalletAddress] = useState('')
|
||||
const [copiedAddr, setCopiedAddr] = useState(false)
|
||||
const [showDeposit, setShowDeposit] = useState(false)
|
||||
const [showNewWalletBackup, setShowNewWalletBackup] = useState(false)
|
||||
const [newWalletKey, setNewWalletKey] = useState('')
|
||||
const [usdcBalance, setUsdcBalance] = useState<string | null>(null)
|
||||
const [keyError, setKeyError] = useState('')
|
||||
const [validating, setValidating] = useState(false)
|
||||
const [claw402Status, setClaw402Status] = useState<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<string | null>(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 => {
|
||||
@@ -298,8 +360,36 @@ function Claw402ConfigForm({
|
||||
|
||||
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.
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [configuredModel?.walletAddress, localWalletAddress, serverWalletAddress])
|
||||
|
||||
// Debounced validation when apiKey changes
|
||||
useEffect(() => {
|
||||
@@ -347,6 +437,23 @@ 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' },
|
||||
@@ -374,7 +481,7 @@ function Claw402ConfigForm({
|
||||
}
|
||||
}
|
||||
|
||||
const balanceNum = usdcBalance ? parseFloat(usdcBalance) : 0
|
||||
const balanceNum = resolvedUsdcBalance ? parseFloat(resolvedUsdcBalance) : 0
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit} className="space-y-5">
|
||||
@@ -396,6 +503,25 @@ function Claw402ConfigForm({
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-4 flex items-center justify-center gap-3 flex-wrap">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTestConnection}
|
||||
disabled={testing || (!hasExistingWallet && !isKeyValid)}
|
||||
className="inline-flex items-center gap-2 rounded-xl px-4 py-2 text-xs font-semibold transition-all hover:scale-[1.02] disabled:cursor-not-allowed disabled:opacity-50"
|
||||
style={{ background: 'rgba(37, 99, 235, 0.15)', border: '1px solid rgba(37, 99, 235, 0.3)', color: '#60A5FA' }}
|
||||
>
|
||||
<span>🔗</span>
|
||||
{testing ? t('modelConfig.testingConnection', language) : t('modelConfig.testConnection', language)}
|
||||
</button>
|
||||
{claw402Status ? (
|
||||
<div className="text-xs" style={{ color: claw402Status === 'ok' ? '#00E096' : '#F59E0B' }}>
|
||||
{claw402Status === 'ok'
|
||||
? t('modelConfig.claw402Connected', language)
|
||||
: t('modelConfig.claw402Unreachable', language)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step 1: Select AI Model */}
|
||||
@@ -467,6 +593,33 @@ function Claw402ConfigForm({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasExistingWallet && (
|
||||
<div className="p-3 rounded-xl" style={{ background: 'rgba(0, 224, 150, 0.05)', border: '1px solid rgba(0, 224, 150, 0.18)' }}>
|
||||
<div className="text-xs font-semibold mb-1.5" style={{ color: '#00E096' }}>
|
||||
{language === 'zh' ? '已自动提取当前钱包' : 'Current wallet loaded automatically'}
|
||||
</div>
|
||||
<div className="text-[11px] leading-5" style={{ color: '#A0AEC0' }}>
|
||||
{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.'}
|
||||
</div>
|
||||
{!configuredModel?.walletAddress && localWalletAddress ? (
|
||||
<div className="mt-2 text-[10px]" style={{ color: '#848E9C' }}>
|
||||
{language === 'zh'
|
||||
? '当前地址来自本地已保存的新手钱包。'
|
||||
: 'This address comes from the locally saved beginner wallet.'}
|
||||
</div>
|
||||
) : null}
|
||||
{!configuredModel?.walletAddress && !localWalletAddress && serverWalletAddress ? (
|
||||
<div className="mt-2 text-[10px]" style={{ color: '#848E9C' }}>
|
||||
{language === 'zh'
|
||||
? '当前地址来自后端保存的钱包配置。'
|
||||
: 'This address comes from the wallet saved on the server.'}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-xs font-medium" style={{ color: '#A0AEC0' }}>
|
||||
{t('modelConfig.walletPrivateKey', language)}
|
||||
@@ -476,72 +629,30 @@ function Claw402ConfigForm({
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(e) => onApiKeyChange(e.target.value)}
|
||||
placeholder="0x..."
|
||||
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
|
||||
required={!hasExistingWallet}
|
||||
/>
|
||||
{!apiKey && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const res = await fetch('/api/wallet/generate', { method: 'POST' })
|
||||
const data = await res.json()
|
||||
if (data.private_key) {
|
||||
onApiKeyChange(data.private_key)
|
||||
setShowNewWalletBackup(true)
|
||||
setNewWalletKey(data.private_key)
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}}
|
||||
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' }}
|
||||
>
|
||||
{language === 'zh' ? '🔑 创建钱包' : '🔑 Create Wallet'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* New wallet backup warning */}
|
||||
{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 className="text-xs font-bold mb-2" style={{ color: '#EF4444' }}>
|
||||
🚨 {language === 'zh' ? '重要:请立即备份私钥!' : 'Important: Backup your private key NOW!'}
|
||||
</div>
|
||||
<div className="text-[11px] mb-2" style={{ color: '#F87171' }}>
|
||||
{language === 'zh'
|
||||
? '这是你的钱包私钥,丢失后无法恢复,钱包里的资产将永久丢失。请复制并安全保存。'
|
||||
: '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 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' }}>
|
||||
{newWalletKey}
|
||||
</code>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(newWalletKey)
|
||||
setCopiedAddr(true)
|
||||
setTimeout(() => setCopiedAddr(false), 2000)
|
||||
}}
|
||||
className="shrink-0 text-[10px] px-2 py-1 rounded"
|
||||
style={{ background: 'rgba(239,68,68,0.15)', color: '#F87171', border: 'none', cursor: 'pointer' }}
|
||||
>
|
||||
{copiedAddr ? '✅ Copied' : '📋 Copy Key'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-[10px] space-y-1" style={{ color: '#848E9C' }}>
|
||||
<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>
|
||||
{hasExistingWallet && !apiKey ? (
|
||||
<div className="text-[11px] leading-5" style={{ color: '#848E9C' }}>
|
||||
{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.'}
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
<div className="flex items-start gap-1.5 text-[11px]" style={{ color: '#848E9C' }}>
|
||||
<span className="mt-px">🔒</span>
|
||||
@@ -552,7 +663,7 @@ function Claw402ConfigForm({
|
||||
</div>
|
||||
|
||||
{/* Wallet Validation Results */}
|
||||
{apiKey && (
|
||||
{(apiKey || hasExistingWallet) && (
|
||||
<div className="space-y-2 pl-1">
|
||||
{/* Validating spinner */}
|
||||
{validating && (
|
||||
@@ -571,7 +682,7 @@ function Claw402ConfigForm({
|
||||
)}
|
||||
|
||||
{/* Success: address + balance + status */}
|
||||
{walletAddress && !validating && !keyError && (
|
||||
{resolvedWalletAddress && !validating && !keyError && (
|
||||
<>
|
||||
<div className="p-2.5 rounded-lg" style={{ background: 'rgba(96,165,250,0.06)', border: '1px solid rgba(96,165,250,0.15)' }}>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
@@ -581,7 +692,7 @@ function Claw402ConfigForm({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(walletAddress)
|
||||
navigator.clipboard.writeText(resolvedWalletAddress)
|
||||
setCopiedAddr(true)
|
||||
setTimeout(() => setCopiedAddr(false), 2000)
|
||||
}}
|
||||
@@ -591,16 +702,16 @@ function Claw402ConfigForm({
|
||||
{copiedAddr ? '✅' : '📋'}
|
||||
</button>
|
||||
</div>
|
||||
<code className="text-[11px] font-mono block select-all" style={{ color: '#60A5FA' }}>{walletAddress}</code>
|
||||
<code className="text-[11px] font-mono block select-all" style={{ color: '#60A5FA' }}>{resolvedWalletAddress}</code>
|
||||
<div className="text-[10px] mt-1.5" style={{ color: '#F59E0B' }}>
|
||||
⚠️ {language === 'zh' ? '请确认这是你的钱包地址(可在 MetaMask 中核对)' : 'Please confirm this is your wallet address (verify in MetaMask)'}
|
||||
</div>
|
||||
</div>
|
||||
{usdcBalance !== null && (
|
||||
{resolvedUsdcBalance !== null && (
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span>💰</span>
|
||||
<span style={{ color: balanceNum > 0 ? '#00E096' : '#F59E0B' }}>
|
||||
{t('modelConfig.usdcBalance', language)}: ${usdcBalance}
|
||||
{t('modelConfig.usdcBalance', language)}: ${resolvedUsdcBalance}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
@@ -621,17 +732,17 @@ function Claw402ConfigForm({
|
||||
</div>
|
||||
<div className="flex gap-3 items-start mb-3">
|
||||
<div className="shrink-0 p-1.5 rounded-lg" style={{ background: '#fff' }}>
|
||||
<QRCodeSVG value={walletAddress} size={80} level="M" />
|
||||
<QRCodeSVG value={resolvedWalletAddress} size={80} level="M" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-[11px] mb-1" style={{ color: '#A0AEC0' }}>
|
||||
{language === 'zh' ? '扫码或复制地址转账' : 'Scan QR or copy address to transfer'}
|
||||
</div>
|
||||
<code className="text-[10px] font-mono break-all select-all block mb-1.5" style={{ color: '#60A5FA' }}>{walletAddress}</code>
|
||||
<code className="text-[10px] font-mono break-all select-all block mb-1.5" style={{ color: '#60A5FA' }}>{resolvedWalletAddress}</code>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(walletAddress)
|
||||
navigator.clipboard.writeText(resolvedWalletAddress)
|
||||
setCopiedAddr(true)
|
||||
setTimeout(() => setCopiedAddr(false), 2000)
|
||||
}}
|
||||
@@ -650,6 +761,13 @@ function Claw402ConfigForm({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!apiKey && hasExistingWallet && (
|
||||
<div className="text-[11px]" style={{ color: '#848E9C' }}>
|
||||
{language === 'zh'
|
||||
? '当前正在使用这个钱包充值。若要切换钱包,再输入新的私钥并保存即可。'
|
||||
: 'This wallet is currently used for funding. Enter a new private key only if you want to switch wallets.'}
|
||||
</div>
|
||||
)}
|
||||
{claw402Status && (
|
||||
<div className="flex items-center gap-2 text-xs" style={{ color: claw402Status === 'ok' ? '#00E096' : '#EF4444' }}>
|
||||
<span>{claw402Status === 'ok' ? '🟢' : '🔴'}</span>
|
||||
@@ -662,11 +780,11 @@ function Claw402ConfigForm({
|
||||
)}
|
||||
|
||||
{/* Test Connection button */}
|
||||
{isKeyValid && !validating && (
|
||||
{(isKeyValid || hasExistingWallet) && !validating && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTestConnection}
|
||||
disabled={testing}
|
||||
disabled={testing || (!hasExistingWallet && !isKeyValid)}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-all hover:scale-[1.02] disabled:opacity-50"
|
||||
style={{ background: 'rgba(37, 99, 235, 0.15)', border: '1px solid rgba(37, 99, 235, 0.3)', color: '#60A5FA' }}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user