mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-13 21:57:30 +08:00
feat: add user onboarding guide system with driver.js
- Add OnboardingProvider with driver.js integration for guided tours - Implement multi-page navigation support (home -> settings -> provider) - Add selectApplicableGuides logic for new users and upgrades - Use MutationObserver for reliable element detection (replaces setTimeout) - Add Redux slice for onboarding state persistence - Add v1.7.0 onboarding guide with 8 steps: - Welcome, free model intro, settings navigation - Add provider, fill API key, add model - Use cases overview, completion - Add i18n translations for all supported languages - Add unit tests for selectApplicableGuides function - Add element IDs for onboarding targets (#sidebar-settings, #navbar-settings, #add-provider-btn, #add-model-btn, #api-key-input) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c7c380d706
commit
5f5f055979
@ -226,6 +226,7 @@
|
||||
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||
"@types/react-transition-group": "^4.4.12",
|
||||
"@types/react-window": "^1",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/swagger-jsdoc": "^6",
|
||||
"@types/swagger-ui-express": "^4.1.8",
|
||||
"@types/tinycolor2": "^1",
|
||||
@ -277,6 +278,7 @@
|
||||
"dompurify": "^3.2.6",
|
||||
"dotenv": "16.6.1",
|
||||
"dotenv-cli": "^7.4.2",
|
||||
"driver.js": "^1.4.0",
|
||||
"drizzle-kit": "^0.31.4",
|
||||
"drizzle-orm": "^0.44.5",
|
||||
"electron": "38.7.0",
|
||||
|
||||
30
pnpm-lock.yaml
generated
30
pnpm-lock.yaml
generated
@ -563,6 +563,9 @@ importers:
|
||||
'@types/react-window':
|
||||
specifier: ^1
|
||||
version: 1.8.8
|
||||
'@types/semver':
|
||||
specifier: ^7.7.1
|
||||
version: 7.7.1
|
||||
'@types/swagger-jsdoc':
|
||||
specifier: ^6
|
||||
version: 6.0.4
|
||||
@ -716,6 +719,9 @@ importers:
|
||||
dotenv-cli:
|
||||
specifier: ^7.4.2
|
||||
version: 7.4.4
|
||||
driver.js:
|
||||
specifier: ^1.4.0
|
||||
version: 1.4.0
|
||||
drizzle-kit:
|
||||
specifier: ^0.31.4
|
||||
version: 0.31.8
|
||||
@ -5287,6 +5293,9 @@ packages:
|
||||
'@types/retry@0.12.0':
|
||||
resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==}
|
||||
|
||||
'@types/semver@7.7.1':
|
||||
resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==}
|
||||
|
||||
'@types/send@1.2.1':
|
||||
resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==}
|
||||
|
||||
@ -6968,6 +6977,9 @@ packages:
|
||||
resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
driver.js@1.4.0:
|
||||
resolution: {integrity: sha512-Gm64jm6PmcU+si21sQhBrTAM1JvUrR0QhNmjkprNLxohOBzul9+pNHXgQaT9lW84gwg9GMLB3NZGuGolsz5uew==}
|
||||
|
||||
drizzle-kit@0.31.8:
|
||||
resolution: {integrity: sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg==}
|
||||
hasBin: true
|
||||
@ -17013,6 +17025,8 @@ snapshots:
|
||||
|
||||
'@types/retry@0.12.0': {}
|
||||
|
||||
'@types/semver@7.7.1': {}
|
||||
|
||||
'@types/send@1.2.1':
|
||||
dependencies:
|
||||
'@types/node': 22.17.2
|
||||
@ -17720,11 +17734,11 @@ snapshots:
|
||||
- utf-8-validate
|
||||
- vite
|
||||
|
||||
'@vitest/browser@3.2.4(msw@2.12.7(@types/node@24.10.4)(typescript@5.8.3))(playwright@1.57.0)(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4)':
|
||||
'@vitest/browser@3.2.4(msw@2.12.7(@types/node@24.10.4)(typescript@5.8.3))(playwright@1.57.0)(rolldown-vite@7.3.0(@types/node@22.17.2)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4)':
|
||||
dependencies:
|
||||
'@testing-library/dom': 10.4.1
|
||||
'@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1)
|
||||
'@vitest/mocker': 3.2.4(msw@2.12.7(@types/node@24.10.4)(typescript@5.8.3))(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
'@vitest/mocker': 3.2.4(msw@2.12.7(@types/node@24.10.4)(typescript@5.8.3))(rolldown-vite@7.3.0(@types/node@22.17.2)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
'@vitest/utils': 3.2.4
|
||||
magic-string: 0.30.21
|
||||
sirv: 3.0.2
|
||||
@ -17778,14 +17792,14 @@ snapshots:
|
||||
msw: 2.12.7(@types/node@22.17.2)(typescript@5.8.3)
|
||||
vite: rolldown-vite@7.3.0(@types/node@22.17.2)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
'@vitest/mocker@3.2.4(msw@2.12.7(@types/node@24.10.4)(typescript@5.8.3))(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
'@vitest/mocker@3.2.4(msw@2.12.7(@types/node@24.10.4)(typescript@5.8.3))(rolldown-vite@7.3.0(@types/node@22.17.2)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.2.4
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
msw: 2.12.7(@types/node@24.10.4)(typescript@5.8.3)
|
||||
vite: rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite: rolldown-vite@7.3.0(@types/node@22.17.2)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
'@vitest/pretty-format@3.2.4':
|
||||
dependencies:
|
||||
@ -17816,7 +17830,7 @@ snapshots:
|
||||
sirv: 3.0.2
|
||||
tinyglobby: 0.2.15
|
||||
tinyrainbow: 2.0.0
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(esbuild@0.25.12)(jiti@2.6.1)(jsdom@26.1.0)(msw@2.12.7(@types/node@24.10.4)(typescript@5.8.3))(tsx@4.21.0)(yaml@2.8.2)
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.2)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(esbuild@0.25.12)(jiti@2.6.1)(jsdom@26.1.0)(msw@2.12.7(@types/node@22.17.2)(typescript@5.8.3))(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
'@vitest/utils@3.2.4':
|
||||
dependencies:
|
||||
@ -19287,6 +19301,8 @@ snapshots:
|
||||
|
||||
dotenv@17.2.3: {}
|
||||
|
||||
driver.js@1.4.0: {}
|
||||
|
||||
drizzle-kit@0.31.8:
|
||||
dependencies:
|
||||
'@drizzle-team/brocli': 0.10.2
|
||||
@ -25271,7 +25287,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/expect': 3.2.4
|
||||
'@vitest/mocker': 3.2.4(msw@2.12.7(@types/node@24.10.4)(typescript@5.8.3))(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
'@vitest/mocker': 3.2.4(msw@2.12.7(@types/node@24.10.4)(typescript@5.8.3))(rolldown-vite@7.3.0(@types/node@22.17.2)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
'@vitest/pretty-format': 3.2.4
|
||||
'@vitest/runner': 3.2.4
|
||||
'@vitest/snapshot': 3.2.4
|
||||
@ -25295,7 +25311,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/debug': 4.1.12
|
||||
'@types/node': 24.10.4
|
||||
'@vitest/browser': 3.2.4(msw@2.12.7(@types/node@24.10.4)(typescript@5.8.3))(playwright@1.57.0)(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4)
|
||||
'@vitest/browser': 3.2.4(msw@2.12.7(@types/node@24.10.4)(typescript@5.8.3))(playwright@1.57.0)(rolldown-vite@7.3.0(@types/node@22.17.2)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4)
|
||||
'@vitest/ui': 3.2.4(vitest@3.2.4)
|
||||
jsdom: 26.1.0
|
||||
transitivePeerDependencies:
|
||||
|
||||
@ -6,6 +6,7 @@ import { HashRouter, Route, Routes } from 'react-router-dom'
|
||||
|
||||
import Sidebar from './components/app/Sidebar'
|
||||
import { ErrorBoundary } from './components/ErrorBoundary'
|
||||
import { OnboardingProvider } from './components/Onboarding'
|
||||
import TabsContainer from './components/Tab/TabContainer'
|
||||
import NavigationHandler from './handler/NavigationHandler'
|
||||
import { useNavbarPosition } from './hooks/useSettings'
|
||||
@ -49,17 +50,21 @@ const Router: FC = () => {
|
||||
if (navbarPosition === 'left') {
|
||||
return (
|
||||
<HashRouter>
|
||||
<Sidebar />
|
||||
{routes}
|
||||
<NavigationHandler />
|
||||
<OnboardingProvider>
|
||||
<Sidebar />
|
||||
{routes}
|
||||
<NavigationHandler />
|
||||
</OnboardingProvider>
|
||||
</HashRouter>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<HashRouter>
|
||||
<NavigationHandler />
|
||||
<TabsContainer>{routes}</TabsContainer>
|
||||
<OnboardingProvider>
|
||||
<NavigationHandler />
|
||||
<TabsContainer>{routes}</TabsContainer>
|
||||
</OnboardingProvider>
|
||||
</HashRouter>
|
||||
)
|
||||
}
|
||||
|
||||
223
src/renderer/src/components/Onboarding/OnboardingProvider.tsx
Normal file
223
src/renderer/src/components/Onboarding/OnboardingProvider.tsx
Normal file
@ -0,0 +1,223 @@
|
||||
import 'driver.js/dist/driver.css'
|
||||
import './styles/driver-theme.css'
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { completeFeatureGuide, completeOnboarding, skipOnboarding } from '@renderer/store/onboarding'
|
||||
import type { GuideStep, VersionGuide } from '@renderer/types/onboarding'
|
||||
import { type Driver, driver, type DriveStep } from 'driver.js'
|
||||
import type { FC, PropsWithChildren } from 'react'
|
||||
import { createContext, use, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
|
||||
const logger = loggerService.withContext('Onboarding')
|
||||
|
||||
interface OnboardingContextType {
|
||||
startGuide: (guide: VersionGuide) => void
|
||||
skipGuide: () => void
|
||||
isGuideActive: boolean
|
||||
currentGuide: VersionGuide | null
|
||||
completedOnboardingVersion: string | null
|
||||
completedFeatureGuides: string[]
|
||||
onboardingSkipped: boolean
|
||||
}
|
||||
|
||||
const OnboardingContext = createContext<OnboardingContextType>({
|
||||
startGuide: () => {},
|
||||
skipGuide: () => {},
|
||||
isGuideActive: false,
|
||||
currentGuide: null,
|
||||
completedOnboardingVersion: null,
|
||||
completedFeatureGuides: [],
|
||||
onboardingSkipped: false
|
||||
})
|
||||
|
||||
function resolveElement(step: GuideStep): Element | string | undefined {
|
||||
if (!step.element) return undefined
|
||||
if (typeof step.element === 'function') {
|
||||
return step.element() ?? undefined
|
||||
}
|
||||
return step.element
|
||||
}
|
||||
|
||||
function isOnRoute(currentPath: string, targetPath: string): boolean {
|
||||
// Special case for root path - must match exactly
|
||||
if (targetPath === '/') {
|
||||
return currentPath === '/'
|
||||
}
|
||||
return currentPath === targetPath || currentPath.startsWith(targetPath + '/')
|
||||
}
|
||||
|
||||
export const OnboardingProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
const { theme } = useTheme()
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useAppDispatch()
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [isGuideActive, setIsGuideActive] = useState(false)
|
||||
const [currentGuide, setCurrentGuide] = useState<VersionGuide | null>(null)
|
||||
|
||||
const driverRef = useRef<Driver | null>(null)
|
||||
const guideRef = useRef<{ guide: VersionGuide | null; steps: GuideStep[] }>({ guide: null, steps: [] })
|
||||
const pathRef = useRef(location.pathname)
|
||||
const navigatingRef = useRef(false)
|
||||
|
||||
// Keep pathRef in sync
|
||||
pathRef.current = location.pathname
|
||||
|
||||
const { completedOnboardingVersion, completedFeatureGuides, onboardingSkipped } = useAppSelector(
|
||||
(state) => state.onboarding
|
||||
)
|
||||
|
||||
const finishGuide = useCallback(
|
||||
(wasCompleted: boolean) => {
|
||||
const guide = guideRef.current.guide
|
||||
if (!guide) return
|
||||
|
||||
const action = wasCompleted ? 'completed' : 'skipped'
|
||||
logger.info(`Guide ${action}`, { version: guide.version, type: guide.type })
|
||||
|
||||
if (guide.type === 'onboarding') {
|
||||
dispatch(wasCompleted ? completeOnboarding(guide.version) : skipOnboarding(guide.version))
|
||||
} else {
|
||||
dispatch(completeFeatureGuide(guide.version))
|
||||
}
|
||||
|
||||
setIsGuideActive(false)
|
||||
setCurrentGuide(null)
|
||||
guideRef.current = { guide: null, steps: [] }
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const createDriverSteps = useCallback(
|
||||
(guideSteps: GuideStep[]): DriveStep[] =>
|
||||
guideSteps.map((step) => ({
|
||||
element: resolveElement(step),
|
||||
popover: {
|
||||
title: t(step.titleKey),
|
||||
description: t(step.descriptionKey),
|
||||
side: step.side === 'over' ? undefined : step.side,
|
||||
align: step.align
|
||||
}
|
||||
})),
|
||||
[t]
|
||||
)
|
||||
|
||||
const createAndStartDriver = useCallback(
|
||||
(fromStepIndex: number) => {
|
||||
const { steps: guideSteps } = guideRef.current
|
||||
if (!guideSteps.length) return
|
||||
|
||||
const steps = createDriverSteps(guideSteps)
|
||||
|
||||
const driverInstance = driver({
|
||||
animate: true,
|
||||
showProgress: true,
|
||||
overlayColor: theme === 'dark' ? 'rgba(0, 0, 0, 0.75)' : 'rgba(255, 255, 255, 0.75)',
|
||||
stagePadding: 10,
|
||||
stageRadius: 8,
|
||||
allowClose: true,
|
||||
smoothScroll: true,
|
||||
progressText: t('onboarding.progress'),
|
||||
nextBtnText: t('onboarding.next'),
|
||||
prevBtnText: t('onboarding.previous'),
|
||||
doneBtnText: t('onboarding.done'),
|
||||
popoverClass: `cherry-driver-popover ${theme}`,
|
||||
steps,
|
||||
onHighlightStarted: () => {
|
||||
// Skip if we just navigated (waiting for re-drive)
|
||||
if (navigatingRef.current) return
|
||||
|
||||
const stepIndex = driverRef.current?.getActiveIndex() ?? 0
|
||||
const guideStep = guideRef.current.steps[stepIndex]
|
||||
const targetPath = guideStep?.navigateTo
|
||||
|
||||
if (!targetPath || isOnRoute(pathRef.current, targetPath)) return
|
||||
|
||||
logger.info('Navigating to', { route: targetPath, stepId: guideStep.id })
|
||||
navigatingRef.current = true
|
||||
navigate(targetPath)
|
||||
|
||||
// After navigation, re-drive from same step to re-resolve elements
|
||||
setTimeout(() => {
|
||||
navigatingRef.current = false
|
||||
driverRef.current?.drive(stepIndex)
|
||||
}, 200)
|
||||
},
|
||||
onDestroyStarted: () => {
|
||||
if (navigatingRef.current) return
|
||||
|
||||
const wasCompleted = driverRef.current?.isLastStep() ?? false
|
||||
finishGuide(wasCompleted)
|
||||
driverRef.current?.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
driverRef.current = driverInstance
|
||||
driverInstance.drive(fromStepIndex)
|
||||
},
|
||||
[theme, t, navigate, createDriverSteps, finishGuide]
|
||||
)
|
||||
|
||||
const startGuide = useCallback(
|
||||
(guide: VersionGuide) => {
|
||||
if (isGuideActive) {
|
||||
logger.warn('Guide already active, ignoring request')
|
||||
return
|
||||
}
|
||||
|
||||
logger.info('Starting guide', { version: guide.version, type: guide.type })
|
||||
|
||||
setCurrentGuide(guide)
|
||||
guideRef.current = { guide, steps: guide.steps }
|
||||
setIsGuideActive(true)
|
||||
|
||||
createAndStartDriver(0)
|
||||
},
|
||||
[isGuideActive, createAndStartDriver]
|
||||
)
|
||||
|
||||
const skipGuide = useCallback(() => {
|
||||
if (driverRef.current) {
|
||||
driverRef.current.destroy()
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (driverRef.current) {
|
||||
driverRef.current.destroy()
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
startGuide,
|
||||
skipGuide,
|
||||
isGuideActive,
|
||||
currentGuide,
|
||||
completedOnboardingVersion,
|
||||
completedFeatureGuides,
|
||||
onboardingSkipped
|
||||
}),
|
||||
[
|
||||
startGuide,
|
||||
skipGuide,
|
||||
isGuideActive,
|
||||
currentGuide,
|
||||
completedOnboardingVersion,
|
||||
completedFeatureGuides,
|
||||
onboardingSkipped
|
||||
]
|
||||
)
|
||||
|
||||
return <OnboardingContext value={contextValue}>{children}</OnboardingContext>
|
||||
}
|
||||
|
||||
export const useOnboarding = () => use(OnboardingContext)
|
||||
1
src/renderer/src/components/Onboarding/index.ts
Normal file
1
src/renderer/src/components/Onboarding/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { OnboardingProvider, useOnboarding } from './OnboardingProvider'
|
||||
131
src/renderer/src/components/Onboarding/styles/driver-theme.css
Normal file
131
src/renderer/src/components/Onboarding/styles/driver-theme.css
Normal file
@ -0,0 +1,131 @@
|
||||
/* Cherry Studio driver.js theme customization */
|
||||
|
||||
/* Base popover styles using CSS variables */
|
||||
/* popoverClass is applied directly to .driver-popover element */
|
||||
.driver-popover.cherry-driver-popover {
|
||||
background-color: var(--color-background) !important;
|
||||
color: var(--color-text) !important;
|
||||
border: 1px solid var(--color-border) !important;
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15) !important;
|
||||
max-width: 360px !important;
|
||||
font-family: var(--font-family) !important;
|
||||
}
|
||||
|
||||
.driver-popover.cherry-driver-popover .driver-popover-title {
|
||||
color: var(--color-text) !important;
|
||||
font-size: 16px !important;
|
||||
font-weight: 600 !important;
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
.driver-popover.cherry-driver-popover .driver-popover-description {
|
||||
color: var(--color-text-2) !important;
|
||||
font-size: 14px !important;
|
||||
line-height: 1.6 !important;
|
||||
}
|
||||
|
||||
.driver-popover.cherry-driver-popover .driver-popover-progress-text {
|
||||
color: var(--color-text-3) !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
/* Button styles */
|
||||
.driver-popover.cherry-driver-popover .driver-popover-navigation-btns {
|
||||
gap: 8px !important;
|
||||
}
|
||||
|
||||
.driver-popover.cherry-driver-popover .driver-popover-next-btn,
|
||||
.driver-popover.cherry-driver-popover .driver-popover-prev-btn,
|
||||
.driver-popover.cherry-driver-popover .driver-popover-close-btn {
|
||||
border-radius: 6px !important;
|
||||
padding: 8px 16px !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 500 !important;
|
||||
transition: all 0.2s ease !important;
|
||||
text-shadow: none !important;
|
||||
-webkit-font-smoothing: antialiased !important;
|
||||
-moz-osx-font-smoothing: grayscale !important;
|
||||
}
|
||||
|
||||
/* Remove any pseudo-elements that might cause text duplication */
|
||||
.driver-popover.cherry-driver-popover .driver-popover-next-btn::before,
|
||||
.driver-popover.cherry-driver-popover .driver-popover-next-btn::after,
|
||||
.driver-popover.cherry-driver-popover .driver-popover-prev-btn::before,
|
||||
.driver-popover.cherry-driver-popover .driver-popover-prev-btn::after {
|
||||
content: none !important;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.driver-popover.cherry-driver-popover .driver-popover-next-btn {
|
||||
background-color: var(--color-primary) !important;
|
||||
color: var(--color-white) !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.driver-popover.cherry-driver-popover .driver-popover-next-btn:hover {
|
||||
opacity: 0.9 !important;
|
||||
}
|
||||
|
||||
.driver-popover.cherry-driver-popover .driver-popover-prev-btn {
|
||||
background-color: transparent !important;
|
||||
color: var(--color-text-2) !important;
|
||||
border: 1px solid var(--color-border) !important;
|
||||
}
|
||||
|
||||
.driver-popover.cherry-driver-popover .driver-popover-prev-btn:hover {
|
||||
background-color: var(--color-background-soft) !important;
|
||||
}
|
||||
|
||||
.driver-popover.cherry-driver-popover .driver-popover-close-btn {
|
||||
background-color: transparent !important;
|
||||
color: var(--color-text-3) !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.driver-popover.cherry-driver-popover .driver-popover-close-btn:hover {
|
||||
color: var(--color-text) !important;
|
||||
}
|
||||
|
||||
/* Dark theme specific adjustments */
|
||||
.driver-popover.cherry-driver-popover.dark {
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4) !important;
|
||||
}
|
||||
|
||||
/* Light theme specific adjustments */
|
||||
.driver-popover.cherry-driver-popover.light {
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
/* Highlight stage */
|
||||
.driver-active-element {
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
/* Arrow styles */
|
||||
.driver-popover.cherry-driver-popover .driver-popover-arrow {
|
||||
border-color: var(--color-background) !important;
|
||||
}
|
||||
|
||||
.driver-popover.cherry-driver-popover .driver-popover-arrow-side-left {
|
||||
border-right-color: var(--color-background) !important;
|
||||
}
|
||||
|
||||
.driver-popover.cherry-driver-popover .driver-popover-arrow-side-right {
|
||||
border-left-color: var(--color-background) !important;
|
||||
}
|
||||
|
||||
.driver-popover.cherry-driver-popover .driver-popover-arrow-side-top {
|
||||
border-bottom-color: var(--color-background) !important;
|
||||
}
|
||||
|
||||
.driver-popover.cherry-driver-popover .driver-popover-arrow-side-bottom {
|
||||
border-top-color: var(--color-background) !important;
|
||||
}
|
||||
|
||||
/* Footer area */
|
||||
.driver-popover.cherry-driver-popover .driver-popover-footer {
|
||||
margin-top: 16px !important;
|
||||
padding-top: 12px !important;
|
||||
border-top: 1px solid var(--color-border) !important;
|
||||
}
|
||||
@ -283,7 +283,7 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
|
||||
)}
|
||||
</ThemeButton>
|
||||
</Tooltip>
|
||||
<SettingsButton onClick={handleSettingsClick} $active={activeTabId === 'settings'}>
|
||||
<SettingsButton id="navbar-settings" onClick={handleSettingsClick} $active={activeTabId === 'settings'}>
|
||||
<Settings size={16} />
|
||||
</SettingsButton>
|
||||
</RightButtonsContainer>
|
||||
|
||||
@ -102,6 +102,7 @@ const Sidebar: FC = () => {
|
||||
</Tooltip>
|
||||
<Tooltip title={t('settings.title')} mouseEnterDelay={0.8} placement="right">
|
||||
<StyledLink
|
||||
id="sidebar-settings"
|
||||
onClick={async () => {
|
||||
hideMinappPopup()
|
||||
await to('/settings/provider')
|
||||
|
||||
@ -0,0 +1,253 @@
|
||||
import type { VersionGuide } from '@renderer/types/onboarding'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// Mock the allGuides to control test data
|
||||
vi.mock('../guides', () => ({
|
||||
allGuides: [] as VersionGuide[]
|
||||
}))
|
||||
|
||||
import { allGuides } from '../guides'
|
||||
import { selectApplicableGuides } from '../index'
|
||||
|
||||
// Helper to set mock guides
|
||||
function setMockGuides(guides: VersionGuide[]) {
|
||||
;(allGuides as VersionGuide[]).length = 0
|
||||
;(allGuides as VersionGuide[]).push(...guides)
|
||||
}
|
||||
|
||||
// Test fixture: minimal guide factory
|
||||
function createGuide(overrides: Partial<VersionGuide> & Pick<VersionGuide, 'version' | 'type'>): VersionGuide {
|
||||
return {
|
||||
titleKey: 'test.title',
|
||||
descriptionKey: 'test.description',
|
||||
steps: [],
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe('selectApplicableGuides', () => {
|
||||
describe('New User (completedOnboardingVersion === null && !onboardingSkipped)', () => {
|
||||
it('should return latest onboarding guide for new user', () => {
|
||||
setMockGuides([
|
||||
createGuide({ version: '1.6.0', type: 'onboarding' }),
|
||||
createGuide({ version: '1.7.0', type: 'onboarding' }),
|
||||
createGuide({ version: '1.5.0', type: 'onboarding' })
|
||||
])
|
||||
|
||||
const result = selectApplicableGuides('1.7.0', null, [], false)
|
||||
|
||||
expect(result.isNewUser).toBe(true)
|
||||
expect(result.previousVersion).toBe(null)
|
||||
expect(result.guides).toHaveLength(1)
|
||||
expect(result.guides[0].version).toBe('1.7.0')
|
||||
})
|
||||
|
||||
it('should only return onboarding guides, not feature guides', () => {
|
||||
setMockGuides([
|
||||
createGuide({ version: '1.7.0', type: 'onboarding' }),
|
||||
createGuide({ version: '1.7.0', type: 'feature' }),
|
||||
createGuide({ version: '1.8.0', type: 'feature' })
|
||||
])
|
||||
|
||||
const result = selectApplicableGuides('1.8.0', null, [], false)
|
||||
|
||||
expect(result.guides).toHaveLength(1)
|
||||
expect(result.guides[0].type).toBe('onboarding')
|
||||
})
|
||||
|
||||
it('should return empty array when no onboarding guides exist', () => {
|
||||
setMockGuides([
|
||||
createGuide({ version: '1.7.0', type: 'feature' }),
|
||||
createGuide({ version: '1.8.0', type: 'feature' })
|
||||
])
|
||||
|
||||
const result = selectApplicableGuides('1.8.0', null, [], false)
|
||||
|
||||
expect(result.isNewUser).toBe(true)
|
||||
expect(result.guides).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Skipped User (onboardingSkipped === true)', () => {
|
||||
it('should return no guides when user has skipped', () => {
|
||||
setMockGuides([
|
||||
createGuide({ version: '1.7.0', type: 'onboarding' }),
|
||||
createGuide({ version: '1.8.0', type: 'feature' })
|
||||
])
|
||||
|
||||
const result = selectApplicableGuides('1.8.0', null, [], true)
|
||||
|
||||
expect(result.isNewUser).toBe(false)
|
||||
expect(result.guides).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should preserve previousVersion when user has skipped', () => {
|
||||
setMockGuides([createGuide({ version: '1.7.0', type: 'onboarding' })])
|
||||
|
||||
const result = selectApplicableGuides('1.8.0', '1.6.0', [], true)
|
||||
|
||||
expect(result.previousVersion).toBe('1.6.0')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Upgrade User (completed onboarding, checking feature guides)', () => {
|
||||
it('should return feature guides newer than completed version', () => {
|
||||
setMockGuides([
|
||||
createGuide({ version: '1.7.0', type: 'onboarding' }),
|
||||
createGuide({ version: '1.7.5', type: 'feature' }),
|
||||
createGuide({ version: '1.8.0', type: 'feature' })
|
||||
])
|
||||
|
||||
const result = selectApplicableGuides('1.8.0', '1.7.0', [], false)
|
||||
|
||||
expect(result.isNewUser).toBe(false)
|
||||
expect(result.previousVersion).toBe('1.7.0')
|
||||
expect(result.guides).toHaveLength(2)
|
||||
expect(result.guides.map((g) => g.version)).toEqual(['1.7.5', '1.8.0'])
|
||||
})
|
||||
|
||||
it('should not return feature guides older than completed version', () => {
|
||||
setMockGuides([
|
||||
createGuide({ version: '1.5.0', type: 'feature' }),
|
||||
createGuide({ version: '1.6.0', type: 'feature' }),
|
||||
createGuide({ version: '1.8.0', type: 'feature' })
|
||||
])
|
||||
|
||||
const result = selectApplicableGuides('1.8.0', '1.7.0', [], false)
|
||||
|
||||
expect(result.guides).toHaveLength(1)
|
||||
expect(result.guides[0].version).toBe('1.8.0')
|
||||
})
|
||||
|
||||
it('should not return feature guides newer than current app version', () => {
|
||||
setMockGuides([
|
||||
createGuide({ version: '1.8.0', type: 'feature' }),
|
||||
createGuide({ version: '1.9.0', type: 'feature' }),
|
||||
createGuide({ version: '2.0.0', type: 'feature' })
|
||||
])
|
||||
|
||||
const result = selectApplicableGuides('1.8.0', '1.7.0', [], false)
|
||||
|
||||
expect(result.guides).toHaveLength(1)
|
||||
expect(result.guides[0].version).toBe('1.8.0')
|
||||
})
|
||||
|
||||
it('should exclude already completed feature guides', () => {
|
||||
setMockGuides([
|
||||
createGuide({ version: '1.7.5', type: 'feature' }),
|
||||
createGuide({ version: '1.8.0', type: 'feature' })
|
||||
])
|
||||
|
||||
const result = selectApplicableGuides('1.8.0', '1.7.0', ['1.7.5'], false)
|
||||
|
||||
expect(result.guides).toHaveLength(1)
|
||||
expect(result.guides[0].version).toBe('1.8.0')
|
||||
})
|
||||
|
||||
it('should not return onboarding guides for upgrade users', () => {
|
||||
setMockGuides([
|
||||
createGuide({ version: '1.8.0', type: 'onboarding' }),
|
||||
createGuide({ version: '1.8.0', type: 'feature' })
|
||||
])
|
||||
|
||||
const result = selectApplicableGuides('1.8.0', '1.7.0', [], false)
|
||||
|
||||
expect(result.guides).toHaveLength(1)
|
||||
expect(result.guides[0].type).toBe('feature')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Sorting', () => {
|
||||
it('should sort feature guides by version ascending', () => {
|
||||
setMockGuides([
|
||||
createGuide({ version: '1.9.0', type: 'feature' }),
|
||||
createGuide({ version: '1.7.5', type: 'feature' }),
|
||||
createGuide({ version: '1.8.0', type: 'feature' })
|
||||
])
|
||||
|
||||
const result = selectApplicableGuides('1.9.0', '1.7.0', [], false)
|
||||
|
||||
expect(result.guides.map((g) => g.version)).toEqual(['1.7.5', '1.8.0', '1.9.0'])
|
||||
})
|
||||
|
||||
it('should sort by priority (descending) when versions are equal', () => {
|
||||
setMockGuides([
|
||||
createGuide({ version: '1.8.0', type: 'feature', priority: 10 }),
|
||||
createGuide({ version: '1.8.0', type: 'feature', priority: 50 }),
|
||||
createGuide({ version: '1.8.0', type: 'feature', priority: 30 })
|
||||
])
|
||||
|
||||
const result = selectApplicableGuides('1.8.0', '1.7.0', [], false)
|
||||
|
||||
expect(result.guides.map((g) => g.priority)).toEqual([50, 30, 10])
|
||||
})
|
||||
|
||||
it('should handle guides without priority (default to 0)', () => {
|
||||
setMockGuides([
|
||||
createGuide({ version: '1.8.0', type: 'feature' }),
|
||||
createGuide({ version: '1.8.0', type: 'feature', priority: 20 })
|
||||
])
|
||||
|
||||
const result = selectApplicableGuides('1.8.0', '1.7.0', [], false)
|
||||
|
||||
expect(result.guides[0].priority).toBe(20)
|
||||
expect(result.guides[1].priority).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should sort onboarding guides by version descending (latest first)', () => {
|
||||
setMockGuides([
|
||||
createGuide({ version: '1.5.0', type: 'onboarding' }),
|
||||
createGuide({ version: '1.8.0', type: 'onboarding' }),
|
||||
createGuide({ version: '1.7.0', type: 'onboarding' })
|
||||
])
|
||||
|
||||
const result = selectApplicableGuides('1.8.0', null, [], false)
|
||||
|
||||
// Only returns the latest one
|
||||
expect(result.guides).toHaveLength(1)
|
||||
expect(result.guides[0].version).toBe('1.8.0')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should return empty array when no guides exist', () => {
|
||||
setMockGuides([])
|
||||
|
||||
const newUserResult = selectApplicableGuides('1.8.0', null, [], false)
|
||||
expect(newUserResult.guides).toHaveLength(0)
|
||||
|
||||
const upgradeResult = selectApplicableGuides('1.8.0', '1.7.0', [], false)
|
||||
expect(upgradeResult.guides).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should handle semver pre-release versions', () => {
|
||||
setMockGuides([
|
||||
createGuide({ version: '1.8.0-beta.1', type: 'feature' }),
|
||||
createGuide({ version: '1.8.0', type: 'feature' })
|
||||
])
|
||||
|
||||
const result = selectApplicableGuides('1.8.0', '1.7.0', [], false)
|
||||
|
||||
// Pre-release < release for same version
|
||||
expect(result.guides.map((g) => g.version)).toEqual(['1.8.0-beta.1', '1.8.0'])
|
||||
})
|
||||
|
||||
it('should handle feature guide version equal to completed version (not shown)', () => {
|
||||
setMockGuides([createGuide({ version: '1.7.0', type: 'feature' })])
|
||||
|
||||
const result = selectApplicableGuides('1.8.0', '1.7.0', [], false)
|
||||
|
||||
// 1.7.0 is not > 1.7.0, so should not be included
|
||||
expect(result.guides).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should handle current version equal to feature guide version', () => {
|
||||
setMockGuides([createGuide({ version: '1.8.0', type: 'feature' })])
|
||||
|
||||
const result = selectApplicableGuides('1.8.0', '1.7.0', [], false)
|
||||
|
||||
// 1.8.0 <= 1.8.0, so should be included
|
||||
expect(result.guides).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
3
src/renderer/src/config/onboarding/guides/index.ts
Normal file
3
src/renderer/src/config/onboarding/guides/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { v170Onboarding } from './v1.7.0-onboarding'
|
||||
|
||||
export const allGuides = [v170Onboarding]
|
||||
@ -0,0 +1,79 @@
|
||||
import type { VersionGuide } from '@renderer/types/onboarding'
|
||||
|
||||
/**
|
||||
* Initial onboarding guide for new users
|
||||
* Covers: free built-in model, adding providers, adding models, use cases
|
||||
*/
|
||||
export const v170Onboarding: VersionGuide = {
|
||||
version: '1.7.0',
|
||||
type: 'onboarding',
|
||||
titleKey: 'onboarding.welcome.title',
|
||||
descriptionKey: 'onboarding.welcome.description',
|
||||
route: '/',
|
||||
priority: 100,
|
||||
steps: [
|
||||
{
|
||||
id: 'welcome',
|
||||
titleKey: 'onboarding.steps.welcome.title',
|
||||
descriptionKey: 'onboarding.steps.welcome.description',
|
||||
side: 'over'
|
||||
},
|
||||
{
|
||||
id: 'free-model',
|
||||
element: '.home-navbar',
|
||||
titleKey: 'onboarding.steps.freeModel.title',
|
||||
descriptionKey: 'onboarding.steps.freeModel.description',
|
||||
side: 'bottom',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
id: 'settings-intro',
|
||||
// TODO: improve selector to work in both sidebar and top nav layouts
|
||||
element: () => document.querySelector('#sidebar-settings') || document.querySelector('#navbar-settings'),
|
||||
titleKey: 'onboarding.steps.settingsIntro.title',
|
||||
descriptionKey: 'onboarding.steps.settingsIntro.description',
|
||||
side: 'bottom',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
id: 'add-provider',
|
||||
navigateTo: '/settings/provider',
|
||||
element: '#add-provider-btn',
|
||||
titleKey: 'onboarding.steps.addProvider.title',
|
||||
descriptionKey: 'onboarding.steps.addProvider.description',
|
||||
side: 'top',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
id: 'fill-api-key',
|
||||
element: '#api-key-input',
|
||||
titleKey: 'onboarding.steps.fillApiKey.title',
|
||||
descriptionKey: 'onboarding.steps.fillApiKey.description',
|
||||
side: 'bottom',
|
||||
align: 'start'
|
||||
},
|
||||
{
|
||||
id: 'add-model',
|
||||
element: '#add-model-btn',
|
||||
titleKey: 'onboarding.steps.addModel.title',
|
||||
descriptionKey: 'onboarding.steps.addModel.description',
|
||||
side: 'bottom',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
id: 'use-cases',
|
||||
navigateTo: '/',
|
||||
element: '#inputbar',
|
||||
titleKey: 'onboarding.steps.useCases.title',
|
||||
descriptionKey: 'onboarding.steps.useCases.description',
|
||||
side: 'top',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
id: 'complete',
|
||||
titleKey: 'onboarding.steps.complete.title',
|
||||
descriptionKey: 'onboarding.steps.complete.description',
|
||||
side: 'over'
|
||||
}
|
||||
]
|
||||
}
|
||||
68
src/renderer/src/config/onboarding/index.ts
Normal file
68
src/renderer/src/config/onboarding/index.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import type { VersionGuide } from '@renderer/types/onboarding'
|
||||
import { compare } from 'semver'
|
||||
|
||||
import { allGuides } from './guides'
|
||||
|
||||
export interface GuideSelectionResult {
|
||||
guides: VersionGuide[]
|
||||
isNewUser: boolean
|
||||
previousVersion: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Select applicable guides based on user's onboarding history
|
||||
*/
|
||||
export function selectApplicableGuides(
|
||||
currentVersion: string,
|
||||
completedOnboardingVersion: string | null,
|
||||
completedFeatureGuides: string[],
|
||||
onboardingSkipped: boolean
|
||||
): GuideSelectionResult {
|
||||
const isNewUser = completedOnboardingVersion === null && !onboardingSkipped
|
||||
|
||||
if (isNewUser) {
|
||||
// New user: Show latest onboarding guide only
|
||||
const onboardingGuides = allGuides
|
||||
.filter((g) => g.type === 'onboarding')
|
||||
.sort((a, b) => compare(b.version, a.version))
|
||||
|
||||
return {
|
||||
guides: onboardingGuides.slice(0, 1),
|
||||
isNewUser: true,
|
||||
previousVersion: null
|
||||
}
|
||||
}
|
||||
|
||||
if (onboardingSkipped) {
|
||||
// User has skipped onboarding, don't show any guides automatically
|
||||
return {
|
||||
guides: [],
|
||||
isNewUser: false,
|
||||
previousVersion: completedOnboardingVersion
|
||||
}
|
||||
}
|
||||
|
||||
// Upgrade user: Show feature guides for versions between last completed and current
|
||||
const pendingFeatureGuides = allGuides
|
||||
.filter((g) => {
|
||||
if (g.type !== 'feature') return false
|
||||
if (completedFeatureGuides.includes(g.version)) return false
|
||||
|
||||
// Guide version must be > completed version AND <= current version
|
||||
return compare(g.version, completedOnboardingVersion!) > 0 && compare(g.version, currentVersion) <= 0
|
||||
})
|
||||
.sort((a, b) => {
|
||||
// Sort by version (ascending) then priority (descending)
|
||||
const versionCompare = compare(a.version, b.version)
|
||||
if (versionCompare !== 0) return versionCompare
|
||||
return (b.priority ?? 0) - (a.priority ?? 0)
|
||||
})
|
||||
|
||||
return {
|
||||
guides: pendingFeatureGuides,
|
||||
isNewUser: false,
|
||||
previousVersion: completedOnboardingVersion
|
||||
}
|
||||
}
|
||||
|
||||
export { allGuides }
|
||||
132
src/renderer/src/hooks/useOnboardingInit.ts
Normal file
132
src/renderer/src/hooks/useOnboardingInit.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { useOnboarding } from '@renderer/components/Onboarding'
|
||||
import { selectApplicableGuides } from '@renderer/config/onboarding'
|
||||
import type { GuideStep, VersionGuide } from '@renderer/types/onboarding'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
const logger = loggerService.withContext('useOnboardingInit')
|
||||
|
||||
/**
|
||||
* Wait for an element to appear in the DOM using MutationObserver
|
||||
*/
|
||||
function waitForElement(selector: string, timeout = 3000): Promise<Element | null> {
|
||||
return new Promise((resolve) => {
|
||||
const el = document.querySelector(selector)
|
||||
if (el) return resolve(el)
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
const el = document.querySelector(selector)
|
||||
if (el) {
|
||||
observer.disconnect()
|
||||
resolve(el)
|
||||
}
|
||||
})
|
||||
|
||||
observer.observe(document.body, { childList: true, subtree: true })
|
||||
|
||||
setTimeout(() => {
|
||||
observer.disconnect()
|
||||
resolve(null)
|
||||
}, timeout)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first element selector from guide steps
|
||||
*/
|
||||
function getFirstElementSelector(guide: VersionGuide): string | null {
|
||||
for (const step of guide.steps) {
|
||||
if (typeof step.element === 'string') {
|
||||
return step.element
|
||||
}
|
||||
if (typeof step.element === 'function') {
|
||||
// For function selectors, use a known fallback
|
||||
return '.home-navbar'
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a step has an element selector (not a modal-style step)
|
||||
*/
|
||||
function stepHasElement(step: GuideStep): boolean {
|
||||
return step.element !== undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to initialize and trigger onboarding guides
|
||||
* Should be called in a component that renders after app is ready
|
||||
*/
|
||||
export function useOnboardingInit() {
|
||||
const location = useLocation()
|
||||
const { startGuide, isGuideActive, completedOnboardingVersion, completedFeatureGuides, onboardingSkipped } =
|
||||
useOnboarding()
|
||||
const [hasTriggered, setHasTriggered] = useState(false)
|
||||
const [appVersion, setAppVersion] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
window.api.getAppInfo().then((info) => {
|
||||
setAppVersion(info.version)
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Trigger onboarding after first target element is ready
|
||||
useEffect(() => {
|
||||
if (!appVersion || hasTriggered || isGuideActive) return
|
||||
|
||||
if (location.pathname !== '/') return
|
||||
|
||||
let cancelled = false
|
||||
|
||||
const triggerOnboarding = async () => {
|
||||
const result = selectApplicableGuides(
|
||||
appVersion,
|
||||
completedOnboardingVersion,
|
||||
completedFeatureGuides,
|
||||
onboardingSkipped
|
||||
)
|
||||
|
||||
if (result.guides.length === 0) return
|
||||
|
||||
const guide = result.guides[0]
|
||||
const firstSelector = getFirstElementSelector(guide)
|
||||
|
||||
// Wait for first target element if guide has element-based steps
|
||||
if (firstSelector && guide.steps.some(stepHasElement)) {
|
||||
const element = await waitForElement(firstSelector)
|
||||
if (cancelled) return
|
||||
if (!element) {
|
||||
logger.warn('First onboarding element not found', { selector: firstSelector })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (cancelled) return
|
||||
|
||||
logger.info('Starting onboarding guide', {
|
||||
version: guide.version,
|
||||
type: guide.type,
|
||||
isNewUser: result.isNewUser
|
||||
})
|
||||
startGuide(guide)
|
||||
setHasTriggered(true)
|
||||
}
|
||||
|
||||
triggerOnboarding()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [
|
||||
appVersion,
|
||||
hasTriggered,
|
||||
isGuideActive,
|
||||
location.pathname,
|
||||
completedOnboardingVersion,
|
||||
completedFeatureGuides,
|
||||
onboardingSkipped,
|
||||
startGuide
|
||||
])
|
||||
}
|
||||
@ -2359,6 +2359,51 @@
|
||||
},
|
||||
"title": "Ollama"
|
||||
},
|
||||
"onboarding": {
|
||||
"done": "Get Started",
|
||||
"next": "Next",
|
||||
"previous": "Previous",
|
||||
"progress": "{{current}} of {{total}}",
|
||||
"skip": "Skip Tour",
|
||||
"steps": {
|
||||
"addModel": {
|
||||
"description": "After adding a provider, click this button to add models. Different models excel at different tasks:\n• GPT-4 for complex reasoning\n• Claude for long-form content\n• GLM for Chinese conversations",
|
||||
"title": "Add Models"
|
||||
},
|
||||
"addProvider": {
|
||||
"description": "Click this button to add an AI provider. We recommend CherryIN for beginners - sign up to get free credits with access to GPT, Claude, and other popular models.",
|
||||
"title": "Add Provider"
|
||||
},
|
||||
"complete": {
|
||||
"description": "You're all set! Start chatting with AI now. Explore more features and let AI become your powerful assistant!",
|
||||
"title": "Start Exploring!"
|
||||
},
|
||||
"fillApiKey": {
|
||||
"description": "Enter your API Key here. You can get the API Key from the provider's website. After filling it in, click the \"Check\" button to verify the connection.",
|
||||
"title": "Fill in API Key"
|
||||
},
|
||||
"freeModel": {
|
||||
"description": "Cherry Studio includes a free built-in GLM 4.5 Flash model. Click the model selector in the top navbar to switch models. No configuration needed - start chatting right away!",
|
||||
"title": "Free Model"
|
||||
},
|
||||
"settingsIntro": {
|
||||
"description": "Click here to access settings. Next, we'll guide you through adding AI providers and models.",
|
||||
"title": "Settings"
|
||||
},
|
||||
"useCases": {
|
||||
"description": "Cherry Studio can help you with various tasks:\n• Intelligent Q&A\n• Document translation & summarization\n• Code writing & debugging\n• Creative writing\n• Knowledge base queries\n\nType your question in the input box to get started!",
|
||||
"title": "Use Cases"
|
||||
},
|
||||
"welcome": {
|
||||
"description": "Cherry Studio is a powerful AI desktop client supporting multiple AI models. Let's take a quick tour to get you started!",
|
||||
"title": "Welcome to Cherry Studio!"
|
||||
}
|
||||
},
|
||||
"welcome": {
|
||||
"description": "Your powerful AI assistant. Let's take a quick tour to help you get started.",
|
||||
"title": "Welcome to Cherry Studio"
|
||||
}
|
||||
},
|
||||
"ovms": {
|
||||
"action": {
|
||||
"install": "Install",
|
||||
|
||||
@ -2359,6 +2359,51 @@
|
||||
},
|
||||
"title": "Ollama"
|
||||
},
|
||||
"onboarding": {
|
||||
"done": "开始使用",
|
||||
"next": "下一步",
|
||||
"previous": "上一步",
|
||||
"progress": "{{current}} / {{total}}",
|
||||
"skip": "跳过引导",
|
||||
"steps": {
|
||||
"addModel": {
|
||||
"description": "添加服务商后,点击此按钮为该服务商添加模型。不同模型擅长不同任务:\n• GPT-4 适合复杂推理\n• Claude 适合长文本处理\n• GLM 适合中文对话",
|
||||
"title": "添加模型"
|
||||
},
|
||||
"addProvider": {
|
||||
"description": "点击此按钮添加 AI 服务商。推荐新手使用 CherryIN,注册即可获得免费额度,支持 GPT、Claude 等主流模型。",
|
||||
"title": "添加服务商"
|
||||
},
|
||||
"complete": {
|
||||
"description": "您已准备就绪!现在可以开始与 AI 对话了。探索更多功能,让 AI 成为您的得力助手!",
|
||||
"title": "开始探索!"
|
||||
},
|
||||
"fillApiKey": {
|
||||
"description": "在此处填入您的 API Key。您可以从服务商官网获取 API Key,填写后点击「检查」按钮验证连接。",
|
||||
"title": "填写 API Key"
|
||||
},
|
||||
"freeModel": {
|
||||
"description": "Cherry Studio 内置了免费的 GLM 4.5 Flash 模型,点击顶部导航栏的模型选择器即可切换使用。无需任何配置,立即开始对话!",
|
||||
"title": "免费模型"
|
||||
},
|
||||
"settingsIntro": {
|
||||
"description": "点击这里进入设置页面。接下来我们将引导您添加 AI 服务商和模型。",
|
||||
"title": "设置入口"
|
||||
},
|
||||
"useCases": {
|
||||
"description": "Cherry Studio 可以帮您完成多种任务:\n• 智能对话问答\n• 文档翻译与总结\n• 代码编写与调试\n• 创意写作\n• 知识库问答\n\n在输入框中输入您的问题,开始体验吧!",
|
||||
"title": "功能用例"
|
||||
},
|
||||
"welcome": {
|
||||
"description": "Cherry Studio 是一款强大的 AI 桌面客户端,支持多种 AI 模型。让我们快速了解如何开始使用!",
|
||||
"title": "欢迎使用 Cherry Studio!"
|
||||
}
|
||||
},
|
||||
"welcome": {
|
||||
"description": "您的强大 AI 助手。让我们快速了解一下如何开始使用。",
|
||||
"title": "欢迎使用 Cherry Studio"
|
||||
}
|
||||
},
|
||||
"ovms": {
|
||||
"action": {
|
||||
"install": "安装",
|
||||
|
||||
@ -2359,6 +2359,51 @@
|
||||
},
|
||||
"title": "Ollama"
|
||||
},
|
||||
"onboarding": {
|
||||
"done": "[to be translated]:Get Started",
|
||||
"next": "[to be translated]:Next",
|
||||
"previous": "[to be translated]:Previous",
|
||||
"progress": "[to be translated]:{{current}} of {{total}}",
|
||||
"skip": "[to be translated]:Skip Tour",
|
||||
"steps": {
|
||||
"addModel": {
|
||||
"description": "[to be translated]:Add the models you need from the provider page. Different models excel at different tasks like chatting, writing, coding, and more.",
|
||||
"title": "[to be translated]:Add Models"
|
||||
},
|
||||
"addProvider": {
|
||||
"description": "[to be translated]:Go to Settings to add AI providers. We recommend CherryIN for beginners - get free credits upon registration and access to various popular models.",
|
||||
"title": "[to be translated]:Add Provider"
|
||||
},
|
||||
"complete": {
|
||||
"description": "[to be translated]:You're all set! Start chatting with your first assistant. Need help? Check Settings for more options.",
|
||||
"title": "[to be translated]:You're All Set!"
|
||||
},
|
||||
"fillApiKey": {
|
||||
"description": "[to be translated]:Enter your API Key here. You can get the API Key from the provider's website. After filling it in, click the \"Check\" button to verify the connection.",
|
||||
"title": "[to be translated]:Fill in API Key"
|
||||
},
|
||||
"freeModel": {
|
||||
"description": "[to be translated]:Cherry Studio includes a free built-in GLM 4.5 Flash model. Start chatting right away without any configuration - perfect for a quick trial!",
|
||||
"title": "[to be translated]:Free Model"
|
||||
},
|
||||
"settingsIntro": {
|
||||
"description": "[to be translated]:Click here to access settings. Next, we'll guide you through adding AI providers and models.",
|
||||
"title": "[to be translated]:Settings"
|
||||
},
|
||||
"useCases": {
|
||||
"description": "[to be translated]:Cherry Studio can help you with: intelligent Q&A, document translation & summarization, code writing & debugging, creative writing, knowledge base queries, and more.",
|
||||
"title": "[to be translated]:Use Cases"
|
||||
},
|
||||
"welcome": {
|
||||
"description": "[to be translated]:Cherry Studio helps you interact with various AI models. Let's take a quick tour to help you get started.",
|
||||
"title": "[to be translated]:Welcome to Cherry Studio!"
|
||||
}
|
||||
},
|
||||
"welcome": {
|
||||
"description": "[to be translated]:Your powerful AI assistant. Let's take a quick tour to help you get started.",
|
||||
"title": "[to be translated]:Welcome to Cherry Studio"
|
||||
}
|
||||
},
|
||||
"ovms": {
|
||||
"action": {
|
||||
"install": "安裝",
|
||||
|
||||
@ -2359,6 +2359,51 @@
|
||||
},
|
||||
"title": "Ollama"
|
||||
},
|
||||
"onboarding": {
|
||||
"done": "[to be translated]:Get Started",
|
||||
"next": "[to be translated]:Next",
|
||||
"previous": "[to be translated]:Previous",
|
||||
"progress": "[to be translated]:{{current}} of {{total}}",
|
||||
"skip": "[to be translated]:Skip Tour",
|
||||
"steps": {
|
||||
"addModel": {
|
||||
"description": "[to be translated]:Add the models you need from the provider page. Different models excel at different tasks like chatting, writing, coding, and more.",
|
||||
"title": "[to be translated]:Add Models"
|
||||
},
|
||||
"addProvider": {
|
||||
"description": "[to be translated]:Go to Settings to add AI providers. We recommend CherryIN for beginners - get free credits upon registration and access to various popular models.",
|
||||
"title": "[to be translated]:Add Provider"
|
||||
},
|
||||
"complete": {
|
||||
"description": "[to be translated]:You're all set! Start chatting with your first assistant. Need help? Check Settings for more options.",
|
||||
"title": "[to be translated]:You're All Set!"
|
||||
},
|
||||
"fillApiKey": {
|
||||
"description": "[to be translated]:Enter your API Key here. You can get the API Key from the provider's website. After filling it in, click the \"Check\" button to verify the connection.",
|
||||
"title": "[to be translated]:Fill in API Key"
|
||||
},
|
||||
"freeModel": {
|
||||
"description": "[to be translated]:Cherry Studio includes a free built-in GLM 4.5 Flash model. Start chatting right away without any configuration - perfect for a quick trial!",
|
||||
"title": "[to be translated]:Free Model"
|
||||
},
|
||||
"settingsIntro": {
|
||||
"description": "[to be translated]:Click here to access settings. Next, we'll guide you through adding AI providers and models.",
|
||||
"title": "[to be translated]:Settings"
|
||||
},
|
||||
"useCases": {
|
||||
"description": "[to be translated]:Cherry Studio can help you with: intelligent Q&A, document translation & summarization, code writing & debugging, creative writing, knowledge base queries, and more.",
|
||||
"title": "[to be translated]:Use Cases"
|
||||
},
|
||||
"welcome": {
|
||||
"description": "[to be translated]:Cherry Studio helps you interact with various AI models. Let's take a quick tour to help you get started.",
|
||||
"title": "[to be translated]:Welcome to Cherry Studio!"
|
||||
}
|
||||
},
|
||||
"welcome": {
|
||||
"description": "[to be translated]:Your powerful AI assistant. Let's take a quick tour to help you get started.",
|
||||
"title": "[to be translated]:Welcome to Cherry Studio"
|
||||
}
|
||||
},
|
||||
"ovms": {
|
||||
"action": {
|
||||
"install": "Installieren",
|
||||
|
||||
@ -2359,6 +2359,51 @@
|
||||
},
|
||||
"title": "Ollama"
|
||||
},
|
||||
"onboarding": {
|
||||
"done": "[to be translated]:Get Started",
|
||||
"next": "[to be translated]:Next",
|
||||
"previous": "[to be translated]:Previous",
|
||||
"progress": "[to be translated]:{{current}} of {{total}}",
|
||||
"skip": "[to be translated]:Skip Tour",
|
||||
"steps": {
|
||||
"addModel": {
|
||||
"description": "[to be translated]:Add the models you need from the provider page. Different models excel at different tasks like chatting, writing, coding, and more.",
|
||||
"title": "[to be translated]:Add Models"
|
||||
},
|
||||
"addProvider": {
|
||||
"description": "[to be translated]:Go to Settings to add AI providers. We recommend CherryIN for beginners - get free credits upon registration and access to various popular models.",
|
||||
"title": "[to be translated]:Add Provider"
|
||||
},
|
||||
"complete": {
|
||||
"description": "[to be translated]:You're all set! Start chatting with your first assistant. Need help? Check Settings for more options.",
|
||||
"title": "[to be translated]:You're All Set!"
|
||||
},
|
||||
"fillApiKey": {
|
||||
"description": "[to be translated]:Enter your API Key here. You can get the API Key from the provider's website. After filling it in, click the \"Check\" button to verify the connection.",
|
||||
"title": "[to be translated]:Fill in API Key"
|
||||
},
|
||||
"freeModel": {
|
||||
"description": "[to be translated]:Cherry Studio includes a free built-in GLM 4.5 Flash model. Start chatting right away without any configuration - perfect for a quick trial!",
|
||||
"title": "[to be translated]:Free Model"
|
||||
},
|
||||
"settingsIntro": {
|
||||
"description": "[to be translated]:Click here to access settings. Next, we'll guide you through adding AI providers and models.",
|
||||
"title": "[to be translated]:Settings"
|
||||
},
|
||||
"useCases": {
|
||||
"description": "[to be translated]:Cherry Studio can help you with: intelligent Q&A, document translation & summarization, code writing & debugging, creative writing, knowledge base queries, and more.",
|
||||
"title": "[to be translated]:Use Cases"
|
||||
},
|
||||
"welcome": {
|
||||
"description": "[to be translated]:Cherry Studio helps you interact with various AI models. Let's take a quick tour to help you get started.",
|
||||
"title": "[to be translated]:Welcome to Cherry Studio!"
|
||||
}
|
||||
},
|
||||
"welcome": {
|
||||
"description": "[to be translated]:Your powerful AI assistant. Let's take a quick tour to help you get started.",
|
||||
"title": "[to be translated]:Welcome to Cherry Studio"
|
||||
}
|
||||
},
|
||||
"ovms": {
|
||||
"action": {
|
||||
"install": "Εγκατάσταση",
|
||||
|
||||
@ -2359,6 +2359,51 @@
|
||||
},
|
||||
"title": "Ollama"
|
||||
},
|
||||
"onboarding": {
|
||||
"done": "[to be translated]:Get Started",
|
||||
"next": "[to be translated]:Next",
|
||||
"previous": "[to be translated]:Previous",
|
||||
"progress": "[to be translated]:{{current}} of {{total}}",
|
||||
"skip": "[to be translated]:Skip Tour",
|
||||
"steps": {
|
||||
"addModel": {
|
||||
"description": "[to be translated]:Add the models you need from the provider page. Different models excel at different tasks like chatting, writing, coding, and more.",
|
||||
"title": "[to be translated]:Add Models"
|
||||
},
|
||||
"addProvider": {
|
||||
"description": "[to be translated]:Go to Settings to add AI providers. We recommend CherryIN for beginners - get free credits upon registration and access to various popular models.",
|
||||
"title": "[to be translated]:Add Provider"
|
||||
},
|
||||
"complete": {
|
||||
"description": "[to be translated]:You're all set! Start chatting with your first assistant. Need help? Check Settings for more options.",
|
||||
"title": "[to be translated]:You're All Set!"
|
||||
},
|
||||
"fillApiKey": {
|
||||
"description": "[to be translated]:Enter your API Key here. You can get the API Key from the provider's website. After filling it in, click the \"Check\" button to verify the connection.",
|
||||
"title": "[to be translated]:Fill in API Key"
|
||||
},
|
||||
"freeModel": {
|
||||
"description": "[to be translated]:Cherry Studio includes a free built-in GLM 4.5 Flash model. Start chatting right away without any configuration - perfect for a quick trial!",
|
||||
"title": "[to be translated]:Free Model"
|
||||
},
|
||||
"settingsIntro": {
|
||||
"description": "[to be translated]:Click here to access settings. Next, we'll guide you through adding AI providers and models.",
|
||||
"title": "[to be translated]:Settings"
|
||||
},
|
||||
"useCases": {
|
||||
"description": "[to be translated]:Cherry Studio can help you with: intelligent Q&A, document translation & summarization, code writing & debugging, creative writing, knowledge base queries, and more.",
|
||||
"title": "[to be translated]:Use Cases"
|
||||
},
|
||||
"welcome": {
|
||||
"description": "[to be translated]:Cherry Studio helps you interact with various AI models. Let's take a quick tour to help you get started.",
|
||||
"title": "[to be translated]:Welcome to Cherry Studio!"
|
||||
}
|
||||
},
|
||||
"welcome": {
|
||||
"description": "[to be translated]:Your powerful AI assistant. Let's take a quick tour to help you get started.",
|
||||
"title": "[to be translated]:Welcome to Cherry Studio"
|
||||
}
|
||||
},
|
||||
"ovms": {
|
||||
"action": {
|
||||
"install": "Instalar",
|
||||
|
||||
@ -2359,6 +2359,51 @@
|
||||
},
|
||||
"title": "Ollama"
|
||||
},
|
||||
"onboarding": {
|
||||
"done": "[to be translated]:Get Started",
|
||||
"next": "[to be translated]:Next",
|
||||
"previous": "[to be translated]:Previous",
|
||||
"progress": "[to be translated]:{{current}} of {{total}}",
|
||||
"skip": "[to be translated]:Skip Tour",
|
||||
"steps": {
|
||||
"addModel": {
|
||||
"description": "[to be translated]:Add the models you need from the provider page. Different models excel at different tasks like chatting, writing, coding, and more.",
|
||||
"title": "[to be translated]:Add Models"
|
||||
},
|
||||
"addProvider": {
|
||||
"description": "[to be translated]:Go to Settings to add AI providers. We recommend CherryIN for beginners - get free credits upon registration and access to various popular models.",
|
||||
"title": "[to be translated]:Add Provider"
|
||||
},
|
||||
"complete": {
|
||||
"description": "[to be translated]:You're all set! Start chatting with your first assistant. Need help? Check Settings for more options.",
|
||||
"title": "[to be translated]:You're All Set!"
|
||||
},
|
||||
"fillApiKey": {
|
||||
"description": "[to be translated]:Enter your API Key here. You can get the API Key from the provider's website. After filling it in, click the \"Check\" button to verify the connection.",
|
||||
"title": "[to be translated]:Fill in API Key"
|
||||
},
|
||||
"freeModel": {
|
||||
"description": "[to be translated]:Cherry Studio includes a free built-in GLM 4.5 Flash model. Start chatting right away without any configuration - perfect for a quick trial!",
|
||||
"title": "[to be translated]:Free Model"
|
||||
},
|
||||
"settingsIntro": {
|
||||
"description": "[to be translated]:Click here to access settings. Next, we'll guide you through adding AI providers and models.",
|
||||
"title": "[to be translated]:Settings"
|
||||
},
|
||||
"useCases": {
|
||||
"description": "[to be translated]:Cherry Studio can help you with: intelligent Q&A, document translation & summarization, code writing & debugging, creative writing, knowledge base queries, and more.",
|
||||
"title": "[to be translated]:Use Cases"
|
||||
},
|
||||
"welcome": {
|
||||
"description": "[to be translated]:Cherry Studio helps you interact with various AI models. Let's take a quick tour to help you get started.",
|
||||
"title": "[to be translated]:Welcome to Cherry Studio!"
|
||||
}
|
||||
},
|
||||
"welcome": {
|
||||
"description": "[to be translated]:Your powerful AI assistant. Let's take a quick tour to help you get started.",
|
||||
"title": "[to be translated]:Welcome to Cherry Studio"
|
||||
}
|
||||
},
|
||||
"ovms": {
|
||||
"action": {
|
||||
"install": "Installer",
|
||||
|
||||
@ -2359,6 +2359,51 @@
|
||||
},
|
||||
"title": "Ollama"
|
||||
},
|
||||
"onboarding": {
|
||||
"done": "[to be translated]:Get Started",
|
||||
"next": "[to be translated]:Next",
|
||||
"previous": "[to be translated]:Previous",
|
||||
"progress": "[to be translated]:{{current}} of {{total}}",
|
||||
"skip": "[to be translated]:Skip Tour",
|
||||
"steps": {
|
||||
"addModel": {
|
||||
"description": "[to be translated]:Add the models you need from the provider page. Different models excel at different tasks like chatting, writing, coding, and more.",
|
||||
"title": "[to be translated]:Add Models"
|
||||
},
|
||||
"addProvider": {
|
||||
"description": "[to be translated]:Go to Settings to add AI providers. We recommend CherryIN for beginners - get free credits upon registration and access to various popular models.",
|
||||
"title": "[to be translated]:Add Provider"
|
||||
},
|
||||
"complete": {
|
||||
"description": "[to be translated]:You're all set! Start chatting with your first assistant. Need help? Check Settings for more options.",
|
||||
"title": "[to be translated]:You're All Set!"
|
||||
},
|
||||
"fillApiKey": {
|
||||
"description": "[to be translated]:Enter your API Key here. You can get the API Key from the provider's website. After filling it in, click the \"Check\" button to verify the connection.",
|
||||
"title": "[to be translated]:Fill in API Key"
|
||||
},
|
||||
"freeModel": {
|
||||
"description": "[to be translated]:Cherry Studio includes a free built-in GLM 4.5 Flash model. Start chatting right away without any configuration - perfect for a quick trial!",
|
||||
"title": "[to be translated]:Free Model"
|
||||
},
|
||||
"settingsIntro": {
|
||||
"description": "[to be translated]:Click here to access settings. Next, we'll guide you through adding AI providers and models.",
|
||||
"title": "[to be translated]:Settings"
|
||||
},
|
||||
"useCases": {
|
||||
"description": "[to be translated]:Cherry Studio can help you with: intelligent Q&A, document translation & summarization, code writing & debugging, creative writing, knowledge base queries, and more.",
|
||||
"title": "[to be translated]:Use Cases"
|
||||
},
|
||||
"welcome": {
|
||||
"description": "[to be translated]:Cherry Studio helps you interact with various AI models. Let's take a quick tour to help you get started.",
|
||||
"title": "[to be translated]:Welcome to Cherry Studio!"
|
||||
}
|
||||
},
|
||||
"welcome": {
|
||||
"description": "[to be translated]:Your powerful AI assistant. Let's take a quick tour to help you get started.",
|
||||
"title": "[to be translated]:Welcome to Cherry Studio"
|
||||
}
|
||||
},
|
||||
"ovms": {
|
||||
"action": {
|
||||
"install": "インストール",
|
||||
|
||||
@ -2359,6 +2359,51 @@
|
||||
},
|
||||
"title": "Ollama"
|
||||
},
|
||||
"onboarding": {
|
||||
"done": "[to be translated]:Get Started",
|
||||
"next": "[to be translated]:Next",
|
||||
"previous": "[to be translated]:Previous",
|
||||
"progress": "[to be translated]:{{current}} of {{total}}",
|
||||
"skip": "[to be translated]:Skip Tour",
|
||||
"steps": {
|
||||
"addModel": {
|
||||
"description": "[to be translated]:Add the models you need from the provider page. Different models excel at different tasks like chatting, writing, coding, and more.",
|
||||
"title": "[to be translated]:Add Models"
|
||||
},
|
||||
"addProvider": {
|
||||
"description": "[to be translated]:Go to Settings to add AI providers. We recommend CherryIN for beginners - get free credits upon registration and access to various popular models.",
|
||||
"title": "[to be translated]:Add Provider"
|
||||
},
|
||||
"complete": {
|
||||
"description": "[to be translated]:You're all set! Start chatting with your first assistant. Need help? Check Settings for more options.",
|
||||
"title": "[to be translated]:You're All Set!"
|
||||
},
|
||||
"fillApiKey": {
|
||||
"description": "[to be translated]:Enter your API Key here. You can get the API Key from the provider's website. After filling it in, click the \"Check\" button to verify the connection.",
|
||||
"title": "[to be translated]:Fill in API Key"
|
||||
},
|
||||
"freeModel": {
|
||||
"description": "[to be translated]:Cherry Studio includes a free built-in GLM 4.5 Flash model. Start chatting right away without any configuration - perfect for a quick trial!",
|
||||
"title": "[to be translated]:Free Model"
|
||||
},
|
||||
"settingsIntro": {
|
||||
"description": "[to be translated]:Click here to access settings. Next, we'll guide you through adding AI providers and models.",
|
||||
"title": "[to be translated]:Settings"
|
||||
},
|
||||
"useCases": {
|
||||
"description": "[to be translated]:Cherry Studio can help you with: intelligent Q&A, document translation & summarization, code writing & debugging, creative writing, knowledge base queries, and more.",
|
||||
"title": "[to be translated]:Use Cases"
|
||||
},
|
||||
"welcome": {
|
||||
"description": "[to be translated]:Cherry Studio helps you interact with various AI models. Let's take a quick tour to help you get started.",
|
||||
"title": "[to be translated]:Welcome to Cherry Studio!"
|
||||
}
|
||||
},
|
||||
"welcome": {
|
||||
"description": "[to be translated]:Your powerful AI assistant. Let's take a quick tour to help you get started.",
|
||||
"title": "[to be translated]:Welcome to Cherry Studio"
|
||||
}
|
||||
},
|
||||
"ovms": {
|
||||
"action": {
|
||||
"install": "Instalar",
|
||||
|
||||
@ -2359,6 +2359,51 @@
|
||||
},
|
||||
"title": "Ollama"
|
||||
},
|
||||
"onboarding": {
|
||||
"done": "[to be translated]:Get Started",
|
||||
"next": "[to be translated]:Next",
|
||||
"previous": "[to be translated]:Previous",
|
||||
"progress": "[to be translated]:{{current}} of {{total}}",
|
||||
"skip": "[to be translated]:Skip Tour",
|
||||
"steps": {
|
||||
"addModel": {
|
||||
"description": "[to be translated]:Add the models you need from the provider page. Different models excel at different tasks like chatting, writing, coding, and more.",
|
||||
"title": "[to be translated]:Add Models"
|
||||
},
|
||||
"addProvider": {
|
||||
"description": "[to be translated]:Go to Settings to add AI providers. We recommend CherryIN for beginners - get free credits upon registration and access to various popular models.",
|
||||
"title": "[to be translated]:Add Provider"
|
||||
},
|
||||
"complete": {
|
||||
"description": "[to be translated]:You're all set! Start chatting with your first assistant. Need help? Check Settings for more options.",
|
||||
"title": "[to be translated]:You're All Set!"
|
||||
},
|
||||
"fillApiKey": {
|
||||
"description": "[to be translated]:Enter your API Key here. You can get the API Key from the provider's website. After filling it in, click the \"Check\" button to verify the connection.",
|
||||
"title": "[to be translated]:Fill in API Key"
|
||||
},
|
||||
"freeModel": {
|
||||
"description": "[to be translated]:Cherry Studio includes a free built-in GLM 4.5 Flash model. Start chatting right away without any configuration - perfect for a quick trial!",
|
||||
"title": "[to be translated]:Free Model"
|
||||
},
|
||||
"settingsIntro": {
|
||||
"description": "[to be translated]:Click here to access settings. Next, we'll guide you through adding AI providers and models.",
|
||||
"title": "[to be translated]:Settings"
|
||||
},
|
||||
"useCases": {
|
||||
"description": "[to be translated]:Cherry Studio can help you with: intelligent Q&A, document translation & summarization, code writing & debugging, creative writing, knowledge base queries, and more.",
|
||||
"title": "[to be translated]:Use Cases"
|
||||
},
|
||||
"welcome": {
|
||||
"description": "[to be translated]:Cherry Studio helps you interact with various AI models. Let's take a quick tour to help you get started.",
|
||||
"title": "[to be translated]:Welcome to Cherry Studio!"
|
||||
}
|
||||
},
|
||||
"welcome": {
|
||||
"description": "[to be translated]:Your powerful AI assistant. Let's take a quick tour to help you get started.",
|
||||
"title": "[to be translated]:Welcome to Cherry Studio"
|
||||
}
|
||||
},
|
||||
"ovms": {
|
||||
"action": {
|
||||
"install": "Instalează",
|
||||
|
||||
@ -2359,6 +2359,51 @@
|
||||
},
|
||||
"title": "Ollama"
|
||||
},
|
||||
"onboarding": {
|
||||
"done": "[to be translated]:Get Started",
|
||||
"next": "[to be translated]:Next",
|
||||
"previous": "[to be translated]:Previous",
|
||||
"progress": "[to be translated]:{{current}} of {{total}}",
|
||||
"skip": "[to be translated]:Skip Tour",
|
||||
"steps": {
|
||||
"addModel": {
|
||||
"description": "[to be translated]:Add the models you need from the provider page. Different models excel at different tasks like chatting, writing, coding, and more.",
|
||||
"title": "[to be translated]:Add Models"
|
||||
},
|
||||
"addProvider": {
|
||||
"description": "[to be translated]:Go to Settings to add AI providers. We recommend CherryIN for beginners - get free credits upon registration and access to various popular models.",
|
||||
"title": "[to be translated]:Add Provider"
|
||||
},
|
||||
"complete": {
|
||||
"description": "[to be translated]:You're all set! Start chatting with your first assistant. Need help? Check Settings for more options.",
|
||||
"title": "[to be translated]:You're All Set!"
|
||||
},
|
||||
"fillApiKey": {
|
||||
"description": "[to be translated]:Enter your API Key here. You can get the API Key from the provider's website. After filling it in, click the \"Check\" button to verify the connection.",
|
||||
"title": "[to be translated]:Fill in API Key"
|
||||
},
|
||||
"freeModel": {
|
||||
"description": "[to be translated]:Cherry Studio includes a free built-in GLM 4.5 Flash model. Start chatting right away without any configuration - perfect for a quick trial!",
|
||||
"title": "[to be translated]:Free Model"
|
||||
},
|
||||
"settingsIntro": {
|
||||
"description": "[to be translated]:Click here to access settings. Next, we'll guide you through adding AI providers and models.",
|
||||
"title": "[to be translated]:Settings"
|
||||
},
|
||||
"useCases": {
|
||||
"description": "[to be translated]:Cherry Studio can help you with: intelligent Q&A, document translation & summarization, code writing & debugging, creative writing, knowledge base queries, and more.",
|
||||
"title": "[to be translated]:Use Cases"
|
||||
},
|
||||
"welcome": {
|
||||
"description": "[to be translated]:Cherry Studio helps you interact with various AI models. Let's take a quick tour to help you get started.",
|
||||
"title": "[to be translated]:Welcome to Cherry Studio!"
|
||||
}
|
||||
},
|
||||
"welcome": {
|
||||
"description": "[to be translated]:Your powerful AI assistant. Let's take a quick tour to help you get started.",
|
||||
"title": "[to be translated]:Welcome to Cherry Studio"
|
||||
}
|
||||
},
|
||||
"ovms": {
|
||||
"action": {
|
||||
"install": "Установить",
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { ErrorBoundary } from '@renderer/components/ErrorBoundary'
|
||||
import { useAgentSessionInitializer } from '@renderer/hooks/agents/useAgentSessionInitializer'
|
||||
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { useOnboardingInit } from '@renderer/hooks/useOnboardingInit'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useActiveTopic } from '@renderer/hooks/useTopic'
|
||||
@ -30,6 +31,9 @@ const HomePage: FC = () => {
|
||||
// Initialize agent session hook
|
||||
useAgentSessionInitializer()
|
||||
|
||||
// Initialize onboarding for new users
|
||||
useOnboardingInit()
|
||||
|
||||
const location = useLocation()
|
||||
const state = location.state
|
||||
|
||||
|
||||
@ -183,7 +183,12 @@ const ModelList: React.FC<ModelListProps> = ({ providerId }) => {
|
||||
{t('button.manage')}
|
||||
</Button>
|
||||
{provider.id !== 'ovms' ? (
|
||||
<Button type="default" onClick={onAddModel} icon={<Plus size={16} />} disabled={isHealthChecking}>
|
||||
<Button
|
||||
id="add-model-btn"
|
||||
type="default"
|
||||
onClick={onAddModel}
|
||||
icon={<Plus size={16} />}
|
||||
disabled={isHealthChecking}>
|
||||
{t('button.add')}
|
||||
</Button>
|
||||
) : (
|
||||
|
||||
@ -385,6 +385,7 @@ const ProviderList: FC = () => {
|
||||
</DraggableVirtualList>
|
||||
<AddButtonWrapper>
|
||||
<Button
|
||||
id="add-provider-btn"
|
||||
style={{ width: '100%', borderRadius: 'var(--list-item-border-radius)' }}
|
||||
icon={<PlusIcon size={16} />}
|
||||
onClick={onAddProvider}
|
||||
|
||||
@ -463,6 +463,7 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
</SettingSubtitle>
|
||||
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
|
||||
<Input.Password
|
||||
id="api-key-input"
|
||||
value={localApiKey}
|
||||
placeholder={t('settings.provider.api_key.label')}
|
||||
onChange={(e) => setLocalApiKey(e.target.value)}
|
||||
|
||||
@ -38,6 +38,7 @@ import { setNotesPath } from './note'
|
||||
import note from './note'
|
||||
import nutstore from './nutstore'
|
||||
import ocr from './ocr'
|
||||
import onboarding from './onboarding'
|
||||
import paintings from './paintings'
|
||||
import preprocess from './preprocess'
|
||||
import runtime from './runtime'
|
||||
@ -76,6 +77,7 @@ const rootReducer = combineReducers({
|
||||
translate,
|
||||
ocr,
|
||||
note,
|
||||
onboarding,
|
||||
toolPermissions
|
||||
})
|
||||
|
||||
|
||||
34
src/renderer/src/store/onboarding.ts
Normal file
34
src/renderer/src/store/onboarding.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import type { PayloadAction } from '@reduxjs/toolkit'
|
||||
import { createSlice } from '@reduxjs/toolkit'
|
||||
import type { OnboardingState } from '@renderer/types/onboarding'
|
||||
|
||||
const initialState: OnboardingState = {
|
||||
completedOnboardingVersion: null,
|
||||
completedFeatureGuides: [],
|
||||
onboardingSkipped: false
|
||||
}
|
||||
|
||||
const onboardingSlice = createSlice({
|
||||
name: 'onboarding',
|
||||
initialState,
|
||||
reducers: {
|
||||
completeOnboarding: (state, action: PayloadAction<string>) => {
|
||||
state.completedOnboardingVersion = action.payload
|
||||
state.onboardingSkipped = false
|
||||
},
|
||||
completeFeatureGuide: (state, action: PayloadAction<string>) => {
|
||||
if (!state.completedFeatureGuides.includes(action.payload)) {
|
||||
state.completedFeatureGuides.push(action.payload)
|
||||
}
|
||||
},
|
||||
skipOnboarding: (state, action: PayloadAction<string>) => {
|
||||
state.onboardingSkipped = true
|
||||
state.completedOnboardingVersion = action.payload
|
||||
},
|
||||
resetOnboarding: () => initialState
|
||||
}
|
||||
})
|
||||
|
||||
export const { completeOnboarding, completeFeatureGuide, skipOnboarding, resetOnboarding } = onboardingSlice.actions
|
||||
|
||||
export default onboardingSlice.reducer
|
||||
49
src/renderer/src/types/onboarding.ts
Normal file
49
src/renderer/src/types/onboarding.ts
Normal file
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Onboarding types for user guide system
|
||||
*/
|
||||
|
||||
/** Single step in a guide tour */
|
||||
export interface GuideStep {
|
||||
/** Unique step ID for tracking */
|
||||
id: string
|
||||
/** CSS selector for target element (optional for modal-style steps) */
|
||||
element?: string | (() => Element | null)
|
||||
/** i18n key for title */
|
||||
titleKey: string
|
||||
/** i18n key for description */
|
||||
descriptionKey: string
|
||||
/** Popover position relative to element */
|
||||
side?: 'top' | 'right' | 'bottom' | 'left' | 'over'
|
||||
/** Popover alignment */
|
||||
align?: 'start' | 'center' | 'end'
|
||||
/** Route to navigate to before showing this step */
|
||||
navigateTo?: string
|
||||
}
|
||||
|
||||
/** Version-specific guide definition */
|
||||
export interface VersionGuide {
|
||||
/** Target version (semver string, e.g., "1.7.0", "1.8.0") */
|
||||
version: string
|
||||
/** Guide type: 'onboarding' for new users, 'feature' for upgrades */
|
||||
type: 'onboarding' | 'feature'
|
||||
/** i18n key for guide title */
|
||||
titleKey: string
|
||||
/** i18n key for guide description */
|
||||
descriptionKey: string
|
||||
/** Ordered list of steps */
|
||||
steps: GuideStep[]
|
||||
/** Route where this guide should be triggered (default: '/') */
|
||||
route?: string
|
||||
/** Priority for ordering when multiple guides apply (higher = first) */
|
||||
priority?: number
|
||||
}
|
||||
|
||||
/** Redux state for onboarding */
|
||||
export interface OnboardingState {
|
||||
/** Last completed onboarding version (e.g., "1.7.0") */
|
||||
completedOnboardingVersion: string | null
|
||||
/** Array of completed feature guide versions */
|
||||
completedFeatureGuides: string[]
|
||||
/** Whether user has explicitly skipped onboarding */
|
||||
onboardingSkipped: boolean
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user