feat(beginner): protect default AI model and prevent repeated onboarding (#1444)

Co-authored-by: Dean <afei.wuhao@gmail.com>
This commit is contained in:
deanokk
2026-03-30 21:04:43 +08:00
committed by GitHub
parent fb0bd13f51
commit 1d6e99c74a
5 changed files with 61 additions and 20 deletions
+2 -2
View File
@@ -23,7 +23,7 @@ import { AuthProvider, useAuth } from './contexts/AuthContext'
import { ConfirmDialogProvider } from './components/common/ConfirmDialog' import { ConfirmDialogProvider } from './components/common/ConfirmDialog'
import { t } from './i18n/translations' import { t } from './i18n/translations'
import { useSystemConfig } from './hooks/useSystemConfig' import { useSystemConfig } from './hooks/useSystemConfig'
import { getUserMode } from './lib/onboarding' import { getUserMode, hasCompletedBeginnerOnboarding } from './lib/onboarding'
import { OFFICIAL_LINKS } from './constants/branding' import { OFFICIAL_LINKS } from './constants/branding'
import type { import type {
@@ -352,7 +352,7 @@ function App() {
}, [route]) }, [route])
const showBeginnerOnboarding = const showBeginnerOnboarding =
route === '/welcome' && (!!user || hasPersistedAuth) && getUserMode() === 'beginner' route === '/welcome' && (!!user || hasPersistedAuth) && getUserMode() === 'beginner' && !hasCompletedBeginnerOnboarding()
// Show loading spinner while checking auth or config // Show loading spinner while checking auth or config
if (isLoading || configLoading) { if (isLoading || configLoading) {
@@ -49,8 +49,12 @@ export function BeginnerGuideCards({
: 'Pay per call with Base USDC', : 'Pay per call with Base USDC',
ready: claw402Ready, ready: claw402Ready,
actionLabel: claw402Ready actionLabel: claw402Ready
? isZh ? '已配置' : 'Configured' ? isZh
: isZh ? '一键配置' : 'One-click setup', ? '配置'
: 'Configured'
: isZh
? '一键配置'
: 'One-click setup',
onAction: onQuickSetupClaw402, onAction: onQuickSetupClaw402,
disabled: claw402Ready, disabled: claw402Ready,
}, },
@@ -62,12 +66,20 @@ export function BeginnerGuideCards({
? '交易所接好以后,AI 才能真正下单。' ? '交易所接好以后,AI 才能真正下单。'
: 'Connect an exchange so the AI can actually place trades.', : 'Connect an exchange so the AI can actually place trades.',
meta: exchangeReady meta: exchangeReady
? isZh ? '已准备好' : 'Ready' ? isZh
: isZh ? 'Binance / OKX / Bybit / Hyperliquid' : 'Binance / OKX / Bybit / Hyperliquid', ? '已准备好'
: 'Ready'
: isZh
? 'Binance / OKX / Bybit / Hyperliquid'
: 'Binance / OKX / Bybit / Hyperliquid',
ready: exchangeReady, ready: exchangeReady,
actionLabel: exchangeReady actionLabel: exchangeReady
? isZh ? '继续管理' : 'Manage' ? isZh
: isZh ? '去配置' : 'Configure', ? '继续管理'
: 'Manage'
: isZh
? '去配置'
: 'Configure',
onAction: onOpenExchange, onAction: onOpenExchange,
disabled: false, disabled: false,
}, },
@@ -79,8 +91,12 @@ export function BeginnerGuideCards({
? '先用默认策略也可以,后面再慢慢细调。' ? '先用默认策略也可以,后面再慢慢细调。'
: 'You can start with a default strategy and fine-tune later.', : 'You can start with a default strategy and fine-tune later.',
meta: strategyReady meta: strategyReady
? isZh ? '已有策略可用' : 'Strategy ready' ? isZh
: isZh ? '可选,但建议提前看一眼' : 'Optional, but worth a quick look', ? '已有策略可用'
: 'Strategy ready'
: isZh
? '可选,但建议提前看一眼'
: 'Optional, but worth a quick look',
ready: strategyReady, ready: strategyReady,
actionLabel: isZh ? '打开策略页' : 'Open strategy', actionLabel: isZh ? '打开策略页' : 'Open strategy',
onAction: onOpenStrategy, onAction: onOpenStrategy,
@@ -94,8 +110,12 @@ export function BeginnerGuideCards({
? '最后一步,把模型和交易所绑在一起,就能开始运行。' ? '最后一步,把模型和交易所绑在一起,就能开始运行。'
: 'Last step: bind your model and exchange, then start running.', : 'Last step: bind your model and exchange, then start running.',
meta: canCreateTrader meta: canCreateTrader
? isZh ? '已经可以创建' : 'Ready to create' ? isZh
: isZh ? '先完成前两步' : 'Finish the first two steps first', ? '已经可以创建'
: 'Ready to create'
: isZh
? '先完成前两步'
: 'Finish the first two steps first',
ready: canCreateTrader, ready: canCreateTrader,
actionLabel: isZh ? '立即创建' : 'Create now', actionLabel: isZh ? '立即创建' : 'Create now',
onAction: onCreateTrader, onAction: onCreateTrader,
@@ -111,12 +131,14 @@ export function BeginnerGuideCards({
{isZh ? '新手引导' : 'Quickstart'} {isZh ? '新手引导' : 'Quickstart'}
</div> </div>
<h2 className="mt-1 text-xl font-bold text-white"> <h2 className="mt-1 text-xl font-bold text-white">
{isZh ? '先按这 4 步走,最快上手' : 'Follow these 4 steps to get started fast'} {isZh
? '先按这 4 步走,最快上手'
: 'Follow these 4 steps to get started fast'}
</h2> </h2>
</div> </div>
<div className="rounded-full border border-white/10 bg-white/5 px-3 py-1 text-xs text-zinc-400"> {/* <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'} {isZh ? '老手模式不会看到这块' : 'Hidden in advanced mode'}
</div> </div> */}
</div> </div>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4"> <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
@@ -138,11 +160,19 @@ export function BeginnerGuideCards({
: 'bg-zinc-800 text-zinc-400' : 'bg-zinc-800 text-zinc-400'
}`} }`}
> >
{card.ready ? (isZh ? '已就绪' : 'Ready') : (isZh ? '待完成' : 'Pending')} {card.ready
? isZh
? '已就绪'
: 'Ready'
: isZh
? '待完成'
: 'Pending'}
</span> </span>
</div> </div>
<h3 className="mt-4 text-base font-semibold text-white">{card.title}</h3> <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"> <p className="mt-2 min-h-[72px] text-sm leading-6 text-zinc-400">
{card.desc} {card.desc}
</p> </p>
@@ -13,7 +13,7 @@ import {
AI_PROVIDER_CONFIG, AI_PROVIDER_CONFIG,
getShortName, getShortName,
} from './model-constants' } from './model-constants'
import { getBeginnerWalletAddress } from '../../lib/onboarding' import { getBeginnerWalletAddress, getUserMode } from '../../lib/onboarding'
interface ModelConfigModalProps { interface ModelConfigModalProps {
allModels: AIModel[] allModels: AIModel[]
@@ -84,6 +84,7 @@ export function ModelConfigModal({
const availableModels = allModels || [] const availableModels = allModels || []
const configuredIds = new Set(configuredModels?.map(m => m.id) || []) const configuredIds = new Set(configuredModels?.map(m => m.id) || [])
const isClaw402Selected = selectedModel?.provider === 'claw402' || selectedModel?.id === 'claw402' const isClaw402Selected = 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(
@@ -117,7 +118,7 @@ export function ModelConfigModal({
</h3> </h3>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{editingModelId && ( {editingModelId && !isBeginnerDefaultModel && (
<button <button
type="button" type="button"
onClick={() => onDelete(editingModelId)} onClick={() => onDelete(editingModelId)}
+9
View File
@@ -2,6 +2,7 @@ export type UserMode = 'beginner' | 'advanced'
const USER_MODE_KEY = 'nofx_user_mode' const USER_MODE_KEY = 'nofx_user_mode'
const BEGINNER_WALLET_ADDRESS_KEY = 'nofx_beginner_wallet_address' const BEGINNER_WALLET_ADDRESS_KEY = 'nofx_beginner_wallet_address'
const BEGINNER_ONBOARDING_COMPLETED_KEY = 'nofx_beginner_onboarding_completed'
export function getUserMode(): UserMode | null { export function getUserMode(): UserMode | null {
const value = localStorage.getItem(USER_MODE_KEY) const value = localStorage.getItem(USER_MODE_KEY)
@@ -26,3 +27,11 @@ export function setBeginnerWalletAddress(address: string) {
export function getBeginnerWalletAddress(): string | null { export function getBeginnerWalletAddress(): string | null {
return localStorage.getItem(BEGINNER_WALLET_ADDRESS_KEY) return localStorage.getItem(BEGINNER_WALLET_ADDRESS_KEY)
} }
export function hasCompletedBeginnerOnboarding(): boolean {
return localStorage.getItem(BEGINNER_ONBOARDING_COMPLETED_KEY) === 'true'
}
export function markBeginnerOnboardingCompleted() {
localStorage.setItem(BEGINNER_ONBOARDING_COMPLETED_KEY, 'true')
}
+2 -1
View File
@@ -12,7 +12,7 @@ import { toast } from 'sonner'
import { useLanguage } from '../contexts/LanguageContext' import { useLanguage } from '../contexts/LanguageContext'
import { api } from '../lib/api' import { api } from '../lib/api'
import type { BeginnerOnboardingResponse } from '../types' import type { BeginnerOnboardingResponse } from '../types'
import { setBeginnerWalletAddress } from '../lib/onboarding' import { setBeginnerWalletAddress, markBeginnerOnboardingCompleted } from '../lib/onboarding'
export function BeginnerOnboardingPage() { export function BeginnerOnboardingPage() {
const { language } = useLanguage() const { language } = useLanguage()
@@ -78,6 +78,7 @@ export function BeginnerOnboardingPage() {
} }
const handleContinue = () => { const handleContinue = () => {
markBeginnerOnboardingCompleted()
window.history.pushState({}, '', '/traders') window.history.pushState({}, '', '/traders')
window.dispatchEvent(new PopStateEvent('popstate')) window.dispatchEvent(new PopStateEvent('popstate'))
} }