From 8c92eec94c7181814a523e7a00b3957792413fa0 Mon Sep 17 00:00:00 2001 From: kev1n77 Date: Wed, 25 Mar 2026 15:03:18 +0800 Subject: [PATCH 1/4] chore: remove onboarding page --- BitFun-Installer/scripts/sync-model-i18n.cjs | 65 +- BitFun-Installer/src/i18n/locales/en.json | 22 +- BitFun-Installer/src/i18n/locales/zh.json | 24 +- src/web-ui/src/app/App.tsx | 51 - .../src/app/scenes/agents/AgentsView.scss | 2 +- src/web-ui/src/features/index.ts | 6 - .../components/OnboardingWizard.scss | 1356 ----------------- .../components/OnboardingWizard.tsx | 336 ---- .../features/onboarding/components/index.ts | 6 - .../components/steps/CompletionStep.tsx | 59 - .../components/steps/LanguageStep.tsx | 65 - .../components/steps/ModeIntroStep.tsx | 88 -- .../components/steps/ModelConfigStep.tsx | 875 ----------- .../onboarding/components/steps/ThemeStep.tsx | 303 ---- .../components/steps/WelcomeStep.tsx | 71 - .../onboarding/components/steps/index.ts | 8 - .../onboarding/hooks/useOnboarding.ts | 197 --- src/web-ui/src/features/onboarding/index.ts | 21 - .../onboarding/services/OnboardingService.ts | 248 --- .../onboarding/store/onboardingStore.ts | 243 --- .../infrastructure/i18n/core/I18nService.ts | 5 - src/web-ui/src/locales/en-US/onboarding.json | 204 --- src/web-ui/src/locales/zh-CN/onboarding.json | 204 --- 23 files changed, 54 insertions(+), 4405 deletions(-) delete mode 100644 src/web-ui/src/features/index.ts delete mode 100644 src/web-ui/src/features/onboarding/components/OnboardingWizard.scss delete mode 100644 src/web-ui/src/features/onboarding/components/OnboardingWizard.tsx delete mode 100644 src/web-ui/src/features/onboarding/components/index.ts delete mode 100644 src/web-ui/src/features/onboarding/components/steps/CompletionStep.tsx delete mode 100644 src/web-ui/src/features/onboarding/components/steps/LanguageStep.tsx delete mode 100644 src/web-ui/src/features/onboarding/components/steps/ModeIntroStep.tsx delete mode 100644 src/web-ui/src/features/onboarding/components/steps/ModelConfigStep.tsx delete mode 100644 src/web-ui/src/features/onboarding/components/steps/ThemeStep.tsx delete mode 100644 src/web-ui/src/features/onboarding/components/steps/WelcomeStep.tsx delete mode 100644 src/web-ui/src/features/onboarding/components/steps/index.ts delete mode 100644 src/web-ui/src/features/onboarding/hooks/useOnboarding.ts delete mode 100644 src/web-ui/src/features/onboarding/index.ts delete mode 100644 src/web-ui/src/features/onboarding/services/OnboardingService.ts delete mode 100644 src/web-ui/src/features/onboarding/store/onboardingStore.ts delete mode 100644 src/web-ui/src/locales/en-US/onboarding.json delete mode 100644 src/web-ui/src/locales/zh-CN/onboarding.json diff --git a/BitFun-Installer/scripts/sync-model-i18n.cjs b/BitFun-Installer/scripts/sync-model-i18n.cjs index b25583f9..720f5435 100644 --- a/BitFun-Installer/scripts/sync-model-i18n.cjs +++ b/BitFun-Installer/scripts/sync-model-i18n.cjs @@ -54,39 +54,45 @@ function buildProviderPatch(settingsAiModel) { return providerPatch; } -function buildModelPatch(onboarding, settingsAiModel, languageTag) { +function buildModelPatch(settingsAiModel, languageTag) { const isZh = languageTag === 'zh'; return { description: get( - onboarding, - 'model.description', + settingsAiModel, + 'subtitle', 'Configure AI model provider, API key, and advanced parameters.' ), - providerLabel: get(onboarding, 'model.provider.label', 'Model Provider'), - selectProvider: get(onboarding, 'model.provider.placeholder', 'Select a provider...'), - customProvider: get(onboarding, 'model.provider.options.custom', 'Custom'), - getApiKey: get(onboarding, 'model.apiKey.help', 'How to get an API Key?'), + providerLabel: get(settingsAiModel, 'providerSelection.title', 'Model Provider'), + selectProvider: get(settingsAiModel, 'providerSelection.orSelectProvider', 'Select a provider...'), + customProvider: get(settingsAiModel, 'providerSelection.customTitle', 'Custom'), + getApiKey: get(settingsAiModel, 'providerSelection.getApiKey', 'How to get an API Key?'), modelNamePlaceholder: get( - onboarding, - 'model.modelName.inputPlaceholder', - get(onboarding, 'model.modelName.placeholder', 'Enter model name...') + settingsAiModel, + 'providerSelection.inputModelName', + get(settingsAiModel, 'form.modelName', 'Enter model name...') ), - modelNameSelectPlaceholder: get(onboarding, 'model.modelName.selectPlaceholder', 'Select a model...'), + modelNameSelectPlaceholder: get(settingsAiModel, 'providerSelection.selectModel', 'Select a model...'), modelSearchPlaceholder: get( - onboarding, - 'model.modelName.searchPlaceholder', + settingsAiModel, + 'providerSelection.searchOrInputModel', 'Search or enter a custom model name...' ), modelNoResults: isZh ? '没有匹配的模型' : 'No matching models', - customModel: get(onboarding, 'model.modelName.customHint', 'Use custom model name'), - baseUrlPlaceholder: get(onboarding, 'model.baseUrl.placeholder', 'Enter API URL'), + customModel: get(settingsAiModel, 'providerSelection.useCustomModel', 'Use custom model name'), + baseUrlPlaceholder: isZh + ? '示例:https://open.bigmodel.cn/api/paas/v4/chat/completions' + : 'e.g., https://open.bigmodel.cn/api/paas/v4/chat/completions', customRequestBodyPlaceholder: get( - onboarding, - 'model.advanced.customRequestBodyPlaceholder', + settingsAiModel, + 'advancedSettings.customRequestBody.placeholder', '{\n "temperature": 0.8,\n "top_p": 0.9\n}' ), - jsonValid: get(onboarding, 'model.advanced.jsonValid', 'Valid JSON format'), - jsonInvalid: get(onboarding, 'model.advanced.jsonInvalid', 'Invalid JSON format'), + jsonValid: get(settingsAiModel, 'advancedSettings.customRequestBody.validJson', 'Valid JSON format'), + jsonInvalid: get( + settingsAiModel, + 'advancedSettings.customRequestBody.invalidJson', + 'Invalid JSON format' + ), skipSslVerify: get( settingsAiModel, 'advancedSettings.skipSslVerify.label', @@ -105,10 +111,10 @@ function buildModelPatch(onboarding, settingsAiModel, languageTag) { addHeader: get(settingsAiModel, 'advancedSettings.customHeaders.addHeader', 'Add Field'), headerKey: get(settingsAiModel, 'advancedSettings.customHeaders.keyPlaceholder', 'key'), headerValue: get(settingsAiModel, 'advancedSettings.customHeaders.valuePlaceholder', 'value'), - testConnection: get(onboarding, 'model.testConnection', 'Test Connection'), - testing: get(onboarding, 'model.testing', 'Testing...'), - testSuccess: get(onboarding, 'model.testSuccess', 'Connection successful'), - testFailed: get(onboarding, 'model.testFailed', 'Connection failed'), + testConnection: get(settingsAiModel, 'actions.test', 'Test Connection'), + testing: isZh ? '测试中...' : 'Testing...', + testSuccess: get(settingsAiModel, 'messages.testSuccess', 'Connection successful'), + testFailed: get(settingsAiModel, 'messages.testFailed', 'Connection failed'), advancedShow: 'Show advanced settings', advancedHide: 'Hide advanced settings', providers: buildProviderPatch(settingsAiModel), @@ -119,16 +125,6 @@ function syncOne(languageTag) { const localeDir = languageTag === 'zh' ? 'zh-CN' : 'en-US'; const installerLocale = languageTag === 'zh' ? 'zh.json' : 'en.json'; - const sourceOnboardingPath = path.join( - PROJECT_ROOT, - 'src', - 'web-ui', - 'src', - 'locales', - localeDir, - 'onboarding.json' - ); - const sourceAiModelPath = path.join( PROJECT_ROOT, 'src', @@ -142,11 +138,10 @@ function syncOne(languageTag) { const targetPath = path.join(INSTALLER_ROOT, 'src', 'i18n', 'locales', installerLocale); - const onboarding = readJson(sourceOnboardingPath); const settingsAiModel = readJson(sourceAiModelPath); const target = readJson(targetPath); - const patch = buildModelPatch(onboarding, settingsAiModel, languageTag); + const patch = buildModelPatch(settingsAiModel, languageTag); target.model = mergeDeep(target.model || {}, patch); writeJson(targetPath, target); diff --git a/BitFun-Installer/src/i18n/locales/en.json b/BitFun-Installer/src/i18n/locales/en.json index 4d1427e7..54f37ab3 100644 --- a/BitFun-Installer/src/i18n/locales/en.json +++ b/BitFun-Installer/src/i18n/locales/en.json @@ -40,15 +40,15 @@ "back": "Back", "skip": "Skip for now", "nextTheme": "Next: Theme", - "description": "Configuring an AI model is required to use BitFun. Select a provider and enter your API information", - "providerLabel": "Model Provider", - "selectProvider": "Select a model provider...", - "customProvider": "Custom", - "getApiKey": "How to get an API Key?", + "description": "Configure and manage AI model providers", + "providerLabel": "Select Model Provider", + "selectProvider": "or select a preset provider", + "customProvider": "Custom Configuration", + "getApiKey": "Get API Key", "modelNamePlaceholder": "Enter model name...", "baseUrlPlaceholder": "e.g., https://open.bigmodel.cn/api/paas/v4/chat/completions", "customRequestBodyPlaceholder": "{\n \"temperature\": 0.8,\n \"top_p\": 0.9\n}", - "jsonValid": "Valid JSON format", + "jsonValid": "JSON format is valid", "jsonInvalid": "Invalid JSON format, please check syntax", "skipSslVerify": "Skip SSL Certificate Verification", "customHeadersModeMerge": "Merge Override", @@ -126,13 +126,13 @@ "description": "OpenRouter Model Platform" } }, - "modelNameSelectPlaceholder": "Select a model...", - "customModel": "Use custom model name", + "modelNameSelectPlaceholder": "Select model", + "customModel": "Press Enter to use", "testConnection": "Test Connection", "testing": "Testing...", - "testSuccess": "Connection successful", - "testFailed": "Connection failed", - "modelSearchPlaceholder": "Search or enter a custom model name...", + "testSuccess": "Test successful", + "testFailed": "Test failed", + "modelSearchPlaceholder": "Search or enter model name...", "modelNoResults": "No matching models" }, "progress": { diff --git a/BitFun-Installer/src/i18n/locales/zh.json b/BitFun-Installer/src/i18n/locales/zh.json index 6b54c7ca..38a20abf 100644 --- a/BitFun-Installer/src/i18n/locales/zh.json +++ b/BitFun-Installer/src/i18n/locales/zh.json @@ -40,16 +40,16 @@ "back": "返回", "skip": "稍后配置", "nextTheme": "下一步:主题", - "description": "配置 AI 模型是使用 BitFun 的前提,请选择模型服务商并填写 API 信息", - "providerLabel": "模型服务商", - "selectProvider": "选择模型服务商...", - "customProvider": "自定义", - "getApiKey": "如何获取 API Key?", + "description": "配置和管理 AI 模型提供商", + "providerLabel": "选择模型提供商", + "selectProvider": "或选择预设提供商", + "customProvider": "自定义配置", + "getApiKey": "获取 API Key", "modelNamePlaceholder": "输入自定义模型名称...", "baseUrlPlaceholder": "示例:https://open.bigmodel.cn/api/paas/v4/chat/completions", "customRequestBodyPlaceholder": "{\n \"temperature\": 0.8,\n \"top_p\": 0.9\n}", - "jsonValid": "JSON 格式有效", - "jsonInvalid": "JSON 格式错误,请检查语法", + "jsonValid": "JSON格式有效", + "jsonInvalid": "JSON格式错误,请检查语法", "skipSslVerify": "跳过SSL证书验证", "customHeadersModeMerge": "合并覆盖", "customHeadersModeReplace": "完全替换", @@ -126,13 +126,13 @@ "description": "OpenRouter 大模型平台" } }, - "modelNameSelectPlaceholder": "选择模型...", - "customModel": "使用自定义模型名称", + "modelNameSelectPlaceholder": "选择模型", + "customModel": "按 Enter 使用", "testConnection": "测试连接", "testing": "测试中...", - "testSuccess": "连接成功", - "testFailed": "连接失败", - "modelSearchPlaceholder": "搜索或输入自定义模型名称...", + "testSuccess": "测试成功", + "testFailed": "测试失败", + "modelSearchPlaceholder": "搜索或输入模型名称...", "modelNoResults": "没有匹配的模型" }, "progress": { diff --git a/src/web-ui/src/app/App.tsx b/src/web-ui/src/app/App.tsx index 1b5d11d7..44c98e2d 100644 --- a/src/web-ui/src/app/App.tsx +++ b/src/web-ui/src/app/App.tsx @@ -14,10 +14,6 @@ import SplashScreen from './components/SplashScreen/SplashScreen'; // Toolbar Mode import { ToolbarModeProvider } from '../flow_chat'; -// Onboarding -import { OnboardingWizard, useOnboardingStore, onboardingService } from '../features/onboarding'; - - const log = createLogger('App'); /** @@ -32,7 +28,6 @@ const log = createLogger('App'); */ // Minimum time (ms) the splash is shown, so the animation is never a flash. const MIN_SPLASH_MS = 900; -const ENABLE_MAIN_ONBOARDING = false; function App() { // AI initialization @@ -62,45 +57,6 @@ function App() { setSplashVisible(false); }, []); - // Onboarding state - const { isOnboardingActive, forceShowOnboarding, completeOnboarding } = useOnboardingStore(); - - // Handle onboarding completion - const handleOnboardingComplete = useCallback(() => { - completeOnboarding(); - }, [completeOnboarding]); - - // Initialize onboarding: check first launch on startup - useEffect(() => { - if (!ENABLE_MAIN_ONBOARDING) { - onboardingService.markCompleted().catch((error) => { - log.warn('Failed to persist onboarding completion while disabled', error); - }); - return; - } - - onboardingService.initialize().catch((error) => { - log.error('Failed to initialize onboarding service', error); - }); - }, []); - - // In development, trigger onboarding via window.showOnboarding() - useEffect(() => { - if (!ENABLE_MAIN_ONBOARDING) { - delete (window as any).showOnboarding; - return; - } - - (window as any).showOnboarding = () => { - forceShowOnboarding(); - log.debug('Onboarding activated via debug command'); - }; - - return () => { - delete (window as any).showOnboarding; - }; - }, [forceShowOnboarding]); - const showMainWindow = useCallback(async (reason: string) => { if (mainWindowShownRef.current) { return; @@ -220,13 +176,6 @@ function App() { - {/* Onboarding overlay (first launch) */} - {ENABLE_MAIN_ONBOARDING && isOnboardingActive && ( - - )} - {/* Unified app layout with startup/workspace modes */} diff --git a/src/web-ui/src/app/scenes/agents/AgentsView.scss b/src/web-ui/src/app/scenes/agents/AgentsView.scss index 4c5c41b7..dc8f066f 100644 --- a/src/web-ui/src/app/scenes/agents/AgentsView.scss +++ b/src/web-ui/src/app/scenes/agents/AgentsView.scss @@ -93,7 +93,7 @@ } } -// ─── Onboarding ─────────────────────────────────────────────────────────────── +// ─── Agents empty state ─────────────────────────────────────────────────────── .tv-onboard { display: flex; flex-direction: column; diff --git a/src/web-ui/src/features/index.ts b/src/web-ui/src/features/index.ts deleted file mode 100644 index e746d30d..00000000 --- a/src/web-ui/src/features/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Feature module exports - * Aggregates feature entry points - */ - -export * from './onboarding'; diff --git a/src/web-ui/src/features/onboarding/components/OnboardingWizard.scss b/src/web-ui/src/features/onboarding/components/OnboardingWizard.scss deleted file mode 100644 index bd531b31..00000000 --- a/src/web-ui/src/features/onboarding/components/OnboardingWizard.scss +++ /dev/null @@ -1,1356 +0,0 @@ -/** - * First-run onboarding wizard styles - * BEM naming + design tokens - * Dashed/solid style inspired by the Git panel - */ - -@use '../../../component-library/styles/tokens' as *; - -// ==================== Root container ==================== -.bitfun-onboarding { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: $z-fullscreen; - display: flex; - flex-direction: column; - background: var(--color-bg-primary); - color: var(--color-text-primary); - font-family: $font-family-sans; - overflow: hidden; - - // ==================== Background decoration ==================== - &__background { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - pointer-events: none; - overflow: hidden; - z-index: 0; - } - - // ==================== Window controls ==================== - &__window-controls { - position: absolute; - top: $size-gap-4; - right: $size-gap-4; - z-index: 10; - pointer-events: auto; - } - - - // ==================== Main content ==================== - &__content { - position: relative; - z-index: 1; - flex: 1; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: $size-gap-8; - overflow-y: auto; - min-height: 0; - } - - // ==================== Step container ==================== - &__step-container { - width: 100%; - max-width: 720px; - animation: onboarding-fade-in 0.4s ease-out; - } - - // ==================== Progress indicator ==================== - &__progress { - position: relative; - z-index: 1; - display: flex; - align-items: center; - justify-content: center; - gap: $size-gap-2; - padding: $size-gap-4 $size-gap-6; - background: transparent; - } - - &__progress-step { - display: flex; - align-items: center; - gap: $size-gap-2; - padding: $size-gap-1 $size-gap-2; - border-radius: $size-radius-base; - - &--active { - background: var(--element-bg-subtle); - box-shadow: 0 0 0 1px var(--border-medium); - } - - &--clickable { - cursor: pointer; - transition: background $motion-base $easing-standard; - - &:hover { - background: var(--element-bg-subtle); - } - - &:focus { - outline: none; - background: var(--element-bg-subtle); - } - } - } - - &__progress-dot { - width: 10px; - height: 10px; - border-radius: 50%; - border: 1px dashed var(--border-medium); - background: transparent; - transition: all $motion-base $easing-standard; - - &--active { - border-style: solid; - border-color: $color-accent-500; - background: $color-accent-500; - box-shadow: 0 0 8px $color-accent-400; - } - - &--completed { - border-style: solid; - border-color: $color-success; - background: $color-success; - } - } - - &__progress-line { - width: 40px; - height: 1px; - background: repeating-linear-gradient( - 90deg, - var(--border-medium) 0, - var(--border-medium) 4px, - transparent 4px, - transparent 8px - ); - - &--completed { - background: $color-success; - } - } - - &__progress-label { - font-size: $font-size-xs; - color: var(--color-text-muted); - white-space: nowrap; - - &--active { - color: $color-accent-500; - font-weight: $font-weight-medium; - } - - &--completed { - color: $color-success; - } - } - - // ==================== Navigation buttons ==================== - &__navigation { - position: relative; - z-index: 1; - display: flex; - align-items: center; - justify-content: space-between; - gap: $size-gap-4; - padding: $size-gap-2 $size-gap-3; - border-top: 1px dashed var(--border-medium); - } - - &__nav-info { - font-size: $font-size-sm; - color: var(--color-text-muted); - } - - &__nav-buttons { - display: flex; - gap: $size-gap-3; - } - - &__nav-btn { - display: flex; - align-items: center; - justify-content: center; - gap: $size-gap-2; - min-height: 32px; - padding: 6px 16px; - min-width: 80px; - border: 1px dashed var(--border-medium); - border-radius: 4px; - background: transparent; - color: var(--color-text-secondary); - font-size: $font-size-sm; - font-weight: $font-weight-medium; - font-family: $font-family-sans; - cursor: pointer; - box-sizing: border-box; - transition: all $motion-base $easing-standard; - - &:hover:not(:disabled) { - border-style: solid; - border-color: var(--color-text-secondary); - background: var(--element-bg-subtle); - color: var(--color-text-primary); - } - - &:active:not(:disabled) { - transform: scale(0.98); - } - - &:disabled { - opacity: 0.4; - cursor: default; - } - - &--primary { - border-color: $color-accent-500; - border-style: solid; - background: $color-accent-100; - color: $color-accent-500; - - &:hover:not(:disabled) { - background: $color-accent-200; - border-color: $color-accent-600; - color: $color-accent-600; - } - } - - &--skip { - border: none; - background: transparent; - color: var(--color-text-muted); - min-width: auto; - - &:hover:not(:disabled) { - color: var(--color-text-secondary); - background: transparent; - } - } - } -} - -// ==================== Common step styles ==================== -.bitfun-onboarding-step { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - - &__header { - margin-bottom: $size-gap-6; - } - - &__icon { - display: flex; - align-items: center; - justify-content: center; - width: 64px; - height: 64px; - margin: 0 auto $size-gap-4; - border-radius: 50%; - background: var(--element-bg-subtle); - color: $color-accent-500; - - svg { - width: 32px; - height: 32px; - } - } - - &__title { - font-size: $font-size-3xl; - font-weight: $font-weight-semibold; - color: var(--color-text-primary); - margin: 0 0 $size-gap-2 0; - line-height: 1.2; - } - - &__subtitle { - font-size: $font-size-lg; - color: var(--color-text-secondary); - margin: 0 0 $size-gap-2 0; - line-height: 1.4; - } - - &__description { - font-size: $font-size-sm; - color: var(--color-text-muted); - margin: 0; - max-width: 480px; - line-height: 1.6; - } - - &__content { - width: 100%; - margin-top: $size-gap-6; - } -} - -// ==================== Welcome step styles ==================== -.bitfun-onboarding-welcome { - &__logo { - width: 140px; - height: 140px; - margin-bottom: $size-gap-10; - margin-top: -$size-gap-8; - background: none; - border: none; - animation: onboarding-float 3s ease-in-out infinite; - } - - // Override title size for welcome step - product name should be smaller - .bitfun-onboarding-step__title { - font-size: $font-size-xl; - } - - &__pillars { - display: flex; - gap: $size-gap-4; - margin-top: $size-gap-8; - width: 100%; - max-width: 640px; - } - - &__pillar { - flex: 1; - display: flex; - flex-direction: column; - align-items: center; - gap: $size-gap-2; - padding: $size-gap-4; - border: 1px dashed var(--border-medium); - border-radius: $size-radius-lg; - background: transparent; - transition: all $motion-base $easing-standard; - - &:hover { - border-style: solid; - background: var(--element-bg-subtle); - transform: translateY(-2px); - } - } - - &__pillar-icon { - display: flex; - align-items: center; - justify-content: center; - width: 48px; - height: 48px; - border-radius: 50%; - background: var(--element-bg-base); - color: $color-accent-500; - - svg { - width: 24px; - height: 24px; - } - } - - &__pillar-title { - font-size: $font-size-sm; - font-weight: $font-weight-semibold; - color: var(--color-text-primary); - } - - &__pillar-desc { - font-size: $font-size-xs; - color: var(--color-text-muted); - } - - &__actions { - display: flex; - flex-direction: column; - align-items: center; - gap: $size-gap-3; - margin-top: $size-gap-8; - } - - &__start-btn { - display: flex; - align-items: center; - justify-content: center; - gap: $size-gap-2; - padding: $size-gap-3 $size-gap-6; - border: 1px dashed var(--border-medium); - border-radius: 4px; - background: transparent; - color: var(--color-text-secondary); - font-size: $font-size-base; - font-weight: $font-weight-medium; - font-family: $font-family-sans; - cursor: pointer; - transition: all $motion-base $easing-standard; - - &:hover { - border-style: solid; - border-color: var(--color-text-primary); - background: var(--element-bg-subtle); - color: var(--color-text-primary); - transform: translateY(-2px); - } - - &:active { - transform: translateY(0); - } - - svg { - transition: transform $motion-base $easing-standard; - } - - &:hover svg { - transform: translateX(4px); - } - } - - &__skip-btn { - background: transparent; - border: none; - color: var(--color-text-muted); - font-size: $font-size-sm; - cursor: pointer; - transition: color $motion-base $easing-standard; - - &:hover { - color: var(--color-text-secondary); - } - } -} - -// ==================== Language selection styles ==================== -.bitfun-onboarding-language { - &__logo { - width: 140px; - height: 140px; - margin: 0 auto $size-gap-4; - - img { - width: 100%; - height: 100%; - object-fit: contain; - } - } - - &__welcome { - font-size: 28px; - font-weight: $font-weight-medium; - color: var(--color-text-secondary); - margin: 0 0 $size-gap-2 0; - text-align: center; - line-height: 1.2; - } - - &__subtitle { - display: flex; - flex-direction: column; - align-items: center; - font-size: $font-size-lg; - font-weight: $font-weight-medium; - color: var(--color-text-muted); - margin: $size-gap-4 0 $size-gap-6 0; - text-align: center; - line-height: 1.6; - } - - &__options { - display: flex; - gap: $size-gap-4; - justify-content: center; - margin-top: $size-gap-6; - } - - &__option { - display: flex; - flex-direction: column; - align-items: center; - gap: $size-gap-3; - padding: $size-gap-6 $size-gap-8; - min-width: 180px; - border: 1px dashed var(--border-medium); - border-radius: $size-radius-lg; - background: transparent; - cursor: pointer; - transition: all $motion-base $easing-standard; - - &:hover { - border-style: solid; - border-color: var(--color-text-secondary); - background: var(--element-bg-subtle); - transform: translateY(-2px); - } - - &--selected { - border-style: solid; - border-color: $color-accent-500; - background: $color-accent-50; - - .bitfun-onboarding-language__option-icon { - background: $color-accent-500; - color: white; - } - } - } - - &__option-icon { - display: flex; - align-items: center; - justify-content: center; - width: 56px; - height: 56px; - border-radius: 50%; - background: var(--element-bg-base); - font-size: $font-size-2xl; - transition: all $motion-base $easing-standard; - } - - &__option-label { - font-size: $font-size-lg; - font-weight: $font-weight-medium; - color: var(--color-text-primary); - } -} - -// ==================== Theme selection styles ==================== -.bitfun-onboarding-theme { - &__grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: $size-gap-3; - margin-top: $size-gap-6; - width: 100%; - max-width: 800px; - } - - &__card { - display: flex; - flex-direction: column; - padding: $size-gap-2; - border: 1px dashed var(--border-medium); - border-radius: $size-radius-lg; - background: transparent; - cursor: pointer; - transition: all $motion-base $easing-standard; - - &:hover { - border-style: solid; - border-color: var(--color-text-secondary); - background: var(--element-bg-subtle); - transform: translateY(-2px); - } - - &--selected { - border-style: solid; - border-color: $color-accent-500; - background: $color-accent-50; - } - } - - &__info { - padding: $size-gap-2 $size-gap-1 0; - } - - // Theme preview thumbnail - realistic IDE mock - &__preview-thumbnail { - height: 100px; - border-radius: $size-radius-base; - border: 1px solid; - overflow: hidden; - display: flex; - flex-direction: column; - } - - &__preview-titlebar { - height: 14px; - display: flex; - align-items: center; - gap: 4px; - padding: 0 4px; - border-bottom: 1px solid; - flex-shrink: 0; - } - - &__preview-dot { - width: 5px; - height: 5px; - border-radius: 50%; - opacity: 0.8; - } - - &__preview-title { - font-size: 6px; - margin-left: auto; - opacity: 0.6; - } - - &__preview-main { - flex: 1; - display: flex; - overflow: hidden; - } - - &__preview-sidebar { - width: 20%; - padding: 3px; - border-right: 1px solid; - display: flex; - flex-direction: column; - gap: 2px; - } - - &__preview-tree-item { - display: flex; - align-items: center; - gap: 2px; - } - - &__preview-folder, - &__preview-file { - width: 4px; - height: 4px; - border-radius: 1px; - flex-shrink: 0; - } - - &__preview-text { - height: 3px; - width: 70%; - border-radius: 1px; - - &--short { - width: 50%; - } - } - - &__preview-chat { - flex: 1; - padding: 3px; - display: flex; - flex-direction: column; - gap: 3px; - } - - &__preview-message { - padding: 3px; - border-radius: 2px; - border: 1px solid; - - &--user { - margin-left: 15%; - } - - &--ai { - margin-right: 10%; - } - } - - &__preview-line { - height: 2px; - width: 80%; - border-radius: 1px; - margin-bottom: 2px; - - &:last-child { - margin-bottom: 0; - } - - &--short { - width: 50%; - } - } - - &__preview-editor { - width: 25%; - display: flex; - flex-direction: column; - border-left: 1px solid; - } - - &__preview-tabs { - height: 10px; - display: flex; - gap: 2px; - padding: 2px; - border-bottom: 1px solid; - } - - &__preview-tab { - width: 18px; - height: 6px; - border-radius: 2px 2px 0 0; - - &--active { - border-bottom: 1px solid; - } - } - - &__preview-code { - flex: 1; - padding: 3px; - display: flex; - flex-direction: column; - gap: 2px; - } - - &__preview-code-line { - display: flex; - align-items: center; - gap: 3px; - } - - &__preview-line-num { - width: 6px; - height: 2px; - border-radius: 1px; - flex-shrink: 0; - } - - &__preview-line-code { - height: 2px; - border-radius: 1px; - } - - &__preview-statusbar { - height: 10px; - display: flex; - align-items: center; - gap: 3px; - padding: 0 4px; - border-top: 1px solid; - flex-shrink: 0; - } - - &__preview-status-dot { - width: 4px; - height: 4px; - border-radius: 50%; - } - - &__preview-status-text { - width: 20px; - height: 2px; - border-radius: 1px; - } - - // Fallback preview styles - &__preview--fallback { - height: 100px; - background: var(--element-bg-subtle); - border-radius: $size-radius-base; - } - - &__name { - font-size: $font-size-sm; - font-weight: $font-weight-medium; - color: var(--color-text-primary); - margin-bottom: $size-gap-1; - } - - &__desc { - font-size: $font-size-xs; - color: var(--color-text-muted); - line-height: 1.4; - } -} - -// ==================== Mode intro styles (minimal, premium) ==================== -.bitfun-onboarding-modes { - &__grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: $size-gap-4; - margin-top: $size-gap-6; - width: 100%; - max-width: 640px; - } - - &__item { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - padding: $size-gap-5 $size-gap-4; - border: 1px dashed var(--border-medium); - border-radius: $size-radius-lg; - background: transparent; - transition: all $motion-base $easing-standard; - - &:hover { - border-style: solid; - background: var(--element-bg-subtle); - transform: translateY(-2px); - } - } - - &__item-icon { - display: flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - margin-bottom: $size-gap-3; - border-radius: 50%; - background: var(--element-bg-base); - color: $color-accent-500; - transition: transform $motion-base $easing-standard; - - .bitfun-onboarding-modes__item:hover & { - transform: scale(1.1); - } - } - - &__item-name { - font-size: $font-size-sm; - font-weight: $font-weight-semibold; - color: var(--color-text-primary); - margin-bottom: $size-gap-2; - } - - &__item-desc { - font-size: $font-size-xs; - color: var(--color-text-muted); - line-height: 1.5; - margin-bottom: $size-gap-3; - } - - &__item-features { - display: flex; - flex-direction: column; - gap: $size-gap-1; - } - - &__item-feature { - font-size: 11px; - color: var(--color-text-secondary); - opacity: 0.8; - } - - &__tip { - margin-top: $size-gap-6; - font-size: $font-size-sm; - color: var(--color-text-muted); - text-align: center; - } -} - -// ==================== Model configuration styles ==================== -.bitfun-onboarding-model { - &__form { - display: flex; - flex-direction: column; - gap: $size-gap-4; - margin-top: $size-gap-6; - width: 100%; - max-width: 480px; - text-align: left; - } - - &__field { - display: flex; - flex-direction: column; - gap: $size-gap-2; - - // Use component library default Select styles - .select { - width: 100%; - } - } - - &__label { - font-size: $font-size-sm; - font-weight: $font-weight-medium; - color: var(--color-text-primary); - } - - // Inputs - match component library Select sizing - &__input { - width: 100%; - min-height: 36px; - padding: 6px 12px; - border: 1px solid var(--border-base, rgba(255, 255, 255, 0.14)); - border-radius: var(--size-radius-sm, 6px); - background: transparent; - color: var(--color-text-primary); - font-size: var(--font-size-sm, 14px); - font-family: $font-family-sans; - box-sizing: border-box; - transition: all $motion-base $easing-standard; - - &::placeholder { - color: var(--color-text-muted); - } - - &:hover:not(:disabled) { - background: var(--element-bg-subtle, rgba(255, 255, 255, 0.05)); - border-color: var(--border-strong, rgba(255, 255, 255, 0.26)); - } - - &:focus { - outline: none; - border-color: var(--color-accent-500, #60a5fa); - } - } - - &__help { - display: inline-flex; - align-items: center; - gap: 4px; - font-size: $font-size-xs; - color: $color-accent-500; - background: transparent; - border: none; - padding: 0; - cursor: pointer; - font-family: inherit; - - &:hover { - text-decoration: underline; - } - - svg { - flex-shrink: 0; - } - } - - &__hint { - font-size: $font-size-xs; - color: var(--color-text-muted); - line-height: 1.4; - - &--success { - color: $color-success; - } - - &--error { - color: $color-error; - } - } - - &__save-hint { - display: flex; - align-items: center; - justify-content: center; - gap: 6px; - padding: 8px 12px; - border-radius: 6px; - font-size: $font-size-sm; - line-height: 1.4; - color: var(--color-warning, #e8a838); - background: rgba(232, 168, 56, 0.08); - border: 1px solid rgba(232, 168, 56, 0.2); - - svg { - flex-shrink: 0; - } - } - - &__advanced { - margin-top: $size-gap-2; - border-top: 1px solid var(--border-base, rgba(255, 255, 255, 0.06)); - padding-top: $size-gap-2; - } - - &__advanced-toggle { - display: flex; - align-items: center; - gap: $size-gap-2; - width: 100%; - padding: $size-gap-2 0; - background: transparent; - border: none; - color: var(--color-text-secondary); - font-size: $font-size-sm; - font-weight: $font-weight-medium; - font-family: $font-family-sans; - cursor: pointer; - transition: color $motion-base $easing-standard; - - &:hover { - color: var(--color-text-primary); - } - - svg { - flex-shrink: 0; - } - } - - &__advanced-content { - display: flex; - flex-direction: column; - gap: $size-gap-4; - padding-top: $size-gap-2; - animation: onboarding-fade-in 0.2s ease-out; - } - - &__switch-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: $size-gap-4; - - > div:first-child { - flex: 1; - display: flex; - flex-direction: column; - gap: 2px; - } - } - - &__warning { - display: flex; - align-items: center; - gap: $size-gap-2; - padding: $size-gap-2 $size-gap-3; - border-radius: $size-radius-sm; - background: rgba(234, 179, 8, 0.08); - color: var(--color-warning, #eab308); - font-size: $font-size-xs; - line-height: 1.4; - - svg { - flex-shrink: 0; - } - } - - &__textarea { - width: 100%; - min-height: 80px; - padding: 8px 12px; - border: 1px solid var(--border-base, rgba(255, 255, 255, 0.14)); - border-radius: var(--size-radius-sm, 6px); - background: transparent; - color: var(--color-text-primary); - font-size: var(--font-size-sm, 14px); - font-family: monospace; - line-height: 1.5; - box-sizing: border-box; - resize: vertical; - transition: all $motion-base $easing-standard; - - &::placeholder { - color: var(--color-text-muted); - } - - &:hover:not(:disabled) { - background: var(--element-bg-subtle, rgba(255, 255, 255, 0.05)); - border-color: var(--border-strong, rgba(255, 255, 255, 0.26)); - } - - &:focus { - outline: none; - border-color: var(--color-accent-500, #60a5fa); - } - } - - &__error { - padding: $size-gap-2 $size-gap-3; - border-radius: $size-radius-base; - background: var(--color-error-bg); - border: 1px solid var(--color-error-border); - color: var(--color-error); - font-size: $font-size-sm; - text-align: left; - white-space: pre-line; - word-break: break-word; - } - - &__actions { - display: flex; - gap: $size-gap-3; - margin-top: $size-gap-2; - } - - &__test-btn { - flex: 1; - display: flex; - align-items: center; - justify-content: center; - gap: $size-gap-2; - min-height: 36px; - padding: 6px 12px; - border: 1px solid var(--border-base, rgba(255, 255, 255, 0.14)); - border-radius: var(--size-radius-sm, 6px); - background: transparent; - color: var(--color-text-secondary); - font-size: var(--font-size-sm, 14px); - font-weight: $font-weight-medium; - font-family: $font-family-sans; - cursor: pointer; - box-sizing: border-box; - transition: all $motion-base $easing-standard; - - &:hover:not(:disabled) { - background: var(--element-bg-subtle, rgba(255, 255, 255, 0.05)); - border-color: var(--border-strong, rgba(255, 255, 255, 0.26)); - color: var(--color-text-primary); - } - - &:disabled { - opacity: 0.5; - cursor: default; - } - - &--success { - border-style: solid; - border-color: $color-success; - color: $color-success; - } - - &--error { - border-style: solid; - border-color: $color-error; - color: $color-error; - } - } - - &__skip-link { - margin-top: $size-gap-4; - background: transparent; - border: none; - color: var(--color-text-muted); - font-size: $font-size-sm; - cursor: pointer; - text-align: center; - - &:hover { - color: var(--color-text-secondary); - text-decoration: underline; - } - } -} - -// ==================== Completion step styles ==================== -.bitfun-onboarding-completion { - &__success-icon { - display: flex; - align-items: center; - justify-content: center; - width: 80px; - height: 80px; - margin: 0 auto $size-gap-4; - border-radius: 50%; - background: $color-success-bg; - color: $color-success; - animation: onboarding-success-pop 0.5s ease-out; - - svg { - width: 40px; - height: 40px; - } - } - - &__model-status { - display: flex; - align-items: center; - gap: $size-gap-2; - margin-top: $size-gap-6; - padding: $size-gap-3 $size-gap-4; - border-radius: $size-radius-lg; - font-size: $font-size-sm; - width: 100%; - max-width: 400px; - text-align: center; - justify-content: center; - - &--configured { - border: 1px dashed var(--border-medium); - background: transparent; - color: var(--color-text-primary); - } - - &--not-configured { - border: 1px dashed $color-warning; - background: transparent; - color: $color-warning; - } - } - - &__model-status-label { - color: var(--color-text-muted); - } - - &__model-status-value { - font-weight: $font-weight-medium; - color: var(--color-text-primary); - } - - &__start-btn { - display: flex; - align-items: center; - justify-content: center; - gap: $size-gap-2; - margin-top: $size-gap-6; - padding: $size-gap-3 $size-gap-6; - border: 1px dashed var(--border-medium); - border-radius: $size-radius-base; - background: transparent; - color: var(--color-text-secondary); - font-size: $font-size-base; - font-weight: $font-weight-medium; - font-family: $font-family-sans; - cursor: pointer; - transition: all $motion-base $easing-standard; - - &:hover { - border-style: solid; - border-color: var(--color-text-primary); - background: var(--element-bg-subtle); - color: var(--color-text-primary); - transform: translateY(-2px); - } - - &:active { - transform: translateY(0); - } - - svg { - transition: transform $motion-base $easing-standard; - } - - &:hover svg { - transform: translateX(4px); - } - } -} - -// ==================== Inline confirm dialog ==================== -.bitfun-onboarding__confirm-overlay { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 100; - display: flex; - align-items: center; - justify-content: center; - background: rgba(0, 0, 0, 0.5); - animation: onboarding-fade-in 0.2s ease-out; -} - -.bitfun-onboarding__confirm-dialog { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - max-width: 420px; - padding: $size-gap-6 $size-gap-8; - border: 1px solid var(--border-medium); - border-radius: $size-radius-lg; - background: var(--color-bg-primary); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); -} - -.bitfun-onboarding__confirm-icon { - display: flex; - align-items: center; - justify-content: center; - width: 56px; - height: 56px; - margin-bottom: $size-gap-4; - border-radius: 50%; - background: rgba(234, 179, 8, 0.1); - color: var(--color-warning, #eab308); -} - -.bitfun-onboarding__confirm-title { - font-size: $font-size-lg; - font-weight: $font-weight-semibold; - color: var(--color-text-primary); - margin: 0 0 $size-gap-2 0; -} - -.bitfun-onboarding__confirm-message { - font-size: $font-size-sm; - color: var(--color-text-secondary); - margin: 0 0 $size-gap-6 0; - line-height: 1.6; -} - -.bitfun-onboarding__confirm-actions { - display: flex; - gap: $size-gap-3; - width: 100%; -} - -.bitfun-onboarding__confirm-btn { - flex: 1; - display: flex; - align-items: center; - justify-content: center; - min-height: 32px; - padding: 6px 16px; - border: 1px dashed var(--border-medium); - border-radius: 4px; - background: transparent; - color: var(--color-text-secondary); - font-size: $font-size-sm; - font-weight: $font-weight-medium; - font-family: $font-family-sans; - cursor: pointer; - transition: all $motion-base $easing-standard; - - &:hover { - border-style: solid; - border-color: var(--color-text-secondary); - background: var(--element-bg-subtle); - color: var(--color-text-primary); - } - - &--primary { - border-color: $color-accent-500; - border-style: solid; - background: $color-accent-100; - color: $color-accent-500; - - &:hover { - background: $color-accent-200; - border-color: $color-accent-600; - color: $color-accent-600; - } - } -} - -// ==================== Animations ==================== -@keyframes onboarding-fade-in { - from { - opacity: 0; - transform: translateY(20px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes onboarding-float { - 0%, 100% { - transform: translateY(0); - } - 50% { - transform: translateY(-10px); - } -} - -@keyframes onboarding-success-pop { - 0% { - opacity: 0; - transform: scale(0.5); - } - 50% { - transform: scale(1.1); - } - 100% { - opacity: 1; - transform: scale(1); - } -} diff --git a/src/web-ui/src/features/onboarding/components/OnboardingWizard.tsx b/src/web-ui/src/features/onboarding/components/OnboardingWizard.tsx deleted file mode 100644 index f22f3ed3..00000000 --- a/src/web-ui/src/features/onboarding/components/OnboardingWizard.tsx +++ /dev/null @@ -1,336 +0,0 @@ -/** - * First-run onboarding wizard - * OnboardingWizard - main container component - */ - -import React, { useCallback, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { ChevronLeft, ChevronRight, AlertTriangle } from 'lucide-react'; -import { useOnboarding } from '../hooks/useOnboarding'; -import { onboardingService } from '../services/OnboardingService'; -import { STEP_ORDER, useOnboardingStore, isModelConfigComplete, type OnboardingStep } from '../store/onboardingStore'; -import { - LanguageStep, - ThemeStep, - ModelConfigStep, - CompletionStep -} from './steps'; -import { WindowControls } from '@/component-library'; -import { useWindowControls } from '@/app/hooks/useWindowControls'; -import { createLogger } from '@/shared/utils/logger'; -import './OnboardingWizard.scss'; - -const log = createLogger('OnboardingWizard'); - -interface OnboardingWizardProps { - /** Callback after completion */ - onComplete?: () => void; -} - -/** - * Progress step labels - */ -const STEP_LABELS: Record = { - language: 'progress.step1', - theme: 'progress.step2', - model: 'progress.step3', - completion: 'progress.step4' -}; - -export const OnboardingWizard: React.FC = ({ - onComplete -}) => { - const { t } = useTranslation('onboarding'); - const { - currentStep, - currentStepIndex, - totalSteps, - selectedLanguage, - selectedTheme, - nextStep, - prevStep, - goToStep, - skipOnboarding, - completeOnboarding, - setLanguage, - setTheme, - canGoNext, - canGoPrev, - isStepCompleted - } = useOnboarding(false, false); // Do not auto-initialize; controlled externally - - // Model config state from store (for validation on next step) - const { modelConfig } = useOnboardingStore(); - - // Window controls - const { handleMinimize, handleMaximize, handleClose, isMaximized } = useWindowControls(); - const isMacOS = - typeof window !== 'undefined' && - '__TAURI__' in window && - typeof navigator !== 'undefined' && - typeof navigator.platform === 'string' && - navigator.platform.toUpperCase().includes('MAC'); - - // Handle completion - const handleComplete = useCallback(async () => { - try { - await completeOnboarding(); - onComplete?.(); - } catch (error) { - log.error('Failed to complete onboarding', { error }); - } - }, [completeOnboarding, onComplete]); - - const handleSkip = useCallback(async () => { - skipOnboarding(); - await onboardingService.markCompleted(); - onComplete?.(); - }, [skipOnboarding, onComplete]); - - // Inline confirmation state for incomplete model config - const [showIncompleteWarning, setShowIncompleteWarning] = useState(false); - - // Handle next step with model config validation - const handleNextStep = useCallback(() => { - // On the model step, check if provider is selected but required fields are incomplete - if (currentStep === 'model' && modelConfig?.provider && !isModelConfigComplete(modelConfig)) { - setShowIncompleteWarning(true); - return; - } - nextStep(); - }, [currentStep, modelConfig, nextStep]); - - // User confirms to continue without completing model config - // Keep partial config in store so user can go back and resume editing - // Incomplete config won't be saved on completion (guarded by isModelConfigComplete) - const handleConfirmIncomplete = useCallback(() => { - setShowIncompleteWarning(false); - nextStep(); - }, [nextStep]); - - // User cancels and stays to configure - const handleCancelIncomplete = useCallback(() => { - setShowIncompleteWarning(false); - }, []); - - // Handle step click - const handleStepClick = useCallback((step: OnboardingStep) => { - goToStep(step); - }, [goToStep]); - - // Render progress indicator - const renderProgress = () => { - return ( -
- {STEP_ORDER.map((step, index) => { - const isActive = step === currentStep; - const isCompleted = isStepCompleted(step); - const isClickable = isCompleted && !isActive; - const isLast = index === STEP_ORDER.length - 1; - - return ( - -
handleStepClick(step) : undefined} - role={isClickable ? 'button' : undefined} - tabIndex={isClickable ? 0 : undefined} - onKeyDown={isClickable ? (e) => { - if (e.key === 'Enter' || e.key === ' ') { - handleStepClick(step); - } - } : undefined} - > -
- - {t(STEP_LABELS[step])} - -
- {!isLast && ( -
- )} - - ); - })} -
- ); - }; - - // Render current step content - const renderStepContent = () => { - switch (currentStep) { - case 'language': - return ( - - ); - - case 'theme': - return ( - - ); - - case 'model': - return ( - - ); - - case 'completion': - return ( - - ); - - default: - return null; - } - }; - - // Render navigation - const renderNavigation = () => { - if (currentStep === 'completion') { - return ( -
-
-
- -
-
- ); - } - - return ( -
-
- {t('navigation.stepOf', { - current: currentStepIndex + 1, - total: totalSteps - })} -
-
- - - -
-
- ); - }; - - return ( -
- {/* Background decoration */} -
- - {/* Window controls (macOS uses native traffic lights) */} - {!isMacOS && ( -
- -
- )} - - {/* Progress indicator */} - {renderProgress()} - - {/* Main content */} -
-
- {renderStepContent()} -
-
- - {/* Navigation */} - {renderNavigation()} - - {/* Inline confirm dialog for incomplete model config */} - {showIncompleteWarning && ( -
-
-
- -
-

- {t('model.incompleteConfig.title')} -

-

- {t('model.incompleteConfig.message')} -

-
- - -
-
-
- )} -
- ); -}; - -export default OnboardingWizard; diff --git a/src/web-ui/src/features/onboarding/components/index.ts b/src/web-ui/src/features/onboarding/components/index.ts deleted file mode 100644 index 2d54c682..00000000 --- a/src/web-ui/src/features/onboarding/components/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Onboarding component exports - */ - -export { OnboardingWizard } from './OnboardingWizard'; -export * from './steps'; diff --git a/src/web-ui/src/features/onboarding/components/steps/CompletionStep.tsx b/src/web-ui/src/features/onboarding/components/steps/CompletionStep.tsx deleted file mode 100644 index 7893d5e0..00000000 --- a/src/web-ui/src/features/onboarding/components/steps/CompletionStep.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Completion step - * CompletionStep - shows model status and get started button - */ - -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { CheckCircle, ArrowRight } from 'lucide-react'; -import { useOnboardingStore, isModelConfigComplete } from '../../store/onboardingStore'; - -interface CompletionStepProps { - onComplete: () => void; -} - -export const CompletionStep: React.FC = ({ - onComplete -}) => { - const { t } = useTranslation('onboarding'); - const { modelConfig } = useOnboardingStore(); - - const hasModel = isModelConfigComplete(modelConfig); - - return ( -
- {/* Success icon */} -
- -
- - {/* Title */} -
-

- {t('completion.title')} -

-

- {t('completion.subtitle')} -

-
- - {/* Model status hint - only show warning when not configured */} - {!hasModel && ( -
- {t('completion.modelStatus.notConfigured')} -
- )} - - {/* Get started button */} - -
- ); -}; - -export default CompletionStep; diff --git a/src/web-ui/src/features/onboarding/components/steps/LanguageStep.tsx b/src/web-ui/src/features/onboarding/components/steps/LanguageStep.tsx deleted file mode 100644 index 85bc90fa..00000000 --- a/src/web-ui/src/features/onboarding/components/steps/LanguageStep.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Language selection step - * LanguageStep - choose UI language - */ - -import React from 'react'; -import { useTranslation } from 'react-i18next'; - -interface LanguageStepProps { - selectedLanguage: string; - onLanguageChange: (language: string) => void; -} - -const LANGUAGE_OPTIONS = [ - { id: 'zh-CN', shortLabelKey: 'language.optionsShort.zh-CN', labelKey: 'language.options.zh-CN' }, - { id: 'en-US', shortLabelKey: 'language.optionsShort.en-US', labelKey: 'language.options.en-US' } -]; - -export const LanguageStep: React.FC = ({ - selectedLanguage, - onLanguageChange -}) => { - const { t } = useTranslation('onboarding'); - - return ( -
- {/* Logo */} -
- BitFun Logo -
- - {/* Welcome text */} -

- BitFun -

- - {/* Subtitle - both languages on one line with / separator */} -
- 选择界面语言 / Choose Your Language -
- - {/* Language options */} -
- {LANGUAGE_OPTIONS.map((option) => ( -
onLanguageChange(option.id)} - > -
- {t(option.shortLabelKey)} -
-
- {t(option.labelKey)} -
-
- ))} -
-
- ); -}; - -export default LanguageStep; diff --git a/src/web-ui/src/features/onboarding/components/steps/ModeIntroStep.tsx b/src/web-ui/src/features/onboarding/components/steps/ModeIntroStep.tsx deleted file mode 100644 index 217eb042..00000000 --- a/src/web-ui/src/features/onboarding/components/steps/ModeIntroStep.tsx +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Mode intro step - * ModeIntroStep - minimal, premium mode overview - */ - -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { Zap, GitBranch, Search } from 'lucide-react'; - -interface ModeIntroStepProps {} - -const MODE_LIST = [ - { - id: 'agentic', - icon: Zap, - nameKey: 'modes.modeList.agentic.name', - descKey: 'modes.modeList.agentic.description', - featuresKey: 'modes.modeList.agentic.features' - }, - { - id: 'plan', - icon: GitBranch, - nameKey: 'modes.modeList.plan.name', - descKey: 'modes.modeList.plan.description', - featuresKey: 'modes.modeList.plan.features' - }, - { - id: 'debug', - icon: Search, - nameKey: 'modes.modeList.debug.name', - descKey: 'modes.modeList.debug.description', - featuresKey: 'modes.modeList.debug.features' - } -]; - -export const ModeIntroStep: React.FC = () => { - const { t } = useTranslation('onboarding'); - - return ( -
- {/* Title */} -
-

- {t('modes.title')} -

-

- {t('modes.description')} -

-
- - {/* Mode list */} -
- {MODE_LIST.map((mode) => { - const IconComponent = mode.icon; - const features = t(mode.featuresKey, { returnObjects: true }) as string[]; - - return ( -
-
- -
-
- {t(mode.nameKey)} -
-
- {t(mode.descKey)} -
-
- {Array.isArray(features) && features.slice(0, 2).map((feature, idx) => ( - - {feature} - - ))} -
-
- ); - })} -
- - {/* Tip */} -

- {t('modes.tip')} -

-
- ); -}; - -export default ModeIntroStep; diff --git a/src/web-ui/src/features/onboarding/components/steps/ModelConfigStep.tsx b/src/web-ui/src/features/onboarding/components/steps/ModelConfigStep.tsx deleted file mode 100644 index b420affa..00000000 --- a/src/web-ui/src/features/onboarding/components/steps/ModelConfigStep.tsx +++ /dev/null @@ -1,875 +0,0 @@ -/** - * ModelConfigStep - */ - -import React, { useState, useCallback, useMemo, useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Settings, Loader, Check, X, ExternalLink, ChevronDown, ChevronUp, AlertTriangle, Plus } from 'lucide-react'; -import { useOnboardingStore } from '../../store/onboardingStore'; -import { aiApi } from '@/infrastructure/api'; -import { systemAPI } from '@/infrastructure/api'; -import { Select, Checkbox, Button, IconButton } from '@/component-library'; -import { PROVIDER_TEMPLATES } from '@/infrastructure/config/services/modelConfigs'; -import { createLogger } from '@/shared/utils/logger'; -import { translateConnectionTestMessage } from '@/shared/utils/aiConnectionTestMessages'; - -const log = createLogger('ModelConfigStep'); - -interface ModelConfigStepProps { - onSkipForNow: () => void; -} - -/** Provider display order */ -const PROVIDER_ORDER = ['openbitfun', 'zhipu', 'qwen', 'deepseek', 'volcengine', 'minimax', 'moonshot', 'gemini', 'anthropic']; - -type TestStatus = 'idle' | 'testing' | 'success' | 'error'; -type RemoteModelOption = { id: string; display_name?: string }; - -export const ModelConfigStep: React.FC = ({ onSkipForNow }) => { - const { t } = useTranslation('onboarding'); - const { t: tAiModel } = useTranslation('settings/ai-model'); - const { modelConfig, setModelConfig } = useOnboardingStore(); - - // Basic fields - const [selectedProviderId, setSelectedProviderId] = useState(modelConfig?.provider || ''); - const [apiKey, setApiKey] = useState(modelConfig?.apiKey || ''); - const [baseUrl, setBaseUrl] = useState(modelConfig?.baseUrl || ''); - const [modelName, setModelName] = useState(modelConfig?.modelName || ''); - const [customFormat, setCustomFormat] = useState<'openai' | 'responses' | 'anthropic' | 'gemini'>( - (modelConfig?.format as 'openai' | 'responses' | 'anthropic' | 'gemini') || 'openai' - ); - const [testStatus, setTestStatus] = useState('idle'); - const [testError, setTestError] = useState(''); - const [testNotice, setTestNotice] = useState(''); - const [remoteModelOptions, setRemoteModelOptions] = useState([]); - const [isFetchingRemoteModels, setIsFetchingRemoteModels] = useState(false); - const [remoteModelsError, setRemoteModelsError] = useState(''); - const [hasAttemptedRemoteFetch, setHasAttemptedRemoteFetch] = useState(false); - - // Advanced settings - restore from store so state survives unmount/remount - const [showAdvancedSettings, setShowAdvancedSettings] = useState( - Boolean(modelConfig?.customRequestBody || modelConfig?.skipSslVerify || modelConfig?.customHeaders) - ); - const [customRequestBody, setCustomRequestBody] = useState(modelConfig?.customRequestBody || ''); - const [skipSslVerify, setSkipSslVerify] = useState(modelConfig?.skipSslVerify || false); - const [customHeaders, setCustomHeaders] = useState>(modelConfig?.customHeaders || {}); - const [customHeadersMode, setCustomHeadersMode] = useState<'merge' | 'replace'>( - modelConfig?.customHeadersMode || 'merge' - ); - - // Build sorted provider options from PROVIDER_TEMPLATES - const providerOptions = useMemo(() => { - const sorted = PROVIDER_ORDER - .filter(id => PROVIDER_TEMPLATES[id]) - .map(id => PROVIDER_TEMPLATES[id]); - - // Add any templates not in the explicit order - Object.values(PROVIDER_TEMPLATES).forEach(template => { - if (!PROVIDER_ORDER.includes(template.id)) { - sorted.push(template); - } - }); - - // Dynamically get translated name and description - return sorted.map(provider => ({ - ...provider, - name: tAiModel(`providers.${provider.id}.name`), - description: tAiModel(`providers.${provider.id}.description`) - })); - }, [tAiModel]); - - // Build select options: custom first, then preset providers - const selectOptions = useMemo(() => { - const options: Array<{ label: string; value: string; description: string }> = [{ - label: t('model.provider.options.custom'), - value: 'custom', - description: t('model.provider.customDescription') - }]; - providerOptions.forEach(p => { - options.push({ - label: p.name, - value: p.id, - description: p.description - }); - }); - return options; - }, [providerOptions, t]); - - // Current template (null if custom or not selected) - const currentTemplate = useMemo(() => { - if (!selectedProviderId || selectedProviderId === 'custom') return null; - const template = PROVIDER_TEMPLATES[selectedProviderId]; - if (!template) return null; - // Dynamically get translated name, description, and baseUrlOptions notes - return { - ...template, - name: tAiModel(`providers.${template.id}.name`), - description: tAiModel(`providers.${template.id}.description`), - baseUrlOptions: template.baseUrlOptions?.map(opt => ({ - ...opt, - note: tAiModel(`providers.${template.id}.urlOptions.${opt.note}`, { defaultValue: opt.note }) - })) - }; - }, [selectedProviderId, tAiModel]); - - const resetRemoteModelDiscovery = useCallback(() => { - setRemoteModelOptions([]); - setIsFetchingRemoteModels(false); - setRemoteModelsError(''); - setHasAttemptedRemoteFetch(false); - }, []); - - const buildModelDiscoveryConfig = useCallback(() => { - const template = selectedProviderId !== 'custom' ? PROVIDER_TEMPLATES[selectedProviderId] : null; - const resolvedBaseUrl = (baseUrl || template?.baseUrl || '').trim(); - const resolvedModelName = (modelName || template?.models[0] || 'model-discovery').trim(); - let resolvedFormat: 'openai' | 'responses' | 'anthropic' | 'gemini' = customFormat; - if (template) { - if (template.baseUrlOptions?.length) { - const effectiveUrl = baseUrl || template.baseUrl; - const matchedOption = template.baseUrlOptions.find(opt => opt.url === effectiveUrl); - resolvedFormat = matchedOption ? matchedOption.format : template.format; - } else { - resolvedFormat = template.format; - } - } - const resolvedApiKey = apiKey.trim(); - - if (!resolvedBaseUrl || !resolvedApiKey) { - return null; - } - - return { - id: 'onboarding_model_discovery', - name: 'Onboarding Model Discovery', - provider: resolvedFormat, - api_key: resolvedApiKey, - base_url: resolvedBaseUrl, - request_url: resolvedBaseUrl, - model_name: resolvedModelName, - enabled: true, - category: 'general_chat', - capabilities: ['text_chat'], - recommended_for: [], - metadata: {}, - context_window: 128000, - max_tokens: 8192, - enable_thinking_process: false, - support_preserved_thinking: false, - inline_think_in_text: false, - skip_ssl_verify: skipSslVerify, - custom_headers: Object.keys(customHeaders).length > 0 ? customHeaders : undefined, - custom_headers_mode: Object.keys(customHeaders).length > 0 ? customHeadersMode : undefined, - custom_request_body: customRequestBody.trim() || undefined, - }; - }, [apiKey, baseUrl, modelName, selectedProviderId, customFormat, skipSslVerify, customHeaders, customHeadersMode, customRequestBody]); - - const fetchRemoteModels = useCallback(async () => { - const discoveryConfig = buildModelDiscoveryConfig(); - if (!discoveryConfig) { - setRemoteModelOptions([]); - setRemoteModelsError(tAiModel('providerSelection.fillApiKeyBeforeFetch')); - setHasAttemptedRemoteFetch(true); - return; - } - - setIsFetchingRemoteModels(true); - setRemoteModelsError(''); - setHasAttemptedRemoteFetch(true); - - try { - const remoteModels = await aiApi.listModelsByConfig(discoveryConfig); - const dedupedModels = remoteModels.filter((model, index, arr) => ( - !!model.id && arr.findIndex(item => item.id === model.id) === index - )); - - if (dedupedModels.length === 0) { - setRemoteModelOptions([]); - setRemoteModelsError(tAiModel('providerSelection.fetchEmptyFallback')); - return; - } - - setRemoteModelOptions(dedupedModels); - setRemoteModelsError(''); - } catch (error) { - log.warn('Failed to fetch remote models during onboarding, falling back', { error }); - setRemoteModelOptions([]); - setRemoteModelsError(tAiModel('providerSelection.fetchFailedFallback')); - } finally { - setIsFetchingRemoteModels(false); - } - }, [buildModelDiscoveryConfig, tAiModel]); - - // Stable JSON representation of customHeaders for useEffect dependency - const customHeadersJson = useMemo(() => JSON.stringify(customHeaders), [customHeaders]); - - // Sync form state to onboarding store whenever fields change - useEffect(() => { - if (!selectedProviderId) { - setModelConfig(null); - return; - } - - const template = selectedProviderId !== 'custom' ? PROVIDER_TEMPLATES[selectedProviderId] : null; - const effectiveBaseUrl = baseUrl || (template?.baseUrl || ''); - const effectiveModelName = modelName || (template?.models[0] || ''); - - // Derive format - let format: 'openai' | 'responses' | 'anthropic' | 'gemini' = customFormat; - if (template) { - if (template.baseUrlOptions?.length) { - const effectiveUrl = baseUrl || template.baseUrl; - const matched = template.baseUrlOptions.find(opt => opt.url === effectiveUrl); - format = matched ? matched.format : template.format; - } else { - format = template.format; - } - } - - const translatedName = template ? tAiModel(`providers.${template.id}.name`) : null; - const customLabel = t('model.provider.options.custom'); - const configName = translatedName || customLabel; - - const parsedHeaders = JSON.parse(customHeadersJson) as Record; - - setModelConfig({ - provider: selectedProviderId, - apiKey, - baseUrl: effectiveBaseUrl, - modelName: effectiveModelName, - format, - configName, - customRequestBody: customRequestBody.trim() || undefined, - skipSslVerify: skipSslVerify || undefined, - customHeaders: Object.keys(parsedHeaders).length > 0 ? parsedHeaders : undefined, - customHeadersMode: Object.keys(parsedHeaders).length > 0 ? customHeadersMode : undefined, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedProviderId, apiKey, baseUrl, modelName, customFormat, customRequestBody, skipSslVerify, customHeadersJson, customHeadersMode]); - - // Handle provider change - const handleProviderChange = useCallback((newProviderId: string) => { - resetRemoteModelDiscovery(); - setSelectedProviderId(newProviderId); - setTestStatus('idle'); - setTestError(''); - setTestNotice(''); - - if (newProviderId === 'custom') { - setBaseUrl(''); - setModelName(''); - } else { - const template = PROVIDER_TEMPLATES[newProviderId]; - if (template) { - setBaseUrl(template.baseUrl); - setModelName(template.models[0] || ''); - } - } - }, [resetRemoteModelDiscovery]); - - // Handle "skip for now": clear config and proceed - const handleSkipForNow = useCallback(() => { - setModelConfig(null); - onSkipForNow(); - }, [setModelConfig, onSkipForNow]); - - // Handle model name change from template select - const handleModelNameChange = useCallback((value: string) => { - setModelName(value); - setTestStatus('idle'); - setTestError(''); - setTestNotice(''); - }, []); - - // Open help URL - const handleOpenHelpUrl = useCallback(async () => { - if (currentTemplate?.helpUrl) { - try { - await systemAPI.openExternal(currentTemplate.helpUrl); - } catch { - window.open(currentTemplate.helpUrl, '_blank'); - } - } - }, [currentTemplate]); - - // Get effective format - const getEffectiveFormat = useCallback(() => { - if (currentTemplate) { - // If the template has baseUrlOptions, derive format from the selected URL - if (currentTemplate.baseUrlOptions && currentTemplate.baseUrlOptions.length > 0) { - const effectiveUrl = baseUrl || currentTemplate.baseUrl; - const matchedOption = currentTemplate.baseUrlOptions.find(opt => opt.url === effectiveUrl); - if (matchedOption) { - return matchedOption.format; - } - } - return currentTemplate.format; - } - return customFormat; - }, [currentTemplate, customFormat, baseUrl]); - - // Test connection (purely for connectivity validation, does not affect saving) - const handleTestConnection = useCallback(async () => { - if (!apiKey || !selectedProviderId) return; - - setTestStatus('testing'); - setTestError(''); - setTestNotice(''); - - try { - const effectiveBaseUrl = baseUrl || (currentTemplate?.baseUrl || ''); - const effectiveModelName = modelName || (currentTemplate?.models[0] || ''); - const format = getEffectiveFormat(); - - const result = await aiApi.testConfigConnection({ - base_url: effectiveBaseUrl, - api_key: apiKey, - model_name: effectiveModelName, - provider: format - }); - const localizedMessage = translateConnectionTestMessage(result.message_code, tAiModel); - - if (result.success) { - setTestStatus('success'); - setTestNotice(localizedMessage || result.error_details || ''); - log.info('Connection test passed', { - provider: selectedProviderId, - modelName: effectiveModelName - }); - } else { - setTestStatus('error'); - setTestNotice(''); - const detailLines = [ - localizedMessage, - result.error_details ? `${tAiModel('messages.errorDetails')}: ${result.error_details}` : undefined - ].filter((line): line is string => Boolean(line)); - const errorMsg = detailLines.length > 0 - ? `${t('model.testFailed')}\n${detailLines.join('\n')}` - : t('model.testFailed'); - setTestError(errorMsg); - } - } catch (error) { - log.error('Connection test failed', error); - setTestStatus('error'); - setTestNotice(''); - const rawMsg = error instanceof Error ? error.message : String(error); - // Tauri command errors often have "Connection test failed: " prefix, extract the actual cause - const cleanMsg = rawMsg.replace(/^Connection test failed:\s*/i, ''); - setTestError(cleanMsg ? `${t('model.testFailed')}\n${cleanMsg}` : t('model.testFailed')); - } - }, [apiKey, selectedProviderId, baseUrl, modelName, currentTemplate, getEffectiveFormat, t]); - - // Render test button - const renderTestButton = () => { - const isDisabled = !apiKey || !selectedProviderId || testStatus === 'testing'; - - let buttonClass = 'bitfun-onboarding-model__test-btn'; - let content: React.ReactNode = t('model.testConnection'); - - switch (testStatus) { - case 'testing': - content = ( - <> - - {t('model.testing')} - - ); - break; - case 'success': - buttonClass += ' bitfun-onboarding-model__test-btn--success'; - content = ( - <> - - {t('model.testSuccess')} - - ); - break; - case 'error': - buttonClass += ' bitfun-onboarding-model__test-btn--error'; - content = ( - <> - - {t('model.testFailed')} - - ); - break; - } - - return ( - - ); - }; - - // Validate custom request body JSON - const customRequestBodyValidation = useMemo(() => { - if (!customRequestBody || !customRequestBody.trim()) return null; - try { - JSON.parse(customRequestBody); - return 'valid'; - } catch { - return 'invalid'; - } - }, [customRequestBody]); - - // Whether a provider is selected (to show the form) - const isProviderSelected = !!selectedProviderId; - const availableModelOptions = ( - remoteModelOptions.length > 0 - ? remoteModelOptions.map(model => ({ - label: `${currentTemplate?.name || t('model.provider.options.custom')}/${model.display_name || model.id}`, - value: model.id, - description: model.display_name && model.display_name !== model.id ? model.id : undefined - })) - : (currentTemplate?.models || []).map(model => ({ - label: `${currentTemplate?.name || t('model.provider.options.custom')}/${model}`, - value: model - })) - ); - const modelFetchHint = isFetchingRemoteModels - ? tAiModel('providerSelection.fetchingModels') - : remoteModelsError - ? remoteModelsError - : remoteModelOptions.length > 0 - ? null - : currentTemplate?.models?.length - ? tAiModel('providerSelection.usingPresetModels') - : hasAttemptedRemoteFetch - ? tAiModel('providerSelection.noPresetModels') - : null; - - return ( -
- {/* Icon */} -
- -
- - {/* Header */} -
-

- {t('model.title')} -

-

- {t('model.description')} -

-
- - {/* Config Form */} -
- {/* Provider Select */} -
- handleModelNameChange(value as string)} - placeholder={t('model.modelName.selectPlaceholder')} - options={availableModelOptions} - searchable - allowCustomValue - loading={isFetchingRemoteModels} - emptyText={tAiModel('providerSelection.noPresetModels')} - searchPlaceholder={t('model.modelName.inputPlaceholder')} - customValueHint={t('model.modelName.customHint')} - /> -
- -
- {modelFetchHint && ( - - {modelFetchHint} - - )} -
- - {/* API Key */} -
- - { - resetRemoteModelDiscovery(); - setApiKey(e.target.value); - setTestStatus('idle'); - setTestError(''); - setTestNotice(''); - }} - /> - {currentTemplate.helpUrl && ( - - )} -
- - {/* Base URL (pre-filled, editable) */} -
- - {currentTemplate.baseUrlOptions && currentTemplate.baseUrlOptions.length > 0 ? ( - { - resetRemoteModelDiscovery(); - setBaseUrl(e.target.value); - setTestStatus('idle'); - setTestError(''); - setTestNotice(''); - }} - onFocus={(e) => e.target.select()} - /> - )} -
- - )} - - {/* Custom provider form */} - {isProviderSelected && selectedProviderId === 'custom' && ( - <> - {/* Base URL */} -
- - { - resetRemoteModelDiscovery(); - setBaseUrl(e.target.value); - setTestStatus('idle'); - setTestError(''); - setTestNotice(''); - }} - /> -
- - {/* Model Name (text input) */} -
- - { - resetRemoteModelDiscovery(); - setApiKey(e.target.value); - setTestStatus('idle'); - setTestError(''); - setTestNotice(''); - }} - /> -
- - {/* API Format */} -
- setCustomHeadersMode('merge')} - /> - {tAiModel('advancedSettings.customHeaders.modeMerge')} - - -
- - {Object.entries(customHeaders).map(([key, value], index) => ( -
- { - const newHeaders = { ...customHeaders }; - const oldValue = newHeaders[key]; - delete newHeaders[key]; - if (e.target.value) { - newHeaders[e.target.value] = oldValue; - } - setCustomHeaders(newHeaders); - }} - placeholder={tAiModel('advancedSettings.customHeaders.keyPlaceholder')} - style={{ flex: 1 }} - /> - { - setCustomHeaders(prev => ({ ...prev, [key]: e.target.value })); - }} - placeholder={tAiModel('advancedSettings.customHeaders.valuePlaceholder')} - style={{ flex: 1 }} - /> - { - const newHeaders = { ...customHeaders }; - delete newHeaders[key]; - setCustomHeaders(newHeaders); - }} - tooltip={tAiModel('actions.delete')} - > - - -
- ))} - -
- )} - - {/* Custom Request Body */} -
- - - {t('model.advanced.customRequestBodyHint')} - -