fix: guard short trader ID, i18n setup page, simplify onboarding UX

- main.go: prevent panic when trader ID < 8 chars
- SetupPage: add zh/en i18n labels
- BeginnerOnboardingPage: show private key by default, simplify code
This commit is contained in:
shinchan-zhai
2026-03-28 14:14:20 +08:00
parent 9176aa9844
commit cab58afe6d
3 changed files with 253 additions and 209 deletions
+6 -2
View File
@@ -118,8 +118,12 @@ func main() {
if t.IsRunning {
status = "✅ Running"
}
logger.Infof(" • %s [%s] %s - AI Model: %s, Exchange: %s",
t.Name, t.ID[:8], status, t.AIModelID, t.ExchangeID)
idShort := t.ID
if len(idShort) > 8 {
idShort = idShort[:8]
}
logger.Infof(" • %s [%s] %s - AI Model: %s, Exchange: %s",
t.Name, idShort, status, t.AIModelID, t.ExchangeID)
}
}
+142 -25
View File
@@ -1,14 +1,62 @@
import React, { useState } from 'react'
import { Eye, EyeOff } from 'lucide-react'
import { Eye, EyeOff, Globe } 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'
import type { Language } from '../../i18n/translations'
const labels = {
zh: {
welcome: '欢迎使用 NOFX',
subtitle: '创建账号开始使用',
email: '邮箱',
emailPlaceholder: 'you@example.com',
password: '密码',
passwordPlaceholder: '至少 8 个字符',
passwordError: '密码至少需要 8 个字符',
submit: '开始使用',
submitting: '创建中...',
setupFailed: '创建失败,请重试',
singleUser: '单用户系统 — 这是唯一的账号',
},
en: {
welcome: 'Welcome to NOFX',
subtitle: 'Create your account to get started',
email: 'Email',
emailPlaceholder: 'you@example.com',
password: 'Password',
passwordPlaceholder: 'At least 8 characters',
passwordError: 'Password must be at least 8 characters',
submit: 'Get Started',
submitting: 'Creating account...',
setupFailed: 'Setup failed, please try again',
singleUser: 'Single-user system — this is the only account',
},
id: {
welcome: 'Selamat Datang di NOFX',
subtitle: 'Buat akun untuk memulai',
email: 'Email',
emailPlaceholder: 'you@example.com',
password: 'Kata Sandi',
passwordPlaceholder: 'Minimal 8 karakter',
passwordError: 'Kata sandi minimal 8 karakter',
submit: 'Mulai',
submitting: 'Membuat akun...',
setupFailed: 'Gagal membuat akun, coba lagi',
singleUser: 'Sistem pengguna tunggal — ini satu-satunya akun',
},
} as const
const langOptions: { value: Language; label: string }[] = [
{ value: 'en', label: 'English' },
{ value: 'zh', label: '中文' },
{ value: 'id', label: 'Bahasa' },
]
export function SetupPage() {
const { language } = useLanguage()
const { language, setLanguage } = useLanguage()
const { register } = useAuth()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
@@ -17,11 +65,13 @@ export function SetupPage() {
const [loading, setLoading] = useState(false)
const [mode, setMode] = useState<UserMode>('beginner')
const l = labels[language as keyof typeof labels] || labels.en
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError('')
if (password.length < 8) {
setError('Password must be at least 8 characters')
setError(l.passwordError)
return
}
setLoading(true)
@@ -30,40 +80,100 @@ export function SetupPage() {
if (result.success) {
invalidateSystemConfig()
} else {
setError(result.message || 'Setup failed, please try again')
setError(result.message || l.setupFailed)
}
}
return (
<DeepVoidBackground disableAnimation>
<div className="flex-1 flex items-center justify-center px-4 py-16">
<div className="w-full max-w-sm">
<div className="relative min-h-screen w-full overflow-hidden bg-[#0a0a0f]">
{/* Decorative background - simulates the main app behind a modal */}
{/* Grid */}
<div className="absolute inset-0 pointer-events-none">
<div className="absolute inset-x-0 bottom-0 h-[60vh] bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:40px_40px] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,#000_70%,transparent_100%)] opacity-40" style={{ transform: 'perspective(500px) rotateX(60deg) translateY(80px) scale(2)' }} />
</div>
{/* Glow spots */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute top-[10%] left-[15%] w-[500px] h-[500px] bg-nofx-gold/8 rounded-full blur-[150px]" />
<div className="absolute bottom-[5%] right-[10%] w-[400px] h-[400px] bg-indigo-500/6 rounded-full blur-[140px]" />
<div className="absolute top-[40%] right-[30%] w-[300px] h-[300px] bg-emerald-500/4 rounded-full blur-[120px]" />
</div>
{/* Faux UI elements in background to simulate the app */}
<div className="absolute inset-0 pointer-events-none opacity-[0.06]">
{/* Fake header bar */}
<div className="h-14 border-b border-white/20 flex items-center px-6 gap-4">
<div className="w-8 h-8 rounded-lg bg-white/40" />
<div className="h-3 w-20 rounded bg-white/30" />
<div className="h-3 w-16 rounded bg-white/20 ml-4" />
<div className="h-3 w-16 rounded bg-white/20" />
<div className="h-3 w-16 rounded bg-white/20" />
</div>
{/* Fake content cards */}
<div className="p-6 grid grid-cols-4 gap-4 mt-2">
{[...Array(4)].map((_, i) => (
<div key={i} className="h-24 rounded-xl border border-white/15 bg-white/5" />
))}
</div>
<div className="px-6 mt-2">
<div className="h-64 rounded-xl border border-white/15 bg-white/5" />
</div>
</div>
{/* Blur overlay */}
<div className="absolute inset-0 backdrop-blur-md bg-black/60" />
{/* Language switcher */}
<div className="fixed top-4 right-4 z-50">
<div className="flex items-center gap-1.5 rounded-xl border border-white/10 bg-white/5 backdrop-blur-sm px-2 py-1.5">
<Globe className="h-3.5 w-3.5 text-zinc-500" />
{langOptions.map((opt) => (
<button
key={opt.value}
type="button"
onClick={() => setLanguage(opt.value)}
className={`rounded-lg px-2 py-1 text-xs font-medium transition ${
language === opt.value
? 'bg-nofx-gold/15 text-nofx-gold'
: 'text-zinc-500 hover:text-zinc-300'
}`}
>
{opt.label}
</button>
))}
</div>
</div>
{/* Modal card */}
<div className="relative z-10 flex min-h-screen items-center justify-center px-4 py-16">
<div className="w-full max-w-sm animate-[fadeInUp_0.4s_ease-out]">
{/* Logo + Title */}
<div className="text-center mb-10">
<div className="flex justify-center mb-5">
<div className="text-center mb-8">
<div className="flex justify-center mb-4">
<div className="relative">
<div className="absolute -inset-3 bg-nofx-gold/15 rounded-full blur-2xl" />
<img src="/icons/nofx.svg" alt="NOFX" className="w-14 h-14 relative z-10" />
<div className="absolute -inset-4 bg-nofx-gold/20 rounded-full blur-2xl" />
<img src="/icons/nofx.svg" alt="NOFX" className="w-14 h-14 relative z-10 drop-shadow-[0_0_15px_rgba(240,185,11,0.3)]" />
</div>
</div>
<h1 className="text-2xl font-bold text-white mb-1.5">Welcome to NOFX</h1>
<p className="text-zinc-500 text-sm">Create your account to get started</p>
<h1 className="text-2xl font-bold text-white mb-1.5">{l.welcome}</h1>
<p className="text-zinc-500 text-sm">{l.subtitle}</p>
</div>
{/* Card */}
<div className="bg-zinc-900/60 backdrop-blur-xl border border-zinc-800/80 rounded-2xl p-8 shadow-2xl">
<div className="bg-zinc-900/80 backdrop-blur-2xl border border-white/10 rounded-2xl p-8 shadow-[0_25px_60px_-15px_rgba(0,0,0,0.5),0_0_40px_-10px_rgba(240,185,11,0.08)]">
<form onSubmit={handleSubmit} className="space-y-5">
{/* Email */}
<div>
<label className="block text-xs font-medium text-zinc-400 mb-2">Email</label>
<label className="block text-xs font-medium text-zinc-400 mb-2">{l.email}</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full bg-zinc-950/80 border border-zinc-700/80 rounded-xl px-4 py-3 text-sm text-white placeholder-zinc-600 focus:outline-none focus:border-nofx-gold/60 focus:ring-1 focus:ring-nofx-gold/30 transition-all"
placeholder="you@example.com"
className="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-3 text-sm text-white placeholder-zinc-600 focus:outline-none focus:border-nofx-gold/60 focus:ring-1 focus:ring-nofx-gold/30 transition-all"
placeholder={l.emailPlaceholder}
required
autoFocus
/>
@@ -71,14 +181,14 @@ export function SetupPage() {
{/* Password */}
<div>
<label className="block text-xs font-medium text-zinc-400 mb-2">Password</label>
<label className="block text-xs font-medium text-zinc-400 mb-2">{l.password}</label>
<div className="relative">
<input
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full bg-zinc-950/80 border border-zinc-700/80 rounded-xl px-4 py-3 pr-11 text-sm text-white placeholder-zinc-600 focus:outline-none focus:border-nofx-gold/60 focus:ring-1 focus:ring-nofx-gold/30 transition-all"
placeholder="At least 8 characters"
className="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-3 pr-11 text-sm text-white placeholder-zinc-600 focus:outline-none focus:border-nofx-gold/60 focus:ring-1 focus:ring-nofx-gold/30 transition-all"
placeholder={l.passwordPlaceholder}
required
/>
<button
@@ -108,18 +218,25 @@ export function SetupPage() {
<button
type="submit"
disabled={loading}
className="w-full bg-nofx-gold hover:bg-yellow-400 active:scale-[0.98] text-black font-semibold py-3 rounded-xl text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed mt-2"
className="w-full bg-nofx-gold hover:bg-yellow-400 active:scale-[0.98] text-black font-semibold py-3 rounded-xl text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed mt-2 shadow-[0_0_20px_rgba(240,185,11,0.2)]"
>
{loading ? 'Creating account...' : 'Get Started'}
{loading ? l.submitting : l.submit}
</button>
</form>
</div>
<p className="text-center text-xs text-zinc-600 mt-6">
Single-user system &mdash; this is the only account
{l.singleUser}
</p>
</div>
</div>
</DeepVoidBackground>
<style>{`
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
`}</style>
</div>
)
}
+105 -182
View File
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import { Copy, Eye, EyeOff, RefreshCw, Shield, Wallet, Sparkles } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { Copy, Eye, EyeOff, RefreshCw, Shield, Wallet } from 'lucide-react'
import { QRCodeSVG } from 'qrcode.react'
import { toast } from 'sonner'
import { DeepVoidBackground } from '../components/common/DeepVoidBackground'
@@ -13,64 +13,33 @@ export function BeginnerOnboardingPage() {
const [data, setData] = useState<BeginnerOnboardingResponse | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [showPrivateKey, setShowPrivateKey] = useState(false)
const [showPrivateKey, setShowPrivateKey] = useState(true)
const [refreshingBalance, setRefreshingBalance] = useState(false)
const hasRequestedRef = useRef(false)
const isZh = language === 'zh'
const loadOnboarding = async (showLoading: boolean) => {
if (showLoading) {
setLoading(true)
} else {
setRefreshingBalance(true)
}
if (showLoading) setLoading(true)
else setRefreshingBalance(true)
setError('')
try {
const result = await api.prepareBeginnerOnboarding()
setData(result)
setBeginnerWalletAddress(result.address)
} catch (err) {
setError(
err instanceof Error
? err.message
: isZh
? '新手钱包准备失败'
: 'Failed to prepare beginner wallet'
)
setError(err instanceof Error ? err.message : isZh ? '新手钱包准备失败' : 'Failed to prepare beginner wallet')
} finally {
if (showLoading) {
setLoading(false)
} else {
setRefreshingBalance(false)
}
if (showLoading) setLoading(false)
else setRefreshingBalance(false)
}
}
useEffect(() => {
if (hasRequestedRef.current) {
return
}
if (hasRequestedRef.current) return
hasRequestedRef.current = true
void loadOnboarding(true)
}, [])
const hints = useMemo(
() =>
isZh
? [
'这是你的专属 Base 钱包,只用于后续调用大模型。',
'请保存私钥。丢失后无法恢复。',
'只往这个地址充值 Base 链 USDC,不要充到别的链。',
]
: [
'This dedicated Base wallet is only used to pay for model calls.',
'Save the private key now. It cannot be recovered later.',
'Deposit USDC on Base only. Do not send funds from another chain.',
],
[isZh]
)
const copyText = async (value: string, label: string) => {
try {
await navigator.clipboard.writeText(value)
@@ -87,177 +56,131 @@ export function BeginnerOnboardingPage() {
return (
<DeepVoidBackground disableAnimation>
<div className="mx-auto flex min-h-screen max-w-5xl items-center px-4 py-12">
<div className="grid w-full gap-8 lg:grid-cols-[1.05fr_0.95fr]">
<section className="rounded-[28px] border border-white/10 bg-zinc-950/70 p-8 shadow-2xl backdrop-blur-xl">
<div className="mb-6 flex items-center gap-3">
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-nofx-gold/15 text-nofx-gold">
<Shield className="h-6 w-6" />
</div>
<div>
<div className="text-xs font-semibold uppercase tracking-[0.28em] text-nofx-gold/80">
{isZh ? '新手保护' : 'Beginner Guard'}
</div>
<h1 className="mt-1 text-3xl font-bold text-white">
{isZh ? '钱包已经帮你准备好了' : 'Your wallet is ready'}
</h1>
</div>
<div className="mx-auto flex h-screen max-w-4xl flex-col justify-center px-4">
{/* Header - compact */}
<div className="mb-5 flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-2xl bg-nofx-gold/15 text-nofx-gold">
<Shield className="h-5 w-5" />
</div>
<div>
<div className="text-[10px] font-semibold uppercase tracking-[0.3em] text-nofx-gold/80">
{isZh ? '新手保护' : 'Beginner Guard'}
</div>
<h1 className="text-xl font-bold text-white">
{isZh ? '钱包已经帮你准备好了' : 'Your wallet is ready'}
</h1>
</div>
<div className="ml-auto text-xs text-zinc-500">
Claw402 + DeepSeek · {isZh ? '按次付费' : 'Pay per call'}
</div>
</div>
<p className="max-w-xl text-sm leading-7 text-zinc-300">
{isZh
? '我们已经为你生成了一个专属钱包,并默认接入 Claw402 + DeepSeek。你现在只需要保存私钥,然后往这个地址充值 Base 链 USDC,后面调用大模型时会自动从这里扣费。'
: 'We generated a dedicated wallet for you and preconfigured Claw402 + DeepSeek. Save the private key, then deposit Base USDC to this address so future model calls can be paid automatically.'}
</p>
{error ? (
<div className="mb-4 rounded-xl border border-red-500/20 bg-red-500/10 px-4 py-2 text-sm text-red-300">{error}</div>
) : null}
<div className="mt-6 grid gap-3">
{hints.map((hint) => (
<div
key={hint}
className="flex items-start gap-3 rounded-2xl border border-white/8 bg-white/5 px-4 py-3"
>
<Sparkles className="mt-0.5 h-4 w-4 shrink-0 text-nofx-gold" />
<div className="text-sm leading-6 text-zinc-300">{hint}</div>
</div>
))}
{/* Main card */}
<section className="rounded-[24px] border border-white/10 bg-zinc-950/70 shadow-2xl backdrop-blur-xl">
{loading ? (
<div className="flex h-[400px] items-center justify-center text-sm text-zinc-400">
{isZh ? '正在准备你的 Base 钱包...' : 'Preparing your Base wallet...'}
</div>
<div className="mt-8 rounded-[24px] border border-sky-500/20 bg-sky-500/5 p-5">
<div className="flex items-center gap-2 text-sm font-semibold text-sky-300">
<Wallet className="h-4 w-4" />
<span>{isZh ? '为什么要充值?' : 'Why fund this wallet?'}</span>
</div>
<p className="mt-2 text-sm leading-6 text-zinc-300">
{isZh
? '这里只负责大模型调用费用,不会自动替你充值交易所。先充少量 USDC 就够了,通常 $5-$10 可以用很久。'
: 'This wallet only covers LLM usage costs. It does not fund your exchange automatically. A small amount of USDC is enough to get started, usually $5-$10.'}
</p>
</div>
{error ? (
<div className="mt-6 rounded-2xl border border-red-500/20 bg-red-500/10 px-4 py-3 text-sm text-red-300">
{error}
</div>
) : null}
</section>
<section className="rounded-[28px] border border-white/10 bg-black/60 p-8 shadow-2xl backdrop-blur-xl">
{loading ? (
<div className="flex min-h-[420px] items-center justify-center text-sm text-zinc-400">
{isZh ? '正在准备你的 Base 钱包...' : 'Preparing your Base wallet...'}
</div>
) : data ? (
<div className="space-y-6">
<div className="rounded-[24px] border border-white/10 bg-white/5 p-5">
<div className="text-xs uppercase tracking-[0.28em] text-zinc-500">
{isZh ? '默认模型' : 'Default Model'}
</div>
<div className="mt-2 text-2xl font-bold text-white">Claw402 + DeepSeek</div>
<div className="mt-2 text-sm text-zinc-400">
{isZh ? '按次付费,无需 API Key' : 'Pay per call, no API key needed'}
</div>
) : data ? (
<div className="grid gap-0 lg:grid-cols-[280px_1fr]">
{/* Left: QR + Balance */}
<div className="flex flex-col items-center border-b border-white/8 p-6 lg:border-b-0 lg:border-r">
<div className="rounded-2xl bg-white p-2.5">
<QRCodeSVG value={data.address} size={120} level="M" />
</div>
<div className="rounded-[24px] border border-white/10 bg-white p-5 text-center">
<div className="inline-flex rounded-2xl bg-white p-3">
<QRCodeSVG value={data.address} size={180} level="M" />
</div>
<div className="mt-4 text-sm font-semibold text-zinc-900">
{isZh ? '充值地址(Base 链 USDC' : 'Deposit Address (Base USDC)'}
</div>
<div className="mt-2 break-all rounded-2xl bg-zinc-100 px-3 py-3 font-mono text-xs text-zinc-700">
{data.address}
<div className="mt-3 text-xs font-medium text-zinc-400">
{isZh ? '充值地址(Base USDC' : 'Deposit (Base USDC)'}
</div>
<div className="mt-3 flex items-center gap-2 rounded-xl border border-emerald-500/20 bg-emerald-500/8 px-3 py-2">
<div>
<div className="text-lg font-bold text-emerald-300">{data.balance_usdc} USDC</div>
</div>
<button
type="button"
onClick={() => copyText(data.address, isZh ? '地址' : 'Address')}
className="mt-3 inline-flex items-center gap-2 rounded-xl bg-zinc-900 px-4 py-2 text-sm font-semibold text-white transition hover:bg-zinc-800"
onClick={() => void loadOnboarding(false)}
disabled={refreshingBalance}
className="rounded-lg border border-emerald-500/20 p-1.5 text-emerald-400 transition hover:bg-emerald-500/10 disabled:opacity-50"
>
<Copy className="h-4 w-4" />
{isZh ? '复制地址' : 'Copy address'}
<RefreshCw className={`h-3 w-3 ${refreshingBalance ? 'animate-spin' : ''}`} />
</button>
<div className="mt-4 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-left">
<div className="flex items-center justify-between gap-3">
<div>
<div className="text-[11px] font-semibold uppercase tracking-[0.22em] text-emerald-700">
{isZh ? '当前余额' : 'Current Balance'}
</div>
<div className="mt-1 text-2xl font-bold text-emerald-900">
{data.balance_usdc} USDC
</div>
<div className="mt-1 text-xs text-emerald-700/80">
{isZh ? 'Base 链钱包余额' : 'Base wallet balance'}
</div>
</div>
<button
type="button"
onClick={() => void loadOnboarding(false)}
disabled={refreshingBalance}
className="inline-flex items-center gap-2 rounded-xl border border-emerald-300 bg-white px-3 py-2 text-xs font-semibold text-emerald-800 transition hover:bg-emerald-100 disabled:cursor-not-allowed disabled:opacity-60"
>
<RefreshCw className={`h-3.5 w-3.5 ${refreshingBalance ? 'animate-spin' : ''}`} />
{isZh ? '刷新余额' : 'Refresh'}
</button>
</div>
</div>
</div>
<div className="mt-2 text-[11px] text-zinc-600">
{isZh ? '$5-$10 可以用很久' : '$5-$10 lasts a long time'}
</div>
</div>
<div className="rounded-[24px] border border-amber-500/20 bg-amber-500/8 p-5">
<div className="flex items-center justify-between gap-3">
<div>
<div className="text-sm font-semibold text-amber-200">
{isZh ? '钱包私钥' : 'Wallet Private Key'}
</div>
<div className="mt-1 text-xs leading-5 text-amber-100/75">
{isZh ? '请先备份,再进入下一步。' : 'Back this up before you continue.'}
</div>
{/* Right: Address + Key + Action */}
<div className="flex flex-col gap-4 p-6">
{/* Address */}
<div>
<div className="flex items-center gap-2 text-xs font-medium text-zinc-400">
<Wallet className="h-3.5 w-3.5 text-nofx-gold" />
{isZh ? '钱包地址' : 'Wallet Address'}
</div>
<div className="mt-1.5 flex items-center gap-2">
<div className="min-w-0 flex-1 truncate rounded-lg bg-black/30 px-3 py-2 font-mono text-xs text-zinc-300">
{data.address}
</div>
<button
type="button"
onClick={() => setShowPrivateKey((prev) => !prev)}
className="rounded-xl border border-amber-400/20 px-3 py-2 text-amber-200 transition hover:bg-amber-400/10"
onClick={() => copyText(data.address, isZh ? '地址' : 'Address')}
className="shrink-0 rounded-lg bg-white/10 p-2 text-zinc-400 transition hover:bg-white/15 hover:text-white"
>
{showPrivateKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
<Copy className="h-3.5 w-3.5" />
</button>
</div>
<div className="mt-4 break-all rounded-2xl bg-black/25 px-3 py-3 font-mono text-xs text-amber-50">
{showPrivateKey ? data.private_key : '0x' + '•'.repeat(64)}
</div>
<button
type="button"
onClick={() => copyText(data.private_key, isZh ? '私钥' : 'Private key')}
className="mt-3 inline-flex items-center gap-2 rounded-xl border border-amber-300/20 px-4 py-2 text-sm font-semibold text-amber-100 transition hover:bg-amber-400/10"
>
<Copy className="h-4 w-4" />
{isZh ? '复制私钥' : 'Copy private key'}
</button>
</div>
<div className="rounded-[24px] border border-white/10 bg-white/5 p-4 text-xs leading-6 text-zinc-400">
<div>
{data.env_saved
? isZh
? `已同步保存到环境文件:${data.env_path || '.env'}`
: `Also saved to env: ${data.env_path || '.env'}`
: isZh
? '当前运行环境没有成功写回 .env,但产品已完成默认配置。'
: 'The app is configured, but this runtime could not write back to .env.'}
{/* Private Key */}
<div>
<div className="flex items-center gap-2 text-xs font-medium text-amber-300/80">
<Shield className="h-3.5 w-3.5" />
{isZh ? '私钥 — 请立即备份' : 'Private Key — back up now'}
<button
type="button"
onClick={() => setShowPrivateKey((p) => !p)}
className="ml-auto rounded-lg p-1 text-amber-300/60 transition hover:text-amber-200"
>
{showPrivateKey ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
</button>
</div>
<div className="mt-1.5 flex items-center gap-2">
<div className="min-w-0 flex-1 truncate rounded-lg bg-amber-500/8 border border-amber-500/15 px-3 py-2 font-mono text-xs text-amber-100">
{showPrivateKey ? data.private_key : '0x' + '•'.repeat(64)}
</div>
<button
type="button"
onClick={() => copyText(data.private_key, isZh ? '私钥' : 'Private key')}
className="shrink-0 rounded-lg bg-amber-500/10 border border-amber-500/15 p-2 text-amber-300 transition hover:bg-amber-500/20"
>
<Copy className="h-3.5 w-3.5" />
</button>
</div>
{data.env_warning ? <div className="mt-2 text-amber-300">{data.env_warning}</div> : null}
</div>
{/* Tips */}
<div className="rounded-lg bg-white/3 border border-white/5 px-3 py-2 text-[11px] leading-5 text-zinc-500">
{isZh
? '• 此钱包仅用于大模型调用费用,不会自动充值交易所 • 私钥丢失后无法恢复 • 只充 Base 链 USDC'
: '• This wallet only covers LLM costs, not exchange funding • Private key cannot be recovered • Base USDC only'}
</div>
{/* Continue */}
<button
type="button"
onClick={handleContinue}
className="w-full rounded-2xl bg-nofx-gold px-5 py-4 text-sm font-bold text-black transition hover:bg-yellow-400"
className="mt-auto w-full rounded-xl bg-nofx-gold px-5 py-3 text-sm font-bold text-black transition hover:bg-yellow-400"
>
{isZh ? '我已保存,进入下一步' : 'I saved it, continue'}
{isZh ? '我已保存,进入下一步' : 'I saved it, continue'}
</button>
</div>
) : null}
</section>
</div>
</div>
) : null}
</section>
</div>
</DeepVoidBackground>
)