feat: user guide fully

This commit is contained in:
suyao 2026-01-26 09:57:05 +08:00
parent 77e50f889a
commit 8210f28bfe
No known key found for this signature in database
62 changed files with 3771 additions and 114 deletions

View File

@ -3,11 +3,13 @@ import '@renderer/databases'
import type { FC } from 'react'
import { useMemo } from 'react'
import { HashRouter, Route, Routes } from 'react-router-dom'
import styled from 'styled-components'
import Sidebar from './components/app/Sidebar'
import { ErrorBoundary } from './components/ErrorBoundary'
import { OnboardingProvider } from './components/Onboarding'
import TabsContainer from './components/Tab/TabContainer'
import { CompletionModal, GuidePage, useTaskCompletion, useUserGuide } from './components/UserGuide'
import NavigationHandler from './handler/NavigationHandler'
import { useNavbarPosition } from './hooks/useSettings'
import CodeToolsPage from './pages/code/CodeToolsPage'
@ -23,7 +25,37 @@ import SettingsPage from './pages/settings/SettingsPage'
import AssistantPresetsPage from './pages/store/assistants/presets/AssistantPresetsPage'
import TranslatePage from './pages/translate/TranslatePage'
const Router: FC = () => {
const UserGuideComponents: FC = () => {
// Enable task completion detection
useTaskCompletion()
return <CompletionModal />
}
/**
* Main container that sets navbar-position attribute for child CSS selectors
*/
const AppContainer = styled.div`
display: flex;
flex-direction: row;
height: 100vh;
width: 100vw;
`
/**
* Main content area for left navbar layout
*/
const MainContent = styled.div`
display: flex;
flex: 1;
flex-direction: column;
overflow: hidden;
`
/**
* Main app layout that renders after GuidePage is completed
*/
const MainAppLayout: FC = () => {
const { navbarPosition } = useNavbarPosition()
const routes = useMemo(() => {
@ -49,21 +81,48 @@ const Router: FC = () => {
if (navbarPosition === 'left') {
return (
<HashRouter>
<OnboardingProvider>
<Sidebar />
{routes}
<NavigationHandler />
</OnboardingProvider>
</HashRouter>
<AppContainer navbar-position="left">
<Sidebar />
<MainContent>{routes}</MainContent>
<NavigationHandler />
<UserGuideComponents />
</AppContainer>
)
}
return (
<AppContainer navbar-position="top">
<NavigationHandler />
<TabsContainer>{routes}</TabsContainer>
<UserGuideComponents />
</AppContainer>
)
}
/**
* Content switcher that shows either GuidePage or MainAppLayout
* based on user guide completion status
*/
const AppContent: FC = () => {
const { shouldShowGuidePage } = useUserGuide()
if (shouldShowGuidePage) {
return <GuidePage />
}
return <MainAppLayout />
}
/**
* Router component that conditionally renders GuidePage as a full-screen overlay
* before showing the main app layout. This ensures users complete the guide page
* before seeing any navigation elements.
*/
const Router: FC = () => {
return (
<HashRouter>
<OnboardingProvider>
<NavigationHandler />
<TabsContainer>{routes}</TabsContainer>
<AppContent />
</OnboardingProvider>
</HashRouter>
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 697 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 797 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 915 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -3,9 +3,11 @@ import './styles/driver-theme.css'
import { loggerService } from '@logger'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useProvider } from '@renderer/hooks/useProvider'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { completeFeatureGuide, completeOnboarding, skipOnboarding } from '@renderer/store/onboarding'
import type { GuideStep, VersionGuide } from '@renderer/types/onboarding'
import { completeFeatureGuide, completeOnboarding, completeTask, skipOnboarding } from '@renderer/store/onboarding'
import type { GuideStep, UserGuideTaskStatus, 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'
@ -14,6 +16,13 @@ import { useLocation, useNavigate } from 'react-router-dom'
const logger = loggerService.withContext('Onboarding')
// Mapping from user guide version to task key
const USER_GUIDE_TASK_MAP: Record<string, keyof UserGuideTaskStatus> = {
'user-guide-use-free-model': 'useFreeModel',
'user-guide-configure-provider': 'configureProvider',
'user-guide-send-message': 'sendFirstMessage'
}
interface OnboardingContextType {
startGuide: (guide: VersionGuide) => void
skipGuide: () => void
@ -72,6 +81,49 @@ export const OnboardingProvider: FC<PropsWithChildren> = ({ children }) => {
(state) => state.onboarding
)
// Process guide-video elements: show skeleton while loading, fade in when ready
const injectVideos = useCallback(() => {
const videoContainers = document.querySelectorAll('.guide-video[data-video-light]')
videoContainers.forEach((container) => {
// Skip if already processed
if (container.getAttribute('data-processed')) return
container.setAttribute('data-processed', 'true')
const lightUrl = container.getAttribute('data-video-light')
const darkUrl = container.getAttribute('data-video-dark')
const videoUrl = theme === 'dark' && darkUrl ? darkUrl : lightUrl
if (!videoUrl) return
// Create skeleton placeholder
const skeleton = document.createElement('div')
skeleton.className = 'guide-video-skeleton'
skeleton.innerHTML = `
<div class="ant-skeleton ant-skeleton-active ant-skeleton-element">
<span class="ant-skeleton-image"></span>
</div>
`
container.appendChild(skeleton)
// Create and load video
const video = document.createElement('video')
video.src = videoUrl
video.autoplay = true
video.loop = true
video.muted = true
video.playsInline = true
video.style.opacity = '0'
video.style.transition = 'opacity 0.3s ease'
container.appendChild(video)
// Show video and hide skeleton when loaded
video.onloadeddata = () => {
video.style.opacity = '1'
skeleton.style.display = 'none'
}
})
}, [theme])
const finishGuide = useCallback(
(wasCompleted: boolean) => {
const guide = guideRef.current.guide
@ -86,6 +138,14 @@ export const OnboardingProvider: FC<PropsWithChildren> = ({ children }) => {
dispatch(completeFeatureGuide(guide.version))
}
if (wasCompleted) {
const taskKey = USER_GUIDE_TASK_MAP[guide.version]
if (taskKey && taskKey !== 'configureProvider' && taskKey !== 'sendFirstMessage') {
dispatch(completeTask(taskKey))
logger.info(`Task completed via guide`, { taskKey, guideVersion: guide.version })
}
}
setIsGuideActive(false)
setCurrentGuide(null)
guideRef.current = { guide: null, steps: [] }
@ -95,28 +155,40 @@ export const OnboardingProvider: FC<PropsWithChildren> = ({ children }) => {
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
guideSteps.map((step, index) => {
const isLastStep = index === guideSteps.length - 1
return {
element: resolveElement(step),
popover: {
title: t(step.titleKey),
description: t(step.descriptionKey, step.descriptionInterpolation),
side: step.side === 'over' ? undefined : step.side,
align: step.align,
// Custom button text per step
nextBtnText: step.nextBtnTextKey ? t(step.nextBtnTextKey) : undefined,
doneBtnText: isLastStep && step.doneBtnTextKey ? t(step.doneBtnTextKey) : undefined
}
}
})),
}),
[t]
)
const createAndStartDriver = useCallback(
(fromStepIndex: number) => {
const { steps: guideSteps } = guideRef.current
const { guide, steps: guideSteps } = guideRef.current
if (!guideSteps.length) return
const steps = createDriverSteps(guideSteps)
const popoverClass = guide?.popoverClass || 'cherry-driver-popover'
const isUserGuide = popoverClass.includes('user-guide-popover')
// For user guide popovers, use custom button text from the last step
const lastStep = guideSteps[guideSteps.length - 1]
const doneBtnText = lastStep?.doneBtnTextKey ? t(lastStep.doneBtnTextKey) : t('onboarding.done')
const driverInstance = driver({
animate: true,
showProgress: true,
showProgress: !isUserGuide,
overlayColor: theme === 'dark' ? 'rgba(0, 0, 0, 0.75)' : 'rgba(255, 255, 255, 0.75)',
stagePadding: 10,
stageRadius: 8,
@ -125,9 +197,13 @@ export const OnboardingProvider: FC<PropsWithChildren> = ({ children }) => {
progressText: t('onboarding.progress'),
nextBtnText: t('onboarding.next'),
prevBtnText: t('onboarding.previous'),
doneBtnText: t('onboarding.done'),
popoverClass: `cherry-driver-popover ${theme}`,
doneBtnText,
popoverClass: `${popoverClass} ${theme}`,
steps,
onPopoverRender: () => {
// Inject videos after popover is rendered
injectVideos()
},
onHighlightStarted: () => {
// Skip if we just navigated (waiting for re-drive)
if (navigatingRef.current) return
@ -160,7 +236,7 @@ export const OnboardingProvider: FC<PropsWithChildren> = ({ children }) => {
driverRef.current = driverInstance
driverInstance.drive(fromStepIndex)
},
[theme, t, navigate, createDriverSteps, finishGuide]
[theme, t, navigate, createDriverSteps, finishGuide, injectVideos]
)
const startGuide = useCallback(
@ -187,6 +263,33 @@ export const OnboardingProvider: FC<PropsWithChildren> = ({ children }) => {
}
}, [])
const { models: cherryinModels } = useProvider('cherryin')
const { taskStatus } = useAppSelector((state) => state.onboarding)
useEffect(() => {
if (!taskStatus.configureProvider && cherryinModels.length > 0) {
dispatch(completeTask('configureProvider'))
logger.info('Task completed via CherryIN models', {
taskKey: 'configureProvider',
modelCount: cherryinModels.length
})
}
}, [cherryinModels.length, taskStatus.configureProvider, dispatch])
useEffect(() => {
if (taskStatus.sendFirstMessage) return
const handleMessageSent = () => {
dispatch(completeTask('sendFirstMessage'))
logger.info('Task completed via message sent', { taskKey: 'sendFirstMessage' })
}
EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, handleMessageSent)
return () => {
EventEmitter.off(EVENT_NAMES.SEND_MESSAGE, handleMessageSent)
}
}, [taskStatus.sendFirstMessage, dispatch])
// Cleanup on unmount
useEffect(() => {
return () => {

View File

@ -129,3 +129,401 @@
padding-top: 12px !important;
border-top: 1px solid var(--color-border) !important;
}
/* User Guide specific styles matching Figma design */
/* Supports both light and dark modes */
.driver-popover.user-guide-popover {
background: var(--color-background-soft) !important;
background-color: var(--color-background-soft) !important;
background-image: none !important;
color: var(--color-text) !important;
border: 1px solid var(--color-border) !important;
border-radius: 24px !important;
box-shadow: 0px 4px 13.8px 0px rgba(107, 114, 128, 0.2) !important;
max-width: 427px !important;
padding: 12px !important;
font-family: var(--font-family) !important;
opacity: 1 !important;
}
/* Dark mode shadow adjustment */
.driver-popover.user-guide-popover.dark {
box-shadow: 0px 4px 13.8px 0px rgba(0, 0, 0, 0.4) !important;
}
.driver-popover.user-guide-popover .driver-popover-title {
color: var(--color-text) !important;
font-size: 16px !important;
font-weight: 700 !important;
font-family: inherit !important;
margin-bottom: 12px !important;
line-height: 18px !important;
text-shadow: none !important;
-webkit-font-smoothing: antialiased !important;
-moz-osx-font-smoothing: grayscale !important;
}
.driver-popover.user-guide-popover .driver-popover-description {
color: var(--color-text-2) !important;
font-size: 14px !important;
font-weight: 400 !important;
font-family: inherit !important;
line-height: 1.5 !important;
text-shadow: none !important;
-webkit-font-smoothing: antialiased !important;
-moz-osx-font-smoothing: grayscale !important;
}
/* Hide progress text for user guide */
.driver-popover.user-guide-popover .driver-popover-progress-text {
display: none !important;
}
/* Hide footer border for user guide */
.driver-popover.user-guide-popover .driver-popover-footer {
border-top: none !important;
margin-top: 12px !important;
padding-top: 0 !important;
}
/* Image support in user guide popover */
.driver-popover.user-guide-popover .guide-image {
width: 100%;
border-radius: 16px;
margin-bottom: 12px;
overflow: hidden;
}
.driver-popover.user-guide-popover .guide-image img {
width: 395px;
max-width: 100%;
height: 200px;
object-fit: cover;
display: block;
border-radius: 16px;
}
/* Feature description paragraphs in user guide popover */
.driver-popover.user-guide-popover .guide-features {
margin: 0;
padding: 0;
}
.driver-popover.user-guide-popover .guide-features p {
margin: 0;
color: var(--color-text-2);
font-size: 14px;
font-weight: 400;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Legacy list support - convert to paragraph style */
.driver-popover.user-guide-popover .guide-steps {
margin: 0;
padding: 0;
list-style: none;
}
.driver-popover.user-guide-popover .guide-steps li {
margin: 0;
color: var(--color-text-2);
font-size: 14px;
font-weight: 400;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Legacy popover-image and popover-list support */
.driver-popover.user-guide-popover .popover-image {
width: 100%;
border-radius: 16px;
margin-bottom: 16px;
overflow: hidden;
}
.driver-popover.user-guide-popover .popover-image img {
width: 100%;
height: auto;
display: block;
}
.driver-popover.user-guide-popover .popover-list {
margin: 12px 0;
padding-left: 20px;
}
.driver-popover.user-guide-popover .popover-list li {
margin-bottom: 8px;
color: var(--color-text-2);
font-size: 14px;
line-height: 1.5;
}
/* User guide popover buttons */
.driver-popover.user-guide-popover .driver-popover-navigation-btns {
gap: 12px !important;
margin-top: 16px !important;
justify-content: flex-end !important;
}
/* Hide disabled Previous button (single-step guide) */
.driver-popover.user-guide-popover .driver-popover-prev-btn[disabled] {
display: none !important;
}
.driver-popover.user-guide-popover .driver-popover-next-btn,
.driver-popover.user-guide-popover .driver-popover-prev-btn {
font-family: var(--font-family) !important;
font-size: 14px !important;
font-weight: 500 !important;
line-height: 24px !important;
border-radius: 12px !important;
padding: 8px 16px !important;
text-align: center !important;
text-shadow: none !important;
-webkit-font-smoothing: antialiased !important;
-moz-osx-font-smoothing: grayscale !important;
}
.driver-popover.user-guide-popover .driver-popover-next-btn {
background-color: #3cd45a !important;
color: white !important;
border: none !important;
min-width: 103px !important;
}
.driver-popover.user-guide-popover .driver-popover-next-btn:hover {
background-color: #35c052 !important;
}
.driver-popover.user-guide-popover .driver-popover-prev-btn {
background-color: var(--color-background) !important;
color: var(--color-text-2) !important;
border: 1px solid var(--color-border) !important;
}
.driver-popover.user-guide-popover .driver-popover-prev-btn:hover {
background-color: var(--color-background-mute) !important;
}
/* Close button for user guide popover */
.driver-popover.user-guide-popover .driver-popover-close-btn {
position: absolute !important;
top: 16px !important;
right: 16px !important;
background: transparent !important;
border: none !important;
color: var(--color-text-3) !important;
padding: 0 !important;
width: 24px !important;
height: 24px !important;
}
/* Arrow styles for user guide popover */
.driver-popover.user-guide-popover .driver-popover-arrow-side-left {
border-right-color: var(--color-background-soft) !important;
}
.driver-popover.user-guide-popover .driver-popover-arrow-side-right {
border-left-color: var(--color-background-soft) !important;
}
.driver-popover.user-guide-popover .driver-popover-arrow-side-top {
border-bottom-color: var(--color-background-soft) !important;
}
.driver-popover.user-guide-popover .driver-popover-arrow-side-bottom {
border-top-color: var(--color-background-soft) !important;
}
/* Step 2: Minimal popover for pointing to target elements */
.driver-popover.user-guide-popover-minimal {
background: var(--color-background-soft) !important;
background-color: var(--color-background-soft) !important;
background-image: none !important;
color: var(--color-text) !important;
border: 1px solid var(--color-border) !important;
border-radius: 1.2vw !important;
box-shadow: 0px 4px 13.8px 0px rgba(107, 114, 128, 0.2) !important;
width: 20vw !important;
min-width: 280px !important;
max-width: 400px !important;
padding: 0.8vw !important;
font-family: var(--font-family) !important;
opacity: 1 !important;
}
.driver-popover.user-guide-popover-minimal.dark {
box-shadow: 0px 4px 13.8px 0px rgba(0, 0, 0, 0.4) !important;
}
/* Hide close button for minimal popover */
.driver-popover.user-guide-popover-minimal .driver-popover-close-btn {
display: none !important;
}
/* Hide driver.js title for minimal popover - title is in description */
.driver-popover.user-guide-popover-minimal .driver-popover-title {
display: none !important;
}
.driver-popover.user-guide-popover-minimal .driver-popover-description {
color: var(--color-text-2) !important;
font-size: 12px !important;
font-weight: 400 !important;
font-family: var(--font-family) !important;
line-height: 1.5 !important;
text-shadow: none !important;
-webkit-font-smoothing: antialiased !important;
-moz-osx-font-smoothing: grayscale !important;
}
/* Image support in minimal popover */
.driver-popover.user-guide-popover-minimal .guide-image {
width: 100%;
height: 10vw;
min-height: 120px;
max-height: 200px;
border-radius: 0.8vw;
margin-bottom: 0.8vw;
overflow: hidden;
}
.driver-popover.user-guide-popover-minimal .guide-image img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
border-radius: 0.8vw;
}
/* Video support in minimal popover */
.driver-popover.user-guide-popover-minimal .guide-video {
position: relative;
width: 100%;
height: 10vw;
min-height: 120px;
max-height: 200px;
border-radius: 0.8vw;
margin-bottom: 0.8vw;
overflow: hidden;
}
.driver-popover.user-guide-popover-minimal .guide-video video {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
display: block;
border-radius: 0.8vw;
}
/* Skeleton placeholder for video loading */
.driver-popover.user-guide-popover-minimal .guide-video-skeleton {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: var(--color-background-mute);
border-radius: 0.8vw;
}
.driver-popover.user-guide-popover-minimal .guide-video-skeleton .ant-skeleton-image {
width: 100% !important;
height: 100% !important;
border-radius: 0.8vw;
}
/* Title inside description for minimal popover */
.driver-popover.user-guide-popover-minimal .guide-title {
font-size: 14px;
font-weight: 700;
color: var(--color-text);
font-family: var(--font-family);
line-height: 16px;
margin-bottom: 0.4vw;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Feature list in minimal popover */
.driver-popover.user-guide-popover-minimal .guide-features p {
margin: 0;
color: var(--color-text-2);
font-size: 12px;
font-weight: 400;
font-family: var(--font-family);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Hide progress text for minimal popover */
.driver-popover.user-guide-popover-minimal .driver-popover-progress-text {
display: none !important;
}
/* Hide footer border for minimal popover */
.driver-popover.user-guide-popover-minimal .driver-popover-footer {
border-top: none !important;
margin-top: 0.8vw !important;
padding-top: 0 !important;
}
/* Hide disabled Previous button for minimal popover */
.driver-popover.user-guide-popover-minimal .driver-popover-prev-btn[disabled] {
display: none !important;
}
/* Minimal popover buttons */
.driver-popover.user-guide-popover-minimal .driver-popover-navigation-btns {
gap: 8px !important;
justify-content: flex-end !important;
}
.driver-popover.user-guide-popover-minimal .driver-popover-next-btn,
.driver-popover.user-guide-popover-minimal .driver-popover-prev-btn {
font-family: var(--font-family) !important;
font-size: 12px !important;
font-weight: 500 !important;
line-height: 20px !important;
border-radius: 0.6vw !important;
padding: 0.4vw 0.8vw !important;
min-width: 80px !important;
text-align: center !important;
text-shadow: none !important;
-webkit-font-smoothing: antialiased !important;
-moz-osx-font-smoothing: grayscale !important;
}
.driver-popover.user-guide-popover-minimal .driver-popover-next-btn {
background-color: #3cd45a !important;
color: white !important;
border: none !important;
}
.driver-popover.user-guide-popover-minimal .driver-popover-next-btn:hover {
background-color: #35c052 !important;
}
/* Arrow styles for minimal popover */
.driver-popover.user-guide-popover-minimal .driver-popover-arrow-side-left {
border-right-color: var(--color-background-soft) !important;
}
.driver-popover.user-guide-popover-minimal .driver-popover-arrow-side-right {
border-left-color: var(--color-background-soft) !important;
}
.driver-popover.user-guide-popover-minimal .driver-popover-arrow-side-top {
border-bottom-color: var(--color-background-soft) !important;
}
.driver-popover.user-guide-popover-minimal .driver-popover-arrow-side-bottom {
border-top-color: var(--color-background-soft) !important;
}

View File

@ -2,6 +2,7 @@ import { PlusOutlined } from '@ant-design/icons'
import { loggerService } from '@logger'
import { Sortable, useDndReorder } from '@renderer/components/dnd'
import HorizontalScrollContainer from '@renderer/components/HorizontalScrollContainer'
import { ChecklistContent } from '@renderer/components/UserGuide/UserGuideChecklist'
import { isLinux, isMac } from '@renderer/config/constant'
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
import { useTheme } from '@renderer/context/ThemeProvider'
@ -12,14 +13,16 @@ import { useSettings } from '@renderer/hooks/useSettings'
import { getThemeModeLabel, getTitleLabel } from '@renderer/i18n/label'
import tabsService from '@renderer/services/TabsService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setChecklistVisible } from '@renderer/store/onboarding'
import type { Tab } from '@renderer/store/tabs'
import { addTab, removeTab, setActiveTab, setTabs } from '@renderer/store/tabs'
import type { MinAppType } from '@renderer/types'
import { ThemeMode } from '@renderer/types'
import { classNames } from '@renderer/utils'
import { Tooltip } from 'antd'
import { Popover, Tooltip } from 'antd'
import type { LRUCache } from 'lru-cache'
import {
BadgeCheck,
FileSearch,
Folder,
Home,
@ -126,6 +129,20 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
const { useSystemTitleBar } = useSettings()
const { t } = useTranslation()
// User guide state
const { guidePageCompleted, checklistDismissed, checklistVisible, taskStatus } = useAppSelector(
(state) => state.onboarding
)
const allTasksCompleted = taskStatus.useFreeModel && taskStatus.configureProvider && taskStatus.sendFirstMessage
const showUserGuideButton = guidePageCompleted && !checklistDismissed && !allTasksCompleted
const handleChecklistVisibleChange = useCallback(
(visible: boolean) => {
dispatch(setChecklistVisible(visible))
},
[dispatch]
)
const getTabId = (path: string): string => {
if (path === '/') return 'home'
const segments = path.split('/')
@ -271,6 +288,22 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
</AddTabButton>
</HorizontalScrollContainer>
<RightButtonsContainer style={{ paddingRight: isLinux && useSystemTitleBar ? '12px' : undefined }}>
{showUserGuideButton && (
<Popover
content={<ChecklistContent />}
trigger="click"
open={checklistVisible}
onOpenChange={handleChecklistVisibleChange}
placement="bottomRight"
arrow={false}
styles={{ body: { padding: 0 } }}>
<Tooltip title={t('userGuide.checklist.title')} mouseEnterDelay={0.8} placement="bottom">
<UserGuideButton data-guide-target="user-guide-badge">
<BadgeCheck size={16} />
</UserGuideButton>
</Tooltip>
</Popover>
)}
<Tooltip
title={t('settings.theme.title') + ': ' + getThemeModeLabel(settedTheme)}
mouseEnterDelay={0.8}
@ -436,6 +469,21 @@ const ThemeButton = styled.div`
}
`
const UserGuideButton = styled.div`
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
cursor: pointer;
color: var(--color-primary);
border-radius: 8px;
&:hover {
background: var(--color-list-item);
}
`
const SettingsButton = styled.div<{ $active: boolean }>`
display: flex;
align-items: center;

View File

@ -0,0 +1,35 @@
import type { AssistantPreset } from '@renderer/types'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import {
AssistantCardContainer,
AssistantDescription,
AssistantIcon,
AssistantInfo,
AssistantName,
ChatButton
} from './styles'
interface AssistantCardProps {
assistant: AssistantPreset
onClick: () => void
}
const AssistantCard: FC<AssistantCardProps> = ({ assistant, onClick }) => {
const { t } = useTranslation()
const emoji = assistant.emoji || '🤖'
return (
<AssistantCardContainer onClick={onClick}>
<AssistantIcon>{emoji}</AssistantIcon>
<AssistantInfo>
<AssistantName>{assistant.name}</AssistantName>
<AssistantDescription>{assistant.description || assistant.prompt?.slice(0, 50)}</AssistantDescription>
</AssistantInfo>
<ChatButton className="chat-button">{t('userGuide.completionModal.startChat')}</ChatButton>
</AssistantCardContainer>
)
}
export default AssistantCard

View File

@ -0,0 +1,93 @@
import { motion } from 'framer-motion'
import type { FC } from 'react'
import { useEffect, useState } from 'react'
import styled from 'styled-components'
interface ConfettiProps {
duration?: number
}
interface Particle {
id: number
x: number
color: string
delay: number
rotation: number
scale: number
}
const COLORS = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE']
const Confetti: FC<ConfettiProps> = ({ duration = 2000 }) => {
const [particles, setParticles] = useState<Particle[]>([])
const [isVisible, setIsVisible] = useState(true)
useEffect(() => {
// Generate particles
const newParticles: Particle[] = Array.from({ length: 50 }, (_, i) => ({
id: i,
x: Math.random() * 100, // percentage across screen
color: COLORS[Math.floor(Math.random() * COLORS.length)],
delay: Math.random() * 0.5,
rotation: Math.random() * 360,
scale: 0.5 + Math.random() * 0.5
}))
setParticles(newParticles)
// Hide after duration
const timer = setTimeout(() => {
setIsVisible(false)
}, duration)
return () => clearTimeout(timer)
}, [duration])
if (!isVisible) return null
return (
<Container>
{particles.map((particle) => (
<motion.div
key={particle.id}
initial={{
y: -20,
x: `${particle.x}vw`,
opacity: 1,
rotate: particle.rotation,
scale: particle.scale
}}
animate={{
y: '100vh',
rotate: particle.rotation + 720,
opacity: [1, 1, 0]
}}
transition={{
duration: 2,
delay: particle.delay,
ease: 'easeOut'
}}
style={{
position: 'absolute',
width: 10,
height: 10,
backgroundColor: particle.color,
borderRadius: Math.random() > 0.5 ? '50%' : '2px'
}}
/>
))}
</Container>
)
}
const Container = styled.div`
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1200;
overflow: hidden;
`
export default Confetti

View File

@ -0,0 +1,116 @@
import assistantBackground from '@renderer/assets/images/guide/assistant_background.png'
import cherryai3d from '@renderer/assets/images/guide/cherryai_3d.png'
import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets'
import { createAssistantFromAgent } from '@renderer/services/AssistantService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { showCompletionModal } from '@renderer/store/onboarding'
import type { AssistantPreset } from '@renderer/types'
import { Compass } from 'lucide-react'
import type { FC } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import AssistantCard from './AssistantCard'
import Confetti from './Confetti'
import {
AssistantsGrid,
AssistantsSection,
CherryImage,
ExploreButton,
ModalContainer,
ModalFooter,
ModalHeader,
ModalOverlay,
ModalSubtitle,
ModalTitle
} from './styles'
const CompletionModal: FC = () => {
const { t } = useTranslation()
const dispatch = useAppDispatch()
const navigate = useNavigate()
const { presets } = useAssistantPresets()
const { taskStatus, completionModalShown, guidePageCompleted } = useAppSelector((state) => state.onboarding)
const [isVisible, setIsVisible] = useState(false)
const [showConfetti, setShowConfetti] = useState(false)
const allTasksCompleted = useMemo(
() => taskStatus.useFreeModel && taskStatus.configureProvider && taskStatus.sendFirstMessage,
[taskStatus]
)
// Show modal when all tasks completed and not shown before
useEffect(() => {
if (allTasksCompleted && !completionModalShown && guidePageCompleted) {
setShowConfetti(true)
setIsVisible(true)
dispatch(showCompletionModal())
}
}, [allTasksCompleted, completionModalShown, guidePageCompleted, dispatch])
// Get recommended assistants (first 6 from presets)
const recommendedAssistants = useMemo(() => presets.slice(0, 6), [presets])
const handleAssistantClick = useCallback(
(assistant: AssistantPreset) => {
createAssistantFromAgent(assistant)
setIsVisible(false)
navigate('/')
},
[navigate]
)
const handleExplore = useCallback(() => {
setIsVisible(false)
navigate('/store')
}, [navigate])
const handleClose = useCallback(() => {
setIsVisible(false)
}, [])
if (!isVisible) {
return null
}
return (
<>
{showConfetti && <Confetti duration={2000} />}
<ModalOverlay onClick={handleClose}>
<ModalContainer $backgroundImage={assistantBackground} onClick={(e) => e.stopPropagation()}>
<ModalHeader>
<CherryImage src={cherryai3d} alt="Cherry AI" />
<ModalTitle>{t('userGuide.completionModal.title')}</ModalTitle>
<ModalSubtitle>{t('userGuide.completionModal.subtitle')}</ModalSubtitle>
</ModalHeader>
{recommendedAssistants.length > 0 && (
<AssistantsSection>
<AssistantsGrid>
{recommendedAssistants.map((assistant) => (
<AssistantCard
key={assistant.id}
assistant={assistant}
onClick={() => handleAssistantClick(assistant)}
/>
))}
</AssistantsGrid>
</AssistantsSection>
)}
<ModalFooter>
<ExploreButton onClick={handleExplore}>
<Compass size={16} />
{t('userGuide.completionModal.exploreMore')}
</ExploreButton>
</ModalFooter>
</ModalContainer>
</ModalOverlay>
</>
)
}
export default CompletionModal

View File

@ -0,0 +1,198 @@
import styled from 'styled-components'
export interface ModalContainerProps {
$backgroundImage?: string
}
export const ModalOverlay = styled.div`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(8px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1100;
animation: fadeIn 0.2s ease;
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
`
export const ModalContainer = styled.div<ModalContainerProps>`
background: var(--color-background);
background-image: ${({ $backgroundImage }) => ($backgroundImage ? `url(${$backgroundImage})` : 'none')};
background-size: cover;
background-position: center;
border-radius: 24px;
width: 520px;
max-width: 90vw;
max-height: 90vh;
overflow: hidden;
box-shadow: 0px 8px 40px rgba(0, 0, 0, 0.2);
animation: slideUp 0.3s ease;
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
`
export const ModalHeader = styled.div`
display: flex;
flex-direction: column;
align-items: center;
padding: 32px 32px 20px;
text-align: center;
`
export const CherryImage = styled.img`
width: 160px;
height: auto;
margin-bottom: 16px;
`
export const ModalTitle = styled.h2`
font-size: 22px;
font-weight: 700;
color: var(--color-text);
margin: 0 0 8px 0;
`
export const ModalSubtitle = styled.p`
font-size: 14px;
color: var(--color-text-2);
margin: 0;
`
export const AssistantsSection = styled.div`
padding: 0 24px 20px;
`
export const AssistantsGrid = styled.div`
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
`
export const AssistantCardContainer = styled.div`
position: relative;
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 12px;
border: 1px solid var(--color-border);
background: var(--color-background);
cursor: pointer;
transition: all 0.2s ease;
&:hover {
border-color: var(--color-primary);
background: var(--color-background-soft);
.chat-button {
opacity: 1;
transform: translateY(0);
}
}
`
export const AssistantIcon = styled.div`
width: 36px;
height: 36px;
border-radius: 8px;
background: var(--color-background-soft);
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
`
export const AssistantInfo = styled.div`
flex: 1;
min-width: 0;
`
export const AssistantName = styled.div`
font-size: 13px;
font-weight: 500;
color: var(--color-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`
export const AssistantDescription = styled.div`
font-size: 11px;
color: var(--color-text-3);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-top: 2px;
`
export const ChatButton = styled.button`
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%) translateY(4px);
opacity: 0;
padding: 4px 10px;
background: var(--color-primary);
color: white;
border: none;
border-radius: 6px;
font-size: 11px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
&:hover {
background: var(--color-primary-hover, #35c052);
}
`
export const ModalFooter = styled.div`
display: flex;
justify-content: center;
padding: 0 24px 24px;
`
export const ExploreButton = styled.button`
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
width: 100%;
padding: 12px 20px;
background: var(--color-primary);
color: var(--color-white);
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
opacity: 0.9;
}
`

View File

@ -0,0 +1,90 @@
import carousel1 from '@renderer/assets/images/guide/Carousel_1.png'
import carousel1Dark from '@renderer/assets/images/guide/Carousel_1_dark.png'
import carousel2 from '@renderer/assets/images/guide/Carousel_2.png'
import carousel2Dark from '@renderer/assets/images/guide/Carousel_2_dark.png'
import carousel3 from '@renderer/assets/images/guide/Carousel_3.png'
import carousel3Dark from '@renderer/assets/images/guide/Carousel_3_dark.png'
import { useTheme } from '@renderer/context/ThemeProvider'
import type { FC } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
CarouselDot,
CarouselDots,
FeatureContent,
FeatureDescription,
FeatureImage,
FeatureTitle,
gradients,
RightPanel
} from './styles'
interface FeatureSlide {
titleKey: string
descriptionKey: string
gradient: string
image: string
imageDark: string
}
const slides: FeatureSlide[] = [
{
titleKey: 'userGuide.guidePage.carousel.assistants.title',
descriptionKey: 'userGuide.guidePage.carousel.assistants.description',
gradient: gradients.assistants,
image: carousel1,
imageDark: carousel1Dark
},
{
titleKey: 'userGuide.guidePage.carousel.paintings.title',
descriptionKey: 'userGuide.guidePage.carousel.paintings.description',
gradient: gradients.paintings,
image: carousel2,
imageDark: carousel2Dark
},
{
titleKey: 'userGuide.guidePage.carousel.models.title',
descriptionKey: 'userGuide.guidePage.carousel.models.description',
gradient: gradients.models,
image: carousel3,
imageDark: carousel3Dark
}
]
const FeatureCarousel: FC = () => {
const { t } = useTranslation()
const { theme } = useTheme()
const [activeIndex, setActiveIndex] = useState(0)
const isDark = theme === 'dark'
const nextSlide = useCallback(() => {
setActiveIndex((prev) => (prev + 1) % slides.length)
}, [])
useEffect(() => {
const interval = setInterval(nextSlide, 5000)
return () => clearInterval(interval)
}, [nextSlide])
const currentSlide = slides[activeIndex]
return (
<RightPanel $gradient={currentSlide.gradient}>
<FeatureContent>
<FeatureTitle>{t(currentSlide.titleKey)}</FeatureTitle>
<FeatureDescription>{t(currentSlide.descriptionKey)}</FeatureDescription>
<FeatureImage>
<img src={isDark ? currentSlide.imageDark : currentSlide.image} alt="" />
</FeatureImage>
</FeatureContent>
<CarouselDots>
{slides.map((_, index) => (
<CarouselDot key={index} $active={index === activeIndex} onClick={() => setActiveIndex(index)} />
))}
</CarouselDots>
</RightPanel>
)
}
export default FeatureCarousel

View File

@ -0,0 +1,39 @@
import layoutNav from '@renderer/assets/images/guide/layout_nav.png'
import layoutNavDark from '@renderer/assets/images/guide/layout_nav_dark.png'
import layoutSide from '@renderer/assets/images/guide/layout_side.png'
import layoutSideDark from '@renderer/assets/images/guide/layout_side_dark.png'
import { useTheme } from '@renderer/context/ThemeProvider'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { NavStyleLabel, NavStyleOption, NavStyleOptions, NavStylePreview } from './styles'
interface NavStyleSelectorProps {
value: 'left' | 'top'
onChange: (value: 'left' | 'top') => void
}
const NavStyleSelector: FC<NavStyleSelectorProps> = ({ value, onChange }) => {
const { t } = useTranslation()
const { theme } = useTheme()
const isDark = theme === 'dark'
return (
<NavStyleOptions>
<NavStyleOption $selected={value === 'left'} onClick={() => onChange('left')}>
<NavStylePreview>
<img src={isDark ? layoutSideDark : layoutSide} alt="Left sidebar" />
</NavStylePreview>
<NavStyleLabel>{t('userGuide.guidePage.navStyle.left')}</NavStyleLabel>
</NavStyleOption>
<NavStyleOption $selected={value === 'top'} onClick={() => onChange('top')}>
<NavStylePreview>
<img src={isDark ? layoutNavDark : layoutNav} alt="Top navigation" />
</NavStylePreview>
<NavStyleLabel>{t('userGuide.guidePage.navStyle.top')}</NavStyleLabel>
</NavStyleOption>
</NavStyleOptions>
)
}
export default NavStyleSelector

View File

@ -0,0 +1,127 @@
import logoImage from '@renderer/assets/images/logo.png'
import Selector from '@renderer/components/Selector'
import i18n from '@renderer/i18n'
import { useAppDispatch } from '@renderer/store'
import { completeGuidePage } from '@renderer/store/onboarding'
import { setLanguage, setNavbarPosition } from '@renderer/store/settings'
import type { LanguageVarious } from '@renderer/types'
import { Flex } from 'antd'
import type { FC } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import FeatureCarousel from './FeatureCarousel'
import NavStyleSelector from './NavStyleSelector'
import {
ContentSection,
GuidePageContainer,
LanguageLabel,
LanguageSection,
LanguageSelector,
LeftPanel,
LeftPanelContent,
LogoContainer,
LogoImage,
LogoText,
SectionTitle,
SettingSection,
SettingsWrapper,
StartButton,
TitleSection,
WelcomeSubtitle,
WelcomeTitle
} from './styles'
const languagesOptions: { value: LanguageVarious; label: string; flag: string }[] = [
{ value: 'zh-CN', label: '中文', flag: '🇨🇳' },
{ value: 'zh-TW', label: '中文(繁体)', flag: '🇭🇰' },
{ value: 'en-US', label: 'English', flag: '🇺🇸' },
{ value: 'de-DE', label: 'Deutsch', flag: '🇩🇪' },
{ value: 'ja-JP', label: '日本語', flag: '🇯🇵' },
{ value: 'ru-RU', label: 'Русский', flag: '🇷🇺' },
{ value: 'el-GR', label: 'Ελληνικά', flag: '🇬🇷' },
{ value: 'es-ES', label: 'Español', flag: '🇪🇸' },
{ value: 'fr-FR', label: 'Français', flag: '🇫🇷' },
{ value: 'pt-PT', label: 'Português', flag: '🇵🇹' },
{ value: 'ro-RO', label: 'Română', flag: '🇷🇴' }
]
const GuidePage: FC = () => {
const { t } = useTranslation()
const dispatch = useAppDispatch()
const navigate = useNavigate()
const [navStyle, setNavStyle] = useState<'left' | 'top'>('top')
const [language, setLanguageState] = useState<LanguageVarious>(
(localStorage.getItem('language') as LanguageVarious) || (navigator.language as LanguageVarious) || 'en-US'
)
const handleLanguageChange = (value: LanguageVarious) => {
setLanguageState(value)
dispatch(setLanguage(value))
localStorage.setItem('language', value)
window.api.setLanguage(value)
i18n.changeLanguage(value)
}
const handleStart = () => {
dispatch(setNavbarPosition(navStyle))
dispatch(completeGuidePage())
navigate('/')
}
return (
<GuidePageContainer>
<LeftPanel>
<LeftPanelContent>
<LogoContainer>
<LogoImage src={logoImage} alt="Cherry Studio" />
<LogoText>Cherry Studio</LogoText>
</LogoContainer>
<ContentSection>
<SettingsWrapper>
<TitleSection>
<WelcomeTitle>{t('userGuide.guidePage.welcome.title')}</WelcomeTitle>
<WelcomeSubtitle>{t('userGuide.guidePage.welcome.subtitle')}</WelcomeSubtitle>
</TitleSection>
<SettingSection>
<SectionTitle>{t('userGuide.guidePage.navStyle.title')}</SectionTitle>
<NavStyleSelector value={navStyle} onChange={setNavStyle} />
</SettingSection>
<LanguageSection>
<LanguageLabel>{t('common.language')}</LanguageLabel>
<LanguageSelector>
<Selector
size={14}
value={language}
onChange={handleLanguageChange}
options={languagesOptions.map((lang) => ({
label: (
<Flex align="center" gap={8}>
<span role="img" aria-label={lang.flag}>
{lang.flag}
</span>
{lang.label}
</Flex>
),
value: lang.value
}))}
/>
</LanguageSelector>
</LanguageSection>
</SettingsWrapper>
<StartButton onClick={handleStart}>{t('userGuide.guidePage.startButton')}</StartButton>
</ContentSection>
</LeftPanelContent>
</LeftPanel>
<FeatureCarousel />
</GuidePageContainer>
)
}
export default GuidePage

View File

@ -0,0 +1,313 @@
import styled from 'styled-components'
export const GuidePageContainer = styled.div`
display: flex;
width: 100vw;
height: 100vh;
background: var(--color-background);
overflow: hidden;
-webkit-app-region: drag;
gap: 27px;
padding: 0;
`
export const LeftPanel = styled.div`
flex: 0 0 682px;
display: flex;
flex-direction: column;
align-items: center;
padding: 64px 10px;
-webkit-app-region: no-drag;
`
export const LeftPanelContent = styled.div`
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
padding: 0 32px;
gap: 129px;
`
export const LogoContainer = styled.div`
display: flex;
align-items: center;
gap: 12px;
`
export const LogoImage = styled.img`
width: 32px;
height: 32px;
border-radius: 8px;
`
export const LogoText = styled.span`
font-size: 16px;
font-weight: 700;
color: rgba(0, 0, 0, 0.9);
line-height: 18px;
body[theme-mode='dark'] & {
color: rgba(255, 255, 255, 0.9);
}
`
export const ContentSection = styled.div`
display: flex;
flex-direction: column;
align-items: center;
gap: 64px;
padding: 32px;
width: 100%;
`
export const SettingsWrapper = styled.div`
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
`
export const TitleSection = styled.div`
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
width: 100%;
`
export const WelcomeTitle = styled.h1`
font-size: 32px;
font-weight: 700;
color: rgba(0, 0, 0, 0.9);
margin: 0;
text-align: center;
line-height: 22px;
body[theme-mode='dark'] & {
color: rgba(255, 255, 255, 0.9);
}
`
export const WelcomeSubtitle = styled.p`
font-size: 14px;
font-weight: 500;
color: rgba(0, 0, 0, 0.6);
margin: 0;
text-align: center;
line-height: 16px;
body[theme-mode='dark'] & {
color: rgba(255, 255, 255, 0.6);
}
`
export const SettingSection = styled.div`
width: 100%;
`
export const SectionTitle = styled.h2`
font-size: 14px;
font-weight: 500;
color: rgba(0, 0, 0, 0.9);
margin: 0;
padding: 8px;
line-height: 16px;
body[theme-mode='dark'] & {
color: rgba(255, 255, 255, 0.9);
}
`
export const NavStyleOptions = styled.div`
display: flex;
gap: 8px;
justify-content: space-between;
padding: 8px;
width: 100%;
`
export const NavStyleOption = styled.div<{ $selected?: boolean }>`
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
padding: 8px 12px;
border-radius: 8px;
border: 1px solid ${(props) => (props.$selected ? 'rgba(60, 212, 90, 0.8)' : 'transparent')};
background: #f5f5f5;
cursor: pointer;
transition: all 0.2s ease;
width: 216px;
&:hover {
border-color: ${(props) => (props.$selected ? 'rgba(60, 212, 90, 0.8)' : 'rgba(60, 212, 90, 0.4)')};
}
body[theme-mode='dark'] & {
background: #2a2a2a;
}
`
export const NavStylePreview = styled.div`
width: 183px;
height: 100px;
border-radius: 8px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
`
export const NavStyleLabel = styled.span`
font-size: 14px;
font-weight: 500;
color: rgba(0, 0, 0, 0.9);
line-height: 16px;
body[theme-mode='dark'] & {
color: rgba(255, 255, 255, 0.9);
}
`
export const LanguageSection = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 36px;
padding: 8px;
`
export const LanguageLabel = styled.span`
font-size: 14px;
font-weight: 500;
color: rgba(0, 0, 0, 0.9);
line-height: 16px;
body[theme-mode='dark'] & {
color: rgba(255, 255, 255, 0.9);
}
`
export const LanguageSelector = styled.div`
height: 32px;
`
export const StartButton = styled.button`
display: flex;
align-items: center;
justify-content: center;
width: 300px;
padding: 8px 16px;
background: #3cd45a;
color: white;
border: none;
border-radius: 12px;
font-size: 14px;
font-weight: 500;
line-height: 24px;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background: #35c052;
}
&:active {
transform: scale(0.98);
}
`
// Gradient definitions for each carousel slide
export const gradients = {
assistants: 'linear-gradient(135deg, #a78bfa 0%, #f472b6 50%, #fb923c 100%)',
paintings: 'linear-gradient(135deg, #fb923c 0%, #f472b6 50%, #a78bfa 100%)',
models: 'linear-gradient(135deg, #60a5fa 0%, #a78bfa 50%, #f472b6 100%)'
}
export const RightPanel = styled.div<{ $gradient?: string }>`
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
background: ${(props) => props.$gradient || gradients.assistants};
border-radius: 24px;
position: relative;
overflow: hidden;
margin: 0;
height: 100%;
`
export const FeatureContent = styled.div`
display: flex;
flex-direction: column;
align-items: flex-start;
width: 598px;
z-index: 1;
`
export const FeatureTitle = styled.h2`
font-size: 32px;
font-weight: 700;
color: rgba(255, 255, 255, 0.9);
margin: 0 0 24px 0;
line-height: 22px;
`
export const FeatureDescription = styled.p`
font-size: 14px;
font-weight: 500;
color: rgba(255, 255, 255, 0.6);
line-height: 16px;
margin: 0;
`
export const FeatureImage = styled.div`
width: 600px;
height: 408px;
margin-top: 24px;
border-radius: 12px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
`
export const CarouselDots = styled.div`
display: flex;
gap: 8px;
justify-content: center;
position: absolute;
bottom: 46px;
left: 50%;
transform: translateX(-50%);
`
export const CarouselDot = styled.button<{ $active?: boolean }>`
width: 135px;
height: ${(props) => (props.$active ? '6px' : '4px')};
border-radius: 4px;
border: none;
background: #fafafa;
opacity: ${(props) => (props.$active ? '0.5' : '0.2')};
cursor: pointer;
transition: all 0.2s ease;
padding: 0;
&:hover {
opacity: 0.6;
}
`

View File

@ -0,0 +1,138 @@
import { useOnboarding } from '@renderer/components/Onboarding'
import {
configureProviderGuideStep2,
sendMessageGuideStep2,
useFreeModelGuideStep2
} from '@renderer/config/onboarding/guides'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setChecklistVisible } from '@renderer/store/onboarding'
import { Popover } from 'antd'
import { X } from 'lucide-react'
import type { FC } from 'react'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import {
ChecklistHeader,
ChecklistSubtitle,
ChecklistTitle,
CloseButton,
ContentContainer,
ProgressText,
Separator,
TaskList,
TitleSection
} from './styles'
import TaskItem from './TaskItem'
import TaskPopoverContent from './TaskPopoverContent'
type TaskKey = 'useFreeModel' | 'configureProvider' | 'sendFirstMessage'
const ChecklistContent: FC = () => {
const { t } = useTranslation()
const dispatch = useAppDispatch()
const navigate = useNavigate()
const { startGuide } = useOnboarding()
const { taskStatus } = useAppSelector((state) => state.onboarding)
const [activePopover, setActivePopover] = useState<TaskKey | null>(null)
const tasks = useMemo(
() => [
{
key: 'useFreeModel' as const,
label: t('userGuide.checklist.tasks.useFreeModel'),
completed: taskStatus.useFreeModel
},
{
key: 'configureProvider' as const,
label: t('userGuide.checklist.tasks.configureProvider'),
completed: taskStatus.configureProvider
},
{
key: 'sendFirstMessage' as const,
label: t('userGuide.checklist.tasks.sendFirstMessage'),
completed: taskStatus.sendFirstMessage
}
],
[taskStatus, t]
)
const completedCount = tasks.filter((task) => task.completed).length
const handleClose = useCallback(() => {
dispatch(setChecklistVisible(false))
}, [dispatch])
// Handle popover visibility change
const handlePopoverOpenChange = useCallback(
(taskKey: TaskKey, open: boolean) => {
if (taskStatus[taskKey]) return
setActivePopover(open ? taskKey : null)
},
[taskStatus]
)
// Start Driver.js guide after popover button is clicked
const handlePopoverConfirm = useCallback(
(taskKey: TaskKey) => {
setActivePopover(null)
dispatch(setChecklistVisible(false))
switch (taskKey) {
case 'useFreeModel':
navigate('/')
setTimeout(() => startGuide(useFreeModelGuideStep2), 300)
break
case 'configureProvider':
navigate('/settings/provider')
setTimeout(() => startGuide(configureProviderGuideStep2), 300)
break
case 'sendFirstMessage':
navigate('/')
setTimeout(() => startGuide(sendMessageGuideStep2), 300)
break
}
},
[navigate, startGuide, dispatch]
)
return (
<ContentContainer>
<ChecklistHeader>
<TitleSection>
<ChecklistTitle>{t('userGuide.checklist.title')}</ChecklistTitle>
<ChecklistSubtitle>{t('userGuide.checklist.subtitle')}</ChecklistSubtitle>
</TitleSection>
<CloseButton onClick={handleClose}>
<X size={16} />
</CloseButton>
</ChecklistHeader>
<Separator />
<TaskList>
{tasks.map((task) => (
<Popover
key={task.key}
content={<TaskPopoverContent taskKey={task.key} onConfirm={() => handlePopoverConfirm(task.key)} />}
trigger="click"
open={activePopover === task.key}
onOpenChange={(open) => handlePopoverOpenChange(task.key, open)}
placement="leftTop"
arrow={false}
align={{ offset: [-8, 0] }}>
<div>
<TaskItem label={task.label} completed={task.completed} />
</div>
</Popover>
))}
</TaskList>
<ProgressText>
{t('userGuide.checklist.progress', { completed: completedCount, total: tasks.length })}
</ProgressText>
</ContentContainer>
)
}
export default ChecklistContent

View File

@ -0,0 +1,83 @@
import { Skeleton } from 'antd'
import type { FC } from 'react'
import { useCallback, useState } from 'react'
import styled from 'styled-components'
interface LazyMediaProps {
type: 'image' | 'video'
src: string
alt?: string
}
const LazyMedia: FC<LazyMediaProps> = ({ type, src, alt = '' }) => {
const [isLoaded, setIsLoaded] = useState(false)
const handleLoad = useCallback(() => {
setIsLoaded(true)
}, [])
return (
<MediaWrapper>
{!isLoaded && (
<SkeletonWrapper>
<Skeleton.Image active style={{ width: '100%', height: '100%' }} />
</SkeletonWrapper>
)}
<MediaContent $isLoaded={isLoaded}>
{type === 'video' ? (
<video src={src} autoPlay loop muted playsInline onLoadedData={handleLoad} />
) : (
<img src={src} alt={alt} onLoad={handleLoad} />
)}
</MediaContent>
</MediaWrapper>
)
}
const MediaWrapper = styled.div`
position: relative;
width: 100%;
height: 10vw;
min-height: 120px;
max-height: 200px;
border-radius: 0.8vw;
overflow: hidden;
`
const SkeletonWrapper = styled.div`
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
.ant-skeleton-image {
width: 100% !important;
height: 100% !important;
border-radius: 0.8vw;
}
.ant-skeleton-image-svg {
width: 48px;
height: 48px;
}
`
const MediaContent = styled.div<{ $isLoaded: boolean }>`
position: absolute;
inset: 0;
opacity: ${({ $isLoaded }) => ($isLoaded ? 1 : 0)};
transition: opacity 0.3s ease;
img,
video {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
border-radius: 0.8vw;
}
`
export default LazyMedia

View File

@ -0,0 +1,21 @@
import { Check } from 'lucide-react'
import type { FC } from 'react'
import { Checkbox, TaskItemContainer, TaskText } from './styles'
interface TaskItemProps {
label: string
completed: boolean
onClick?: () => void
}
const TaskItem: FC<TaskItemProps> = ({ label, completed, onClick }) => {
return (
<TaskItemContainer $completed={completed} onClick={onClick}>
<Checkbox $checked={completed}>{completed && <Check size={12} color="white" strokeWidth={3} />}</Checkbox>
<TaskText $completed={completed}>{label}</TaskText>
</TaskItemContainer>
)
}
export default TaskItem

View File

@ -0,0 +1,170 @@
import chatVideoLight from '@renderer/assets/images/guide/chat.mp4'
import chatVideoDark from '@renderer/assets/images/guide/chat_dark.mp4'
import configureProviderLight from '@renderer/assets/images/guide/Configure_Provider_step0.mp4'
import configureProviderDark from '@renderer/assets/images/guide/Configure_Provider_step0_dark.mp4'
import freeModelGif from '@renderer/assets/images/guide/free_model.gif'
import { useTheme } from '@renderer/context/ThemeProvider'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import LazyMedia from './LazyMedia'
type TaskKey = 'useFreeModel' | 'configureProvider' | 'sendFirstMessage'
interface TaskPopoverContentProps {
taskKey: TaskKey
onConfirm: () => void
}
interface MediaContent {
type: 'image' | 'video'
light: string
dark?: string
}
interface TaskContent {
titleKey: string
features: string[]
buttonKey: string
media?: MediaContent
}
const TASK_CONTENT: Record<TaskKey, TaskContent> = {
useFreeModel: {
titleKey: 'userGuide.taskPopover.useFreeModel.title',
features: [
'userGuide.taskPopover.useFreeModel.features.noRegistration',
'userGuide.taskPopover.useFreeModel.features.fastResponse',
'userGuide.taskPopover.useFreeModel.features.allInOne'
],
buttonKey: 'userGuide.taskPopover.useFreeModel.button',
media: {
type: 'image',
light: freeModelGif
}
},
configureProvider: {
titleKey: 'userGuide.taskPopover.configureProvider.title',
features: [
'userGuide.taskPopover.configureProvider.features.unlockModels',
'userGuide.taskPopover.configureProvider.features.privacyFirst',
'userGuide.taskPopover.configureProvider.features.localModels'
],
buttonKey: 'userGuide.taskPopover.configureProvider.button',
media: {
type: 'video',
light: configureProviderLight,
dark: configureProviderDark
}
},
sendFirstMessage: {
titleKey: 'userGuide.taskPopover.sendFirstMessage.title',
features: [
'userGuide.taskPopover.sendFirstMessage.features.markdown',
'userGuide.taskPopover.sendFirstMessage.features.typewriter',
'userGuide.taskPopover.sendFirstMessage.features.fileAnalysis'
],
buttonKey: 'userGuide.taskPopover.sendFirstMessage.button',
media: {
type: 'video',
light: chatVideoLight,
dark: chatVideoDark
}
}
}
const TaskPopoverContent: FC<TaskPopoverContentProps> = ({ taskKey, onConfirm }) => {
const { t } = useTranslation()
const { theme } = useTheme()
const content = TASK_CONTENT[taskKey]
const isDark = theme === 'dark'
const renderMedia = () => {
if (!content.media) return null
const mediaSrc = isDark && content.media.dark ? content.media.dark : content.media.light
return (
<MediaContainer>
<LazyMedia type={content.media.type} src={mediaSrc} />
</MediaContainer>
)
}
return (
<PopoverContainer>
<PopoverTitle>{t(content.titleKey)}</PopoverTitle>
{renderMedia()}
<FeatureList>
{content.features.map((featureKey, index) => (
<FeatureItem key={index}>{t(featureKey)}</FeatureItem>
))}
</FeatureList>
<ButtonContainer>
<ConfirmButton onClick={onConfirm}>{t(content.buttonKey)}</ConfirmButton>
</ButtonContainer>
</PopoverContainer>
)
}
const PopoverContainer = styled.div`
width: 280px;
padding: 4px;
`
const PopoverTitle = styled.h3`
font-size: 14px;
font-weight: 700;
color: var(--color-text);
margin: 0 0 12px 0;
line-height: 16px;
`
const MediaContainer = styled.div`
margin-bottom: 12px;
`
const FeatureList = styled.div`
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 12px;
`
const FeatureItem = styled.p`
margin: 0;
color: var(--color-text-2);
font-size: 12px;
font-weight: 400;
line-height: 1.5;
`
const ButtonContainer = styled.div`
display: flex;
justify-content: flex-end;
`
const ConfirmButton = styled.button`
min-width: 80px;
padding: 6px 12px;
background-color: var(--color-primary);
color: white;
border: none;
border-radius: 8px;
font-size: 12px;
font-weight: 500;
line-height: 20px;
cursor: pointer;
transition: background-color 0.2s ease;
&:hover {
opacity: 0.9;
}
`
export default TaskPopoverContent

View File

@ -0,0 +1 @@
export { default as ChecklistContent } from './ChecklistContent'

View File

@ -0,0 +1,122 @@
import styled from 'styled-components'
export const ContentContainer = styled.div`
width: 280px;
padding: 12px 0;
`
export const ChecklistHeader = styled.div`
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 0 16px;
`
export const TitleSection = styled.div`
display: flex;
flex-direction: column;
gap: 6px;
`
export const ChecklistTitle = styled.h3`
font-size: 14px;
font-weight: 700;
color: var(--color-text);
margin: 0;
line-height: 16px;
`
export const ChecklistSubtitle = styled.p`
font-size: 12px;
font-weight: 500;
color: var(--color-text-2);
margin: 0;
line-height: 14px;
`
export const CloseButton = styled.button`
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 4px;
border: none;
background: transparent;
cursor: pointer;
color: var(--color-text-2);
transition: all 0.2s ease;
flex-shrink: 0;
padding: 0;
&:hover {
background: var(--color-background-mute);
color: var(--color-text);
}
`
export const Separator = styled.div`
width: 100%;
height: 1px;
background: var(--color-border);
margin: 12px 0;
`
export const TaskList = styled.div`
display: flex;
flex-direction: column;
gap: 6px;
padding: 0 16px;
`
export const TaskItemContainer = styled.div<{ $completed?: boolean }>`
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
border-radius: 8px;
background: var(--color-white);
cursor: ${(props) => (props.$completed ? 'default' : 'pointer')};
transition: all 0.2s ease;
body[theme-mode='dark'] & {
background: var(--color-background-soft);
}
&:hover {
${(props) =>
!props.$completed &&
`
background: var(--color-background-mute);
`}
}
`
export const Checkbox = styled.div<{ $checked?: boolean }>`
width: 14px;
height: 14px;
border-radius: 50%;
border: 1.5px solid ${(props) => (props.$checked ? '#3cd45a' : 'var(--color-border)')};
background: ${(props) => (props.$checked ? '#3cd45a' : 'var(--color-background)')};
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.2s ease;
`
export const TaskText = styled.span<{ $completed?: boolean }>`
font-size: 13px;
font-weight: 500;
color: ${(props) => (props.$completed ? 'var(--color-text-3)' : 'var(--color-text)')};
text-decoration: ${(props) => (props.$completed ? 'line-through' : 'none')};
line-height: 15px;
`
export const ProgressText = styled.div`
font-size: 11px;
color: var(--color-text-3);
margin-top: 12px;
text-align: center;
padding: 0 16px;
`

View File

@ -0,0 +1,14 @@
import { useAppSelector } from '@renderer/store'
/**
* Hook to track task completion status for user guide
*
* Task completion is now handled via guide completion in OnboardingProvider.
* When a user completes a Driver.js guide, the corresponding task is marked complete.
*
* This hook simply returns the current task status from Redux state.
*/
export function useTaskCompletion() {
const { taskStatus } = useAppSelector((state) => state.onboarding)
return taskStatus
}

View File

@ -0,0 +1,51 @@
import { useAppSelector } from '@renderer/store'
import { useMemo } from 'react'
/**
* Hook to access user guide state and computed values
*/
export function useUserGuide() {
const onboarding = useAppSelector((state) => state.onboarding)
const {
guidePageCompleted,
checklistDismissed,
taskStatus,
completionModalShown,
completedOnboardingVersion,
onboardingSkipped
} = onboarding
const allTasksCompleted = useMemo(
() => taskStatus.useFreeModel && taskStatus.configureProvider && taskStatus.sendFirstMessage,
[taskStatus]
)
const completedTaskCount = useMemo(() => {
return [taskStatus.useFreeModel, taskStatus.configureProvider, taskStatus.sendFirstMessage].filter(Boolean).length
}, [taskStatus])
const shouldShowGuidePage = useMemo(() => {
return !guidePageCompleted && completedOnboardingVersion === null && !onboardingSkipped
}, [guidePageCompleted, completedOnboardingVersion, onboardingSkipped])
const shouldShowChecklist = useMemo(() => {
return guidePageCompleted && !checklistDismissed && !allTasksCompleted
}, [guidePageCompleted, checklistDismissed, allTasksCompleted])
const shouldShowCompletionModal = useMemo(() => {
return allTasksCompleted && !completionModalShown && guidePageCompleted
}, [allTasksCompleted, completionModalShown, guidePageCompleted])
return {
guidePageCompleted,
checklistDismissed,
taskStatus,
completionModalShown,
allTasksCompleted,
completedTaskCount,
shouldShowGuidePage,
shouldShowChecklist,
shouldShowCompletionModal
}
}

View File

@ -0,0 +1,5 @@
export { default as CompletionModal } from './CompletionModal'
export { default as GuidePage } from './GuidePage'
export { useTaskCompletion } from './hooks/useTaskCompletion'
export { useUserGuide } from './hooks/useUserGuide'
export { ChecklistContent } from './UserGuideChecklist'

View File

@ -1,4 +1,5 @@
import EmojiAvatar from '@renderer/components/Avatar/EmojiAvatar'
import { ChecklistContent } from '@renderer/components/UserGuide/UserGuideChecklist'
import { isMac } from '@renderer/config/constant'
import { UserAvatar } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
@ -10,10 +11,13 @@ import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { getSidebarIconLabel, getThemeModeLabel } from '@renderer/i18n/label'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setChecklistVisible } from '@renderer/store/onboarding'
import { ThemeMode } from '@renderer/types'
import { isEmoji } from '@renderer/utils'
import { Avatar, Tooltip } from 'antd'
import { Avatar, Popover, Tooltip } from 'antd'
import {
BadgeCheck,
Code,
FileSearch,
Folder,
@ -29,6 +33,7 @@ import {
Sun
} from 'lucide-react'
import type { FC } from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
@ -41,6 +46,7 @@ const Sidebar: FC = () => {
const { minappShow } = useRuntime()
const { sidebarIcons } = useSettings()
const { pinned } = useMinapps()
const dispatch = useAppDispatch()
const { pathname } = useLocation()
const navigate = useNavigate()
@ -49,6 +55,20 @@ const Sidebar: FC = () => {
const avatar = useAvatar()
const { t } = useTranslation()
// User guide state
const { guidePageCompleted, checklistDismissed, checklistVisible, taskStatus } = useAppSelector(
(state) => state.onboarding
)
const allTasksCompleted = taskStatus.useFreeModel && taskStatus.configureProvider && taskStatus.sendFirstMessage
const showUserGuideButton = guidePageCompleted && !checklistDismissed && !allTasksCompleted
const handleChecklistVisibleChange = useCallback(
(visible: boolean) => {
dispatch(setChecklistVisible(visible))
},
[dispatch]
)
const onEditUser = () => UserPopup.show()
const backgroundColor = useNavBackgroundColor()
@ -89,6 +109,22 @@ const Sidebar: FC = () => {
)}
</MainMenusContainer>
<Menus>
{showUserGuideButton && (
<Popover
content={<ChecklistContent />}
trigger="click"
open={checklistVisible}
onOpenChange={handleChecklistVisibleChange}
placement="rightBottom"
arrow={false}
styles={{ body: { padding: 0 } }}>
<Tooltip title={t('userGuide.checklist.title')} placement="right">
<Icon theme={theme} data-guide-target="user-guide-badge">
<BadgeCheck size={20} className="icon" />
</Icon>
</Tooltip>
</Popover>
)}
<Tooltip title={t('settings.theme.title') + ': ' + getThemeModeLabel(settedTheme)} placement="right">
<Icon theme={theme} onClick={toggleTheme}>
{settedTheme === ThemeMode.dark ? (

View File

@ -1,3 +1,10 @@
import { v170Onboarding } from './v1.7.0-onboarding'
import type { VersionGuide } from '@renderer/types/onboarding'
export const allGuides = [v170Onboarding]
export const allGuides: VersionGuide[] = []
// Export User Guide Step 2 guides
export {
configureProviderGuideStep2,
sendMessageGuideStep2,
useFreeModelGuideStep2
} from './userGuideSteps'

View File

@ -0,0 +1,115 @@
import chatVideoLight from '@renderer/assets/images/guide/chat.mp4'
import chatVideoDark from '@renderer/assets/images/guide/chat_dark.mp4'
import configureProviderStep1Light from '@renderer/assets/images/guide/Configure_Provider_step1.mp4'
import configureProviderStep1Dark from '@renderer/assets/images/guide/Configure_Provider_step1_dark.mp4'
import configureProviderStep2Light from '@renderer/assets/images/guide/Configure_Provider_step2.mp4'
import configureProviderStep2Dark from '@renderer/assets/images/guide/Configure_Provider_step2_dark.mp4'
import freeModelGif from '@renderer/assets/images/guide/free_model.gif'
import type { VersionGuide } from '@renderer/types/onboarding'
/**
* User Guide Step 2: Configure Provider Guide
* Multi-step guide with videos for configuring AI provider
*/
export const configureProviderGuideStep2: VersionGuide = {
version: 'user-guide-configure-provider',
type: 'feature',
titleKey: 'userGuide.guides.configureProvider.title',
descriptionKey: 'userGuide.guides.configureProvider.description',
route: '/settings/provider',
popoverClass: 'user-guide-popover-minimal',
steps: [
{
id: 'enable-cherryin',
element: '[data-guide-target="provider-cherryin"]',
titleKey: 'userGuide.guides.configureProvider.steps.cherryin.title',
descriptionKey: 'userGuide.guides.configureProvider.steps.cherryin.description',
descriptionInterpolation: {
videoUrlLight: configureProviderStep1Light,
videoUrlDark: configureProviderStep1Dark
},
side: 'right',
align: 'start',
nextBtnTextKey: 'userGuide.buttons.next'
},
{
id: 'connect-service',
element: '[data-guide-target="provider-api-key"]',
titleKey: 'userGuide.guides.configureProvider.steps.connect.title',
descriptionKey: 'userGuide.guides.configureProvider.steps.connect.description',
descriptionInterpolation: {
videoUrlLight: configureProviderStep1Light,
videoUrlDark: configureProviderStep1Dark
},
side: 'left',
align: 'start',
nextBtnTextKey: 'userGuide.buttons.next'
},
{
id: 'use-model',
element: '[data-guide-target="provider-manage-models"]',
titleKey: 'userGuide.guides.configureProvider.steps.useModel.title',
descriptionKey: 'userGuide.guides.configureProvider.steps.useModel.description',
descriptionInterpolation: {
videoUrlLight: configureProviderStep2Light,
videoUrlDark: configureProviderStep2Dark
},
side: 'left',
align: 'start',
doneBtnTextKey: 'userGuide.buttons.gotIt'
}
]
}
/**
* User Guide Step 2: Send First Message Guide
* Points to chat input with video and example prompt
*/
export const sendMessageGuideStep2: VersionGuide = {
version: 'user-guide-send-message',
type: 'feature',
titleKey: 'userGuide.guides.sendMessage.title',
descriptionKey: 'userGuide.guides.sendMessage.description',
route: '/',
popoverClass: 'user-guide-popover-minimal',
steps: [
{
id: 'chat-input',
element: '[data-guide-target="chat-input"]',
titleKey: 'userGuide.guides.sendMessage.steps.input.title',
descriptionKey: 'userGuide.guides.sendMessage.steps.input.description',
descriptionInterpolation: {
videoUrlLight: chatVideoLight,
videoUrlDark: chatVideoDark
},
side: 'top',
align: 'center',
doneBtnTextKey: 'userGuide.guides.sendMessage.steps.input.button'
}
]
}
/**
* User Guide Step 2: Use Free Model Guide
* Points to the model selector with GIF image
*/
export const useFreeModelGuideStep2: VersionGuide = {
version: 'user-guide-use-free-model',
type: 'feature',
titleKey: 'userGuide.guides.useFreeModel.title',
descriptionKey: 'userGuide.guides.useFreeModel.description',
route: '/',
popoverClass: 'user-guide-popover-minimal',
steps: [
{
id: 'model-selector',
element: '[data-guide-target="model-selector"]',
titleKey: 'userGuide.guides.useFreeModel.steps.selector.title',
descriptionKey: 'userGuide.guides.useFreeModel.steps.selector.description',
descriptionInterpolation: { imageUrl: freeModelGif },
side: 'bottom',
align: 'start',
doneBtnTextKey: 'userGuide.buttons.gotIt'
}
]
}

View File

@ -1,79 +0,0 @@
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'
}
]
}

View File

@ -5352,6 +5352,127 @@
"saveDataError": "Failed to save data, please try again.",
"title": "Update"
},
"userGuide": {
"buttons": {
"gotIt": "Got it",
"next": "Next",
"useNow": "Use Now"
},
"checklist": {
"progress": "{{completed}} of {{total}} completed",
"subtitle": "Complete tasks to unlock Cherry Studio advanced tips",
"tasks": {
"configureProvider": "Configure your first AI provider",
"sendFirstMessage": "Send your first message",
"useFreeModel": "Try the free built-in model"
},
"title": "Start Your AI Journey"
},
"completionModal": {
"exploreMore": "Explore More Assistants",
"recommendedAssistants": "Recommended Assistants",
"startChat": "Start Chatting",
"subtitle": "Welcome to Cherry Studio. You're all set to explore!",
"title": "Setup Complete!"
},
"guidePage": {
"carousel": {
"assistants": {
"description": "From project management to chart drawing, define dedicated system prompts to build your professional AI team.",
"title": "AI Assistants for Every Task"
},
"models": {
"description": "One-stop management of OpenAI, Claude, DeepSeek and many other model services. Free switching, local storage, giving the choice back to you.",
"title": "World-Class AI Models"
},
"paintings": {
"description": "Integrate multiple mainstream drawing models with fine-grained parameter control. Whether it's art creation or asset generation, it's all at your fingertips.",
"title": "Unlimited Creativity"
}
},
"navStyle": {
"left": "Left",
"title": "Navigation",
"top": "Top"
},
"startButton": "Get Started",
"welcome": {
"subtitle": "You can change this later in system settings",
"title": "Choose Your Page Style"
}
},
"guides": {
"configureProvider": {
"description": "Set up your AI provider to get started",
"steps": {
"cherryin": {
"description": "<div class=\"guide-video\" data-video-light=\"{{videoUrlLight}}\" data-video-dark=\"{{videoUrlDark}}\"></div><div class=\"guide-title\">🍒 Enable CherryIN Service</div><div class=\"guide-features\"><p>🎟️ Official aggregation — One Key to access multiple mainstream LLMs</p><p>🆓 Built-in free resources — Add free models with one click after account verification</p><p>🚀 Stable & fast — High-availability API nodes maintained officially</p><p><em>⚠️ Account balance required to activate service. Free models available after activation.</em></p></div>",
"title": ""
},
"connect": {
"description": "<div class=\"guide-video\" data-video-light=\"{{videoUrlLight}}\" data-video-dark=\"{{videoUrlDark}}\"></div><div class=\"guide-title\">🔑 Step 1: Connect Service</div><div class=\"guide-features\"><p>1. Click the link to get your CherryIN API key.</p><p>2. Paste the key and click the \"Check\" button on the right.</p></div>",
"title": ""
},
"useModel": {
"description": "<div class=\"guide-video\" data-video-light=\"{{videoUrlLight}}\" data-video-dark=\"{{videoUrlDark}}\"></div><div class=\"guide-title\">🎁 Step 2: Use Models</div><div class=\"guide-features\"><p>After key verification, click \"Manage\" button and select the free or commercial models you want to use!</p></div>",
"title": ""
}
},
"title": "Configure Provider"
},
"sendMessage": {
"description": "Start chatting with AI",
"steps": {
"input": {
"button": "Start Now",
"description": "<div class=\"guide-video\" data-video-light=\"{{videoUrlLight}}\" data-video-dark=\"{{videoUrlDark}}\"></div><div class=\"guide-title\">💬 Start Chatting</div><div class=\"guide-features\"><p>Try asking me: \"You are not just an AI, but...\" and see how I respond?</p></div>",
"title": ""
}
},
"title": "Chat with AI"
},
"useFreeModel": {
"description": "Try our free built-in model",
"steps": {
"selector": {
"description": "<div class=\"guide-image\"><img src=\"{{imageUrl}}\" alt=\"\" /></div><div class=\"guide-title\">👆 Use Free Model Here</div><div class=\"guide-features\"><p>Click the dropdown menu and select GLM-4-Flash (Free) to start chatting for free.</p></div>",
"shortDescription": "Click here to select a free model",
"title": ""
}
},
"title": "Use Free Model"
}
},
"taskPopover": {
"configureProvider": {
"button": "Go to Settings",
"features": {
"localModels": "🖥 Local model support — One-click Ollama connection, run AI offline",
"privacyFirst": "🔐 Privacy first — API Keys stored locally, direct connection to official services",
"unlockModels": "💎 Unlock top models — Access Gemini 3, Claude 4.5, DeepSeek and more"
},
"title": "Configure Your First AI Provider"
},
"sendFirstMessage": {
"button": "Start Chatting",
"features": {
"fileAnalysis": "📎 File analysis — supports PDF/Word",
"markdown": "✨ Markdown rendering — perfect code highlighting",
"typewriter": "🎨 Typewriter effect — smooth experience"
},
"title": "Send Your First Message"
},
"useFreeModel": {
"button": "Use Now",
"features": {
"allInOne": "📝 All-in-one assistant — for Q&A, translation and coding",
"fastResponse": "⚡️ Fast response — millisecond-level latency",
"noRegistration": "🟢 No registration required — built-in free computing power"
},
"title": "Use Free Model"
}
}
},
"warning": {
"missing_provider": "The supplier does not exist; reverted to the default supplier {{provider}}. This may cause issues."
},

View File

@ -5352,6 +5352,127 @@
"saveDataError": "保存数据失败,请重试",
"title": "更新提示"
},
"userGuide": {
"buttons": {
"gotIt": "知道了",
"next": "下一步",
"useNow": "立即使用"
},
"checklist": {
"progress": "已完成 {{completed}}/{{total}}",
"subtitle": "完成任务,解锁 Cherry Studio 进阶使用技巧",
"tasks": {
"configureProvider": "配置第一个模型服务",
"sendFirstMessage": "发送第一条消息",
"useFreeModel": "使用免费模型"
},
"title": "开启您的 AI 探索之旅"
},
"completionModal": {
"exploreMore": "探索更多助手",
"recommendedAssistants": "推荐助手",
"startChat": "开始对话",
"subtitle": "欢迎来到 Cherry Studio您已准备就绪",
"title": "配置完成!"
},
"guidePage": {
"carousel": {
"assistants": {
"description": "从项目管理到图表绘制,定义专属系统提示词,打造属于您的专业 AI 团队。",
"title": "场景化 AI 助手,各司其职"
},
"models": {
"description": "一站式管理 OpenAI、Claude、DeepSeek 等数多种模型服务,自由切换,本地存储,将选择权交还给您。",
"title": "集结全球顶尖模型"
},
"paintings": {
"description": "集成多种主流绘图模型,精细化参数控制。无论是艺术创作还是素材生成,皆可信手拈来。",
"title": "无限创意,绘你所想"
}
},
"navStyle": {
"left": "左侧",
"title": "导航栏",
"top": "顶部"
},
"startButton": "立即使用",
"welcome": {
"subtitle": "之后可以通过系统设置来更换",
"title": "请选择您的页面样式"
}
},
"guides": {
"configureProvider": {
"description": "设置您的 AI 服务商开始使用",
"steps": {
"cherryin": {
"description": "<div class=\"guide-video\" data-video-light=\"{{videoUrlLight}}\" data-video-dark=\"{{videoUrlDark}}\"></div><div class=\"guide-title\">🍒 开启 CherryIN 服务</div><div class=\"guide-features\"><p>🎟️ 官方聚合通道 —— 一个 Key 畅享多种主流大模型</p><p>🆓 内置免费资源 —— 验证账户后,可一键添加多种免费模型</p><p>🚀 稳定高速 —— 官方维护的高可用 API 节点</p><p><em>⚠️ 需账户内有余额才可激活服务,激活后即可使用免费模型。</em></p></div>",
"title": ""
},
"connect": {
"description": "<div class=\"guide-video\" data-video-light=\"{{videoUrlLight}}\" data-video-dark=\"{{videoUrlDark}}\"></div><div class=\"guide-title\">🔑 第一步:连接服务</div><div class=\"guide-features\"><p>1. 点击链接获取您的 CherryIN 密钥。</p><p>2. 粘贴密钥并点击右侧的「检测」按钮。</p></div>",
"title": ""
},
"useModel": {
"description": "<div class=\"guide-video\" data-video-light=\"{{videoUrlLight}}\" data-video-dark=\"{{videoUrlDark}}\"></div><div class=\"guide-title\">🎁 第二步:使用模型</div><div class=\"guide-features\"><p>密钥验证成功后,点击「管理」按钮,在列表中勾选您想使用的免费模型或商用模型即可!</p></div>",
"title": ""
}
},
"title": "配置服务商"
},
"sendMessage": {
"description": "开始与 AI 对话",
"steps": {
"input": {
"button": "立即开始",
"description": "<div class=\"guide-video\" data-video-light=\"{{videoUrlLight}}\" data-video-dark=\"{{videoUrlDark}}\"></div><div class=\"guide-title\">💬 开始对话</div><div class=\"guide-features\"><p>试着问问我「你不仅是一个AI还是...」,看看我会怎么回?</p></div>",
"title": ""
}
},
"title": "与 AI 对话"
},
"useFreeModel": {
"description": "试用内置免费模型",
"steps": {
"selector": {
"description": "<div class=\"guide-image\"><img src=\"{{imageUrl}}\" alt=\"\" /></div><div class=\"guide-title\">👆 使用免费模型在这里</div><div class=\"guide-features\"><p>点击下拉菜单,选择 GLM-4-Flash (Free) 即可开始免费对话。</p></div>",
"shortDescription": "点击这里选择免费模型",
"title": ""
}
},
"title": "使用免费模型"
}
},
"taskPopover": {
"configureProvider": {
"button": "前往设置",
"features": {
"localModels": "🖥 本地模型支持——一键连接 Ollama离线也能跑 AI",
"privacyFirst": "🔐 隐私优先——API Key 仅存本地,直连官方服务",
"unlockModels": "💎 解锁最强模型——接入 Gemini 3, Claude 4.5, DeepSeek 等"
},
"title": "配置第一个模型服务"
},
"sendFirstMessage": {
"button": "开始对话",
"features": {
"fileAnalysis": "📎 文件分析 —— 支持 PDF/Word",
"markdown": "✨ Markdown 渲染 —— 完美支持代码高亮",
"typewriter": "🎨 打字机特效 —— 享受流畅体验"
},
"title": "发送第一条消息"
},
"useFreeModel": {
"button": "立即使用",
"features": {
"allInOne": "📝 全能助手 —— 满足日常问答、翻译与代码需求",
"fastResponse": "⚡️ 高速响应 —— 毫秒级延迟,无需等待",
"noRegistration": "🟢 无需注册,开箱即用 —— 内置免费算力"
},
"title": "使用免费模型"
}
}
},
"warning": {
"missing_provider": "供应商不存在,已回退到默认供应商 {{provider}}。这可能导致问题。"
},

View File

@ -5352,6 +5352,127 @@
"saveDataError": "儲存資料失敗,請重試",
"title": "更新提示"
},
"userGuide": {
"buttons": {
"gotIt": "知道了",
"next": "下一步",
"useNow": "立即使用"
},
"checklist": {
"progress": "已完成 {{completed}}/{{total}}",
"subtitle": "完成任務,解鎖 Cherry Studio 進階使用技巧",
"tasks": {
"configureProvider": "配置第一個模型服務",
"sendFirstMessage": "發送第一條訊息",
"useFreeModel": "使用免費模型"
},
"title": "開啟您的 AI 探索之旅"
},
"completionModal": {
"exploreMore": "探索更多助手",
"recommendedAssistants": "推薦助手",
"startChat": "開始對話",
"subtitle": "歡迎來到 Cherry Studio您已準備就緒",
"title": "配置完成!"
},
"guidePage": {
"carousel": {
"assistants": {
"description": "從項目管理到圖表繪製,定義專屬系統提示詞,打造屬於您的專業 AI 團隊。",
"title": "場景化 AI 助手,各司其職"
},
"models": {
"description": "一站式管理 OpenAI、Claude、DeepSeek 等眾多模型服務,自由切換,本地存儲,將選擇權交還給您。",
"title": "集結全球頂尖模型"
},
"paintings": {
"description": "整合多種主流繪圖模型,精細化參數控制。無論是藝術創作還是素材生成,皆可信手拈來。",
"title": "無限創意,繪你所想"
}
},
"navStyle": {
"left": "左側",
"title": "導航列",
"top": "頂部"
},
"startButton": "立即使用",
"welcome": {
"subtitle": "之後可以通過系統設定來更換",
"title": "請選擇您的頁面樣式"
}
},
"guides": {
"configureProvider": {
"description": "設定您的 AI 服務商開始使用",
"steps": {
"cherryin": {
"description": "<div class=\"guide-video\" data-video-light=\"{{videoUrlLight}}\" data-video-dark=\"{{videoUrlDark}}\"></div><div class=\"guide-title\">🍒 開啟 CherryIN 服務</div><div class=\"guide-features\"><p>🎟️ 官方聚合通道 —— 一個 Key 暢享多種主流大模型</p><p>🆓 內建免費資源 —— 驗證帳戶後,可一鍵添加多種免費模型</p><p>🚀 穩定高速 —— 官方維護的高可用 API 節點</p><p><em>⚠️ 需帳戶內有餘額才可啟用服務,啟用後即可使用免費模型。</em></p></div>",
"title": ""
},
"connect": {
"description": "<div class=\"guide-video\" data-video-light=\"{{videoUrlLight}}\" data-video-dark=\"{{videoUrlDark}}\"></div><div class=\"guide-title\">🔑 第一步:連接服務</div><div class=\"guide-features\"><p>1. 點擊連結取得您的 CherryIN 密鑰。</p><p>2. 貼上密鑰並點擊右側的「檢測」按鈕。</p></div>",
"title": ""
},
"useModel": {
"description": "<div class=\"guide-video\" data-video-light=\"{{videoUrlLight}}\" data-video-dark=\"{{videoUrlDark}}\"></div><div class=\"guide-title\">🎁 第二步:使用模型</div><div class=\"guide-features\"><p>密鑰驗證成功後,點擊「管理」按鈕,在列表中勾選您想使用的免費模型或商用模型即可!</p></div>",
"title": ""
}
},
"title": "配置服務商"
},
"sendMessage": {
"description": "[to be translated]:Start chatting with AI",
"steps": {
"input": {
"description": "[to be translated]:Type your message here and press Enter to send. Cherry Studio supports Markdown, code highlighting, and file attachments.",
"shortDescription": "[to be translated]:Type your message here and send",
"title": "[to be translated]:Send Your First Message"
}
},
"title": "[to be translated]:Chat with AI"
},
"useFreeModel": {
"description": "試用內建免費模型",
"steps": {
"selector": {
"description": "<div class=\"guide-image\"><img src=\"{{imageUrl}}\" alt=\"\" /></div><div class=\"guide-title\">👆 使用免費模型在這裡</div><div class=\"guide-features\"><p>點擊下拉選單,選擇 GLM-4-Flash (Free) 即可開始免費對話。</p></div>",
"shortDescription": "點擊這裡選擇免費模型",
"title": ""
}
},
"title": "使用免費模型"
}
},
"taskPopover": {
"configureProvider": {
"button": "前往設定",
"features": {
"localModels": "🖥 本地模型支援——一鍵連接 Ollama離線也能跑 AI",
"privacyFirst": "🔐 隱私優先——API Key 僅存本地,直連官方服務",
"unlockModels": "💎 解鎖最強模型——接入 Gemini 3, Claude 4.5, DeepSeek 等"
},
"title": "配置第一個模型服務"
},
"sendFirstMessage": {
"button": "開始對話",
"features": {
"fileAnalysis": "📎 檔案分析 —— 支援 PDF/Word",
"markdown": "✨ Markdown 渲染 —— 完美支援程式碼高亮",
"typewriter": "🎨 打字機特效 —— 享受流暢體驗"
},
"title": "發送第一條訊息"
},
"useFreeModel": {
"button": "立即使用",
"features": {
"allInOne": "📝 全能助手 —— 滿足日常問答、翻譯與程式碼需求",
"fastResponse": "⚡️ 高速響應 —— 毫秒級延遲,無需等待",
"noRegistration": "🟢 無需註冊,開箱即用 —— 內建免費算力"
},
"title": "使用免費模型"
}
}
},
"warning": {
"missing_provider": "供應商不存在,已改用預設供應商 {{provider}}。這可能導致問題。"
},

View File

@ -5352,6 +5352,119 @@
"saveDataError": "Speichern fehlgeschlagen, bitte erneut versuchen",
"title": "Update-Hinweis"
},
"userGuide": {
"buttons": {
"gotIt": "[to be translated]:Got it",
"useNow": "[to be translated]:Use Now"
},
"checklist": {
"progress": "[to be translated]:{{completed}} of {{total}} completed",
"subtitle": "[to be translated]:Complete tasks to unlock Cherry Studio advanced tips",
"tasks": {
"configureProvider": "[to be translated]:Configure your first AI provider",
"sendFirstMessage": "[to be translated]:Send your first message",
"useFreeModel": "[to be translated]:Try the free built-in model"
},
"title": "[to be translated]:Start Your AI Journey"
},
"completionModal": {
"exploreMore": "[to be translated]:Explore More Assistants",
"recommendedAssistants": "[to be translated]:Recommended Assistants",
"startChat": "[to be translated]:Start Chatting",
"subtitle": "[to be translated]:Welcome to Cherry Studio. You're all set to explore!",
"title": "[to be translated]:Setup Complete!"
},
"guidePage": {
"carousel": {
"assistants": {
"description": "[to be translated]:From project management to chart drawing, define dedicated system prompts to build your professional AI team.",
"title": "[to be translated]:AI Assistants for Every Task"
},
"models": {
"description": "[to be translated]:One-stop management of OpenAI, Claude, DeepSeek and many other model services. Free switching, local storage, giving the choice back to you.",
"title": "[to be translated]:World-Class AI Models"
},
"paintings": {
"description": "[to be translated]:Integrate multiple mainstream drawing models with fine-grained parameter control. Whether it's art creation or asset generation, it's all at your fingertips.",
"title": "[to be translated]:Unlimited Creativity"
}
},
"navStyle": {
"left": "[to be translated]:Sidebar",
"title": "[to be translated]:Navigation Style",
"top": "[to be translated]:Top Navbar"
},
"startButton": "[to be translated]:Get Started",
"welcome": {
"subtitle": "[to be translated]:Your powerful AI desktop client. Let's set up your preferences before getting started.",
"title": "[to be translated]:Welcome to Cherry Studio"
}
},
"guides": {
"configureProvider": {
"description": "[to be translated]:Set up your AI provider to get started",
"steps": {
"apiKey": {
"description": "[to be translated]:Enter your API key and click the check button to verify the connection.",
"shortDescription": "[to be translated]:Enter your API key here",
"title": "[to be translated]:Connect Your AI Service"
}
},
"title": "[to be translated]:Configure Provider"
},
"sendMessage": {
"description": "[to be translated]:Start chatting with AI",
"steps": {
"input": {
"description": "[to be translated]:Type your message here and press Enter to send. Cherry Studio supports Markdown, code highlighting, and file attachments.",
"shortDescription": "[to be translated]:Type your message here and send",
"title": "[to be translated]:Send Your First Message"
}
},
"title": "[to be translated]:Chat with AI"
},
"useFreeModel": {
"description": "[to be translated]:Try our free built-in model",
"steps": {
"selector": {
"description": "[to be translated]:Click here to select a model. Try the free built-in model to get started without an API key.",
"shortDescription": "[to be translated]:Click here to select a free model",
"title": "[to be translated]:Select a Model"
}
},
"title": "[to be translated]:Use Free Model"
}
},
"taskPopover": {
"configureProvider": {
"button": "[to be translated]:Start Setup",
"features": {
"connectApi": "[to be translated]:🔑 Connect your API service",
"multiProvider": "[to be translated]:⚙️ Support multiple providers",
"secureStorage": "[to be translated]:🔒 Keys stored securely on your device"
},
"title": "[to be translated]:Configure AI Provider"
},
"sendFirstMessage": {
"button": "[to be translated]:Start Chatting",
"features": {
"fileAnalysis": "[to be translated]:📎 File analysis — supports PDF/Word",
"markdown": "[to be translated]:✨ Markdown rendering — perfect code highlighting",
"typewriter": "[to be translated]:🎨 Typewriter effect — smooth experience"
},
"title": "[to be translated]:Send Your First Message"
},
"useFreeModel": {
"button": "[to be translated]:Use Now",
"features": {
"allInOne": "[to be translated]:📝 All-in-one assistant — for Q&A, translation and coding",
"fastResponse": "[to be translated]:⚡️ Fast response — millisecond-level latency",
"noRegistration": "[to be translated]:🟢 No registration required — built-in free computing power"
},
"title": "[to be translated]:Use Free Model"
}
}
},
"warning": {
"missing_provider": "Anbieter nicht gefunden, Standardanbieter {{provider}} verwendet. Dies kann zu Problemen führen."
},

View File

@ -5352,6 +5352,119 @@
"saveDataError": "Η αποθήκευση των δεδομένων απέτυχε, δοκιμάστε ξανά",
"title": "Ενημέρωση"
},
"userGuide": {
"buttons": {
"gotIt": "[to be translated]:Got it",
"useNow": "[to be translated]:Use Now"
},
"checklist": {
"progress": "[to be translated]:{{completed}} of {{total}} completed",
"subtitle": "[to be translated]:Complete tasks to unlock Cherry Studio advanced tips",
"tasks": {
"configureProvider": "[to be translated]:Configure your first AI provider",
"sendFirstMessage": "[to be translated]:Send your first message",
"useFreeModel": "[to be translated]:Try the free built-in model"
},
"title": "[to be translated]:Start Your AI Journey"
},
"completionModal": {
"exploreMore": "[to be translated]:Explore More Assistants",
"recommendedAssistants": "[to be translated]:Recommended Assistants",
"startChat": "[to be translated]:Start Chatting",
"subtitle": "[to be translated]:Welcome to Cherry Studio. You're all set to explore!",
"title": "[to be translated]:Setup Complete!"
},
"guidePage": {
"carousel": {
"assistants": {
"description": "[to be translated]:From project management to chart drawing, define dedicated system prompts to build your professional AI team.",
"title": "[to be translated]:AI Assistants for Every Task"
},
"models": {
"description": "[to be translated]:One-stop management of OpenAI, Claude, DeepSeek and many other model services. Free switching, local storage, giving the choice back to you.",
"title": "[to be translated]:World-Class AI Models"
},
"paintings": {
"description": "[to be translated]:Integrate multiple mainstream drawing models with fine-grained parameter control. Whether it's art creation or asset generation, it's all at your fingertips.",
"title": "[to be translated]:Unlimited Creativity"
}
},
"navStyle": {
"left": "[to be translated]:Sidebar",
"title": "[to be translated]:Navigation Style",
"top": "[to be translated]:Top Navbar"
},
"startButton": "[to be translated]:Get Started",
"welcome": {
"subtitle": "[to be translated]:Your powerful AI desktop client. Let's set up your preferences before getting started.",
"title": "[to be translated]:Welcome to Cherry Studio"
}
},
"guides": {
"configureProvider": {
"description": "[to be translated]:Set up your AI provider to get started",
"steps": {
"apiKey": {
"description": "[to be translated]:Enter your API key and click the check button to verify the connection.",
"shortDescription": "[to be translated]:Enter your API key here",
"title": "[to be translated]:Connect Your AI Service"
}
},
"title": "[to be translated]:Configure Provider"
},
"sendMessage": {
"description": "[to be translated]:Start chatting with AI",
"steps": {
"input": {
"description": "[to be translated]:Type your message here and press Enter to send. Cherry Studio supports Markdown, code highlighting, and file attachments.",
"shortDescription": "[to be translated]:Type your message here and send",
"title": "[to be translated]:Send Your First Message"
}
},
"title": "[to be translated]:Chat with AI"
},
"useFreeModel": {
"description": "[to be translated]:Try our free built-in model",
"steps": {
"selector": {
"description": "[to be translated]:Click here to select a model. Try the free built-in model to get started without an API key.",
"shortDescription": "[to be translated]:Click here to select a free model",
"title": "[to be translated]:Select a Model"
}
},
"title": "[to be translated]:Use Free Model"
}
},
"taskPopover": {
"configureProvider": {
"button": "[to be translated]:Start Setup",
"features": {
"connectApi": "[to be translated]:🔑 Connect your API service",
"multiProvider": "[to be translated]:⚙️ Support multiple providers",
"secureStorage": "[to be translated]:🔒 Keys stored securely on your device"
},
"title": "[to be translated]:Configure AI Provider"
},
"sendFirstMessage": {
"button": "[to be translated]:Start Chatting",
"features": {
"fileAnalysis": "[to be translated]:📎 File analysis — supports PDF/Word",
"markdown": "[to be translated]:✨ Markdown rendering — perfect code highlighting",
"typewriter": "[to be translated]:🎨 Typewriter effect — smooth experience"
},
"title": "[to be translated]:Send Your First Message"
},
"useFreeModel": {
"button": "[to be translated]:Use Now",
"features": {
"allInOne": "[to be translated]:📝 All-in-one assistant — for Q&A, translation and coding",
"fastResponse": "[to be translated]:⚡️ Fast response — millisecond-level latency",
"noRegistration": "[to be translated]:🟢 No registration required — built-in free computing power"
},
"title": "[to be translated]:Use Free Model"
}
}
},
"warning": {
"missing_provider": "Ο προμηθευτής δεν υπάρχει, έγινε επαναφορά στον προεπιλεγμένο προμηθευτή {{provider}}. Αυτό μπορεί να προκαλέσει προβλήματα."
},

View File

@ -5352,6 +5352,119 @@
"saveDataError": "Error al guardar los datos, inténtalo de nuevo",
"title": "Actualización"
},
"userGuide": {
"buttons": {
"gotIt": "[to be translated]:Got it",
"useNow": "[to be translated]:Use Now"
},
"checklist": {
"progress": "[to be translated]:{{completed}} of {{total}} completed",
"subtitle": "[to be translated]:Complete tasks to unlock Cherry Studio advanced tips",
"tasks": {
"configureProvider": "[to be translated]:Configure your first AI provider",
"sendFirstMessage": "[to be translated]:Send your first message",
"useFreeModel": "[to be translated]:Try the free built-in model"
},
"title": "[to be translated]:Start Your AI Journey"
},
"completionModal": {
"exploreMore": "[to be translated]:Explore More Assistants",
"recommendedAssistants": "[to be translated]:Recommended Assistants",
"startChat": "[to be translated]:Start Chatting",
"subtitle": "[to be translated]:Welcome to Cherry Studio. You're all set to explore!",
"title": "[to be translated]:Setup Complete!"
},
"guidePage": {
"carousel": {
"assistants": {
"description": "[to be translated]:From project management to chart drawing, define dedicated system prompts to build your professional AI team.",
"title": "[to be translated]:AI Assistants for Every Task"
},
"models": {
"description": "[to be translated]:One-stop management of OpenAI, Claude, DeepSeek and many other model services. Free switching, local storage, giving the choice back to you.",
"title": "[to be translated]:World-Class AI Models"
},
"paintings": {
"description": "[to be translated]:Integrate multiple mainstream drawing models with fine-grained parameter control. Whether it's art creation or asset generation, it's all at your fingertips.",
"title": "[to be translated]:Unlimited Creativity"
}
},
"navStyle": {
"left": "[to be translated]:Sidebar",
"title": "[to be translated]:Navigation Style",
"top": "[to be translated]:Top Navbar"
},
"startButton": "[to be translated]:Get Started",
"welcome": {
"subtitle": "[to be translated]:Your powerful AI desktop client. Let's set up your preferences before getting started.",
"title": "[to be translated]:Welcome to Cherry Studio"
}
},
"guides": {
"configureProvider": {
"description": "[to be translated]:Set up your AI provider to get started",
"steps": {
"apiKey": {
"description": "[to be translated]:Enter your API key and click the check button to verify the connection.",
"shortDescription": "[to be translated]:Enter your API key here",
"title": "[to be translated]:Connect Your AI Service"
}
},
"title": "[to be translated]:Configure Provider"
},
"sendMessage": {
"description": "[to be translated]:Start chatting with AI",
"steps": {
"input": {
"description": "[to be translated]:Type your message here and press Enter to send. Cherry Studio supports Markdown, code highlighting, and file attachments.",
"shortDescription": "[to be translated]:Type your message here and send",
"title": "[to be translated]:Send Your First Message"
}
},
"title": "[to be translated]:Chat with AI"
},
"useFreeModel": {
"description": "[to be translated]:Try our free built-in model",
"steps": {
"selector": {
"description": "[to be translated]:Click here to select a model. Try the free built-in model to get started without an API key.",
"shortDescription": "[to be translated]:Click here to select a free model",
"title": "[to be translated]:Select a Model"
}
},
"title": "[to be translated]:Use Free Model"
}
},
"taskPopover": {
"configureProvider": {
"button": "[to be translated]:Start Setup",
"features": {
"connectApi": "[to be translated]:🔑 Connect your API service",
"multiProvider": "[to be translated]:⚙️ Support multiple providers",
"secureStorage": "[to be translated]:🔒 Keys stored securely on your device"
},
"title": "[to be translated]:Configure AI Provider"
},
"sendFirstMessage": {
"button": "[to be translated]:Start Chatting",
"features": {
"fileAnalysis": "[to be translated]:📎 File analysis — supports PDF/Word",
"markdown": "[to be translated]:✨ Markdown rendering — perfect code highlighting",
"typewriter": "[to be translated]:🎨 Typewriter effect — smooth experience"
},
"title": "[to be translated]:Send Your First Message"
},
"useFreeModel": {
"button": "[to be translated]:Use Now",
"features": {
"allInOne": "[to be translated]:📝 All-in-one assistant — for Q&A, translation and coding",
"fastResponse": "[to be translated]:⚡️ Fast response — millisecond-level latency",
"noRegistration": "[to be translated]:🟢 No registration required — built-in free computing power"
},
"title": "[to be translated]:Use Free Model"
}
}
},
"warning": {
"missing_provider": "El proveedor no existe, se ha revertido al proveedor predeterminado {{provider}}. Esto podría causar problemas."
},

View File

@ -5352,6 +5352,119 @@
"saveDataError": "Échec de la sauvegarde des données, veuillez réessayer",
"title": "Mise à jour"
},
"userGuide": {
"buttons": {
"gotIt": "[to be translated]:Got it",
"useNow": "[to be translated]:Use Now"
},
"checklist": {
"progress": "[to be translated]:{{completed}} of {{total}} completed",
"subtitle": "[to be translated]:Complete tasks to unlock Cherry Studio advanced tips",
"tasks": {
"configureProvider": "[to be translated]:Configure your first AI provider",
"sendFirstMessage": "[to be translated]:Send your first message",
"useFreeModel": "[to be translated]:Try the free built-in model"
},
"title": "[to be translated]:Start Your AI Journey"
},
"completionModal": {
"exploreMore": "[to be translated]:Explore More Assistants",
"recommendedAssistants": "[to be translated]:Recommended Assistants",
"startChat": "[to be translated]:Start Chatting",
"subtitle": "[to be translated]:Welcome to Cherry Studio. You're all set to explore!",
"title": "[to be translated]:Setup Complete!"
},
"guidePage": {
"carousel": {
"assistants": {
"description": "[to be translated]:From project management to chart drawing, define dedicated system prompts to build your professional AI team.",
"title": "[to be translated]:AI Assistants for Every Task"
},
"models": {
"description": "[to be translated]:One-stop management of OpenAI, Claude, DeepSeek and many other model services. Free switching, local storage, giving the choice back to you.",
"title": "[to be translated]:World-Class AI Models"
},
"paintings": {
"description": "[to be translated]:Integrate multiple mainstream drawing models with fine-grained parameter control. Whether it's art creation or asset generation, it's all at your fingertips.",
"title": "[to be translated]:Unlimited Creativity"
}
},
"navStyle": {
"left": "[to be translated]:Sidebar",
"title": "[to be translated]:Navigation Style",
"top": "[to be translated]:Top Navbar"
},
"startButton": "[to be translated]:Get Started",
"welcome": {
"subtitle": "[to be translated]:Your powerful AI desktop client. Let's set up your preferences before getting started.",
"title": "[to be translated]:Welcome to Cherry Studio"
}
},
"guides": {
"configureProvider": {
"description": "[to be translated]:Set up your AI provider to get started",
"steps": {
"apiKey": {
"description": "[to be translated]:Enter your API key and click the check button to verify the connection.",
"shortDescription": "[to be translated]:Enter your API key here",
"title": "[to be translated]:Connect Your AI Service"
}
},
"title": "[to be translated]:Configure Provider"
},
"sendMessage": {
"description": "[to be translated]:Start chatting with AI",
"steps": {
"input": {
"description": "[to be translated]:Type your message here and press Enter to send. Cherry Studio supports Markdown, code highlighting, and file attachments.",
"shortDescription": "[to be translated]:Type your message here and send",
"title": "[to be translated]:Send Your First Message"
}
},
"title": "[to be translated]:Chat with AI"
},
"useFreeModel": {
"description": "[to be translated]:Try our free built-in model",
"steps": {
"selector": {
"description": "[to be translated]:Click here to select a model. Try the free built-in model to get started without an API key.",
"shortDescription": "[to be translated]:Click here to select a free model",
"title": "[to be translated]:Select a Model"
}
},
"title": "[to be translated]:Use Free Model"
}
},
"taskPopover": {
"configureProvider": {
"button": "[to be translated]:Start Setup",
"features": {
"connectApi": "[to be translated]:🔑 Connect your API service",
"multiProvider": "[to be translated]:⚙️ Support multiple providers",
"secureStorage": "[to be translated]:🔒 Keys stored securely on your device"
},
"title": "[to be translated]:Configure AI Provider"
},
"sendFirstMessage": {
"button": "[to be translated]:Start Chatting",
"features": {
"fileAnalysis": "[to be translated]:📎 File analysis — supports PDF/Word",
"markdown": "[to be translated]:✨ Markdown rendering — perfect code highlighting",
"typewriter": "[to be translated]:🎨 Typewriter effect — smooth experience"
},
"title": "[to be translated]:Send Your First Message"
},
"useFreeModel": {
"button": "[to be translated]:Use Now",
"features": {
"allInOne": "[to be translated]:📝 All-in-one assistant — for Q&A, translation and coding",
"fastResponse": "[to be translated]:⚡️ Fast response — millisecond-level latency",
"noRegistration": "[to be translated]:🟢 No registration required — built-in free computing power"
},
"title": "[to be translated]:Use Free Model"
}
}
},
"warning": {
"missing_provider": "Le fournisseur nexiste pas, retour au fournisseur par défaut {{provider}}. Cela peut entraîner des problèmes."
},

View File

@ -5352,6 +5352,119 @@
"saveDataError": "データの保存に失敗しました。もう一度お試しください。",
"title": "更新"
},
"userGuide": {
"buttons": {
"gotIt": "[to be translated]:Got it",
"useNow": "[to be translated]:Use Now"
},
"checklist": {
"progress": "[to be translated]:{{completed}} of {{total}} completed",
"subtitle": "[to be translated]:Complete tasks to unlock Cherry Studio advanced tips",
"tasks": {
"configureProvider": "[to be translated]:Configure your first AI provider",
"sendFirstMessage": "[to be translated]:Send your first message",
"useFreeModel": "[to be translated]:Try the free built-in model"
},
"title": "[to be translated]:Start Your AI Journey"
},
"completionModal": {
"exploreMore": "[to be translated]:Explore More Assistants",
"recommendedAssistants": "[to be translated]:Recommended Assistants",
"startChat": "[to be translated]:Start Chatting",
"subtitle": "[to be translated]:Welcome to Cherry Studio. You're all set to explore!",
"title": "[to be translated]:Setup Complete!"
},
"guidePage": {
"carousel": {
"assistants": {
"description": "[to be translated]:From project management to chart drawing, define dedicated system prompts to build your professional AI team.",
"title": "[to be translated]:AI Assistants for Every Task"
},
"models": {
"description": "[to be translated]:One-stop management of OpenAI, Claude, DeepSeek and many other model services. Free switching, local storage, giving the choice back to you.",
"title": "[to be translated]:World-Class AI Models"
},
"paintings": {
"description": "[to be translated]:Integrate multiple mainstream drawing models with fine-grained parameter control. Whether it's art creation or asset generation, it's all at your fingertips.",
"title": "[to be translated]:Unlimited Creativity"
}
},
"navStyle": {
"left": "[to be translated]:Sidebar",
"title": "[to be translated]:Navigation Style",
"top": "[to be translated]:Top Navbar"
},
"startButton": "[to be translated]:Get Started",
"welcome": {
"subtitle": "[to be translated]:Your powerful AI desktop client. Let's set up your preferences before getting started.",
"title": "[to be translated]:Welcome to Cherry Studio"
}
},
"guides": {
"configureProvider": {
"description": "[to be translated]:Set up your AI provider to get started",
"steps": {
"apiKey": {
"description": "[to be translated]:Enter your API key and click the check button to verify the connection.",
"shortDescription": "[to be translated]:Enter your API key here",
"title": "[to be translated]:Connect Your AI Service"
}
},
"title": "[to be translated]:Configure Provider"
},
"sendMessage": {
"description": "[to be translated]:Start chatting with AI",
"steps": {
"input": {
"description": "[to be translated]:Type your message here and press Enter to send. Cherry Studio supports Markdown, code highlighting, and file attachments.",
"shortDescription": "[to be translated]:Type your message here and send",
"title": "[to be translated]:Send Your First Message"
}
},
"title": "[to be translated]:Chat with AI"
},
"useFreeModel": {
"description": "[to be translated]:Try our free built-in model",
"steps": {
"selector": {
"description": "[to be translated]:Click here to select a model. Try the free built-in model to get started without an API key.",
"shortDescription": "[to be translated]:Click here to select a free model",
"title": "[to be translated]:Select a Model"
}
},
"title": "[to be translated]:Use Free Model"
}
},
"taskPopover": {
"configureProvider": {
"button": "[to be translated]:Start Setup",
"features": {
"connectApi": "[to be translated]:🔑 Connect your API service",
"multiProvider": "[to be translated]:⚙️ Support multiple providers",
"secureStorage": "[to be translated]:🔒 Keys stored securely on your device"
},
"title": "[to be translated]:Configure AI Provider"
},
"sendFirstMessage": {
"button": "[to be translated]:Start Chatting",
"features": {
"fileAnalysis": "[to be translated]:📎 File analysis — supports PDF/Word",
"markdown": "[to be translated]:✨ Markdown rendering — perfect code highlighting",
"typewriter": "[to be translated]:🎨 Typewriter effect — smooth experience"
},
"title": "[to be translated]:Send Your First Message"
},
"useFreeModel": {
"button": "[to be translated]:Use Now",
"features": {
"allInOne": "[to be translated]:📝 All-in-one assistant — for Q&A, translation and coding",
"fastResponse": "[to be translated]:⚡️ Fast response — millisecond-level latency",
"noRegistration": "[to be translated]:🟢 No registration required — built-in free computing power"
},
"title": "[to be translated]:Use Free Model"
}
}
},
"warning": {
"missing_provider": "サプライヤーが存在しないため、デフォルトのサプライヤー {{provider}} にロールバックされました。これにより問題が発生する可能性があります。"
},

View File

@ -5352,6 +5352,119 @@
"saveDataError": "Falha ao salvar os dados, tente novamente",
"title": "Atualização"
},
"userGuide": {
"buttons": {
"gotIt": "[to be translated]:Got it",
"useNow": "[to be translated]:Use Now"
},
"checklist": {
"progress": "[to be translated]:{{completed}} of {{total}} completed",
"subtitle": "[to be translated]:Complete tasks to unlock Cherry Studio advanced tips",
"tasks": {
"configureProvider": "[to be translated]:Configure your first AI provider",
"sendFirstMessage": "[to be translated]:Send your first message",
"useFreeModel": "[to be translated]:Try the free built-in model"
},
"title": "[to be translated]:Start Your AI Journey"
},
"completionModal": {
"exploreMore": "[to be translated]:Explore More Assistants",
"recommendedAssistants": "[to be translated]:Recommended Assistants",
"startChat": "[to be translated]:Start Chatting",
"subtitle": "[to be translated]:Welcome to Cherry Studio. You're all set to explore!",
"title": "[to be translated]:Setup Complete!"
},
"guidePage": {
"carousel": {
"assistants": {
"description": "[to be translated]:From project management to chart drawing, define dedicated system prompts to build your professional AI team.",
"title": "[to be translated]:AI Assistants for Every Task"
},
"models": {
"description": "[to be translated]:One-stop management of OpenAI, Claude, DeepSeek and many other model services. Free switching, local storage, giving the choice back to you.",
"title": "[to be translated]:World-Class AI Models"
},
"paintings": {
"description": "[to be translated]:Integrate multiple mainstream drawing models with fine-grained parameter control. Whether it's art creation or asset generation, it's all at your fingertips.",
"title": "[to be translated]:Unlimited Creativity"
}
},
"navStyle": {
"left": "[to be translated]:Sidebar",
"title": "[to be translated]:Navigation Style",
"top": "[to be translated]:Top Navbar"
},
"startButton": "[to be translated]:Get Started",
"welcome": {
"subtitle": "[to be translated]:Your powerful AI desktop client. Let's set up your preferences before getting started.",
"title": "[to be translated]:Welcome to Cherry Studio"
}
},
"guides": {
"configureProvider": {
"description": "[to be translated]:Set up your AI provider to get started",
"steps": {
"apiKey": {
"description": "[to be translated]:Enter your API key and click the check button to verify the connection.",
"shortDescription": "[to be translated]:Enter your API key here",
"title": "[to be translated]:Connect Your AI Service"
}
},
"title": "[to be translated]:Configure Provider"
},
"sendMessage": {
"description": "[to be translated]:Start chatting with AI",
"steps": {
"input": {
"description": "[to be translated]:Type your message here and press Enter to send. Cherry Studio supports Markdown, code highlighting, and file attachments.",
"shortDescription": "[to be translated]:Type your message here and send",
"title": "[to be translated]:Send Your First Message"
}
},
"title": "[to be translated]:Chat with AI"
},
"useFreeModel": {
"description": "[to be translated]:Try our free built-in model",
"steps": {
"selector": {
"description": "[to be translated]:Click here to select a model. Try the free built-in model to get started without an API key.",
"shortDescription": "[to be translated]:Click here to select a free model",
"title": "[to be translated]:Select a Model"
}
},
"title": "[to be translated]:Use Free Model"
}
},
"taskPopover": {
"configureProvider": {
"button": "[to be translated]:Start Setup",
"features": {
"connectApi": "[to be translated]:🔑 Connect your API service",
"multiProvider": "[to be translated]:⚙️ Support multiple providers",
"secureStorage": "[to be translated]:🔒 Keys stored securely on your device"
},
"title": "[to be translated]:Configure AI Provider"
},
"sendFirstMessage": {
"button": "[to be translated]:Start Chatting",
"features": {
"fileAnalysis": "[to be translated]:📎 File analysis — supports PDF/Word",
"markdown": "[to be translated]:✨ Markdown rendering — perfect code highlighting",
"typewriter": "[to be translated]:🎨 Typewriter effect — smooth experience"
},
"title": "[to be translated]:Send Your First Message"
},
"useFreeModel": {
"button": "[to be translated]:Use Now",
"features": {
"allInOne": "[to be translated]:📝 All-in-one assistant — for Q&A, translation and coding",
"fastResponse": "[to be translated]:⚡️ Fast response — millisecond-level latency",
"noRegistration": "[to be translated]:🟢 No registration required — built-in free computing power"
},
"title": "[to be translated]:Use Free Model"
}
}
},
"warning": {
"missing_provider": "O fornecedor não existe; foi revertido para o fornecedor predefinido {{provider}}. Isto pode causar problemas."
},

View File

@ -5352,6 +5352,119 @@
"saveDataError": "Salvarea datelor a eșuat, te rugăm să încerci din nou.",
"title": "Actualizare"
},
"userGuide": {
"buttons": {
"gotIt": "[to be translated]:Got it",
"useNow": "[to be translated]:Use Now"
},
"checklist": {
"progress": "[to be translated]:{{completed}} of {{total}} completed",
"subtitle": "[to be translated]:Complete tasks to unlock Cherry Studio advanced tips",
"tasks": {
"configureProvider": "[to be translated]:Configure your first AI provider",
"sendFirstMessage": "[to be translated]:Send your first message",
"useFreeModel": "[to be translated]:Try the free built-in model"
},
"title": "[to be translated]:Start Your AI Journey"
},
"completionModal": {
"exploreMore": "[to be translated]:Explore More Assistants",
"recommendedAssistants": "[to be translated]:Recommended Assistants",
"startChat": "[to be translated]:Start Chatting",
"subtitle": "[to be translated]:Welcome to Cherry Studio. You're all set to explore!",
"title": "[to be translated]:Setup Complete!"
},
"guidePage": {
"carousel": {
"assistants": {
"description": "[to be translated]:From project management to chart drawing, define dedicated system prompts to build your professional AI team.",
"title": "[to be translated]:AI Assistants for Every Task"
},
"models": {
"description": "[to be translated]:One-stop management of OpenAI, Claude, DeepSeek and many other model services. Free switching, local storage, giving the choice back to you.",
"title": "[to be translated]:World-Class AI Models"
},
"paintings": {
"description": "[to be translated]:Integrate multiple mainstream drawing models with fine-grained parameter control. Whether it's art creation or asset generation, it's all at your fingertips.",
"title": "[to be translated]:Unlimited Creativity"
}
},
"navStyle": {
"left": "[to be translated]:Sidebar",
"title": "[to be translated]:Navigation Style",
"top": "[to be translated]:Top Navbar"
},
"startButton": "[to be translated]:Get Started",
"welcome": {
"subtitle": "[to be translated]:Your powerful AI desktop client. Let's set up your preferences before getting started.",
"title": "[to be translated]:Welcome to Cherry Studio"
}
},
"guides": {
"configureProvider": {
"description": "[to be translated]:Set up your AI provider to get started",
"steps": {
"apiKey": {
"description": "[to be translated]:Enter your API key and click the check button to verify the connection.",
"shortDescription": "[to be translated]:Enter your API key here",
"title": "[to be translated]:Connect Your AI Service"
}
},
"title": "[to be translated]:Configure Provider"
},
"sendMessage": {
"description": "[to be translated]:Start chatting with AI",
"steps": {
"input": {
"description": "[to be translated]:Type your message here and press Enter to send. Cherry Studio supports Markdown, code highlighting, and file attachments.",
"shortDescription": "[to be translated]:Type your message here and send",
"title": "[to be translated]:Send Your First Message"
}
},
"title": "[to be translated]:Chat with AI"
},
"useFreeModel": {
"description": "[to be translated]:Try our free built-in model",
"steps": {
"selector": {
"description": "[to be translated]:Click here to select a model. Try the free built-in model to get started without an API key.",
"shortDescription": "[to be translated]:Click here to select a free model",
"title": "[to be translated]:Select a Model"
}
},
"title": "[to be translated]:Use Free Model"
}
},
"taskPopover": {
"configureProvider": {
"button": "[to be translated]:Start Setup",
"features": {
"connectApi": "[to be translated]:🔑 Connect your API service",
"multiProvider": "[to be translated]:⚙️ Support multiple providers",
"secureStorage": "[to be translated]:🔒 Keys stored securely on your device"
},
"title": "[to be translated]:Configure AI Provider"
},
"sendFirstMessage": {
"button": "[to be translated]:Start Chatting",
"features": {
"fileAnalysis": "[to be translated]:📎 File analysis — supports PDF/Word",
"markdown": "[to be translated]:✨ Markdown rendering — perfect code highlighting",
"typewriter": "[to be translated]:🎨 Typewriter effect — smooth experience"
},
"title": "[to be translated]:Send Your First Message"
},
"useFreeModel": {
"button": "[to be translated]:Use Now",
"features": {
"allInOne": "[to be translated]:📝 All-in-one assistant — for Q&A, translation and coding",
"fastResponse": "[to be translated]:⚡️ Fast response — millisecond-level latency",
"noRegistration": "[to be translated]:🟢 No registration required — built-in free computing power"
},
"title": "[to be translated]:Use Free Model"
}
}
},
"warning": {
"missing_provider": "Furnizorul nu există; s-a revenit la furnizorul implicit {{provider}}. Acest lucru poate cauza probleme."
},

View File

@ -5352,6 +5352,119 @@
"saveDataError": "Ошибка сохранения данных, повторите попытку",
"title": "Обновление"
},
"userGuide": {
"buttons": {
"gotIt": "[to be translated]:Got it",
"useNow": "[to be translated]:Use Now"
},
"checklist": {
"progress": "[to be translated]:{{completed}} of {{total}} completed",
"subtitle": "[to be translated]:Complete tasks to unlock Cherry Studio advanced tips",
"tasks": {
"configureProvider": "[to be translated]:Configure your first AI provider",
"sendFirstMessage": "[to be translated]:Send your first message",
"useFreeModel": "[to be translated]:Try the free built-in model"
},
"title": "[to be translated]:Start Your AI Journey"
},
"completionModal": {
"exploreMore": "[to be translated]:Explore More Assistants",
"recommendedAssistants": "[to be translated]:Recommended Assistants",
"startChat": "[to be translated]:Start Chatting",
"subtitle": "[to be translated]:Welcome to Cherry Studio. You're all set to explore!",
"title": "[to be translated]:Setup Complete!"
},
"guidePage": {
"carousel": {
"assistants": {
"description": "[to be translated]:From project management to chart drawing, define dedicated system prompts to build your professional AI team.",
"title": "[to be translated]:AI Assistants for Every Task"
},
"models": {
"description": "[to be translated]:One-stop management of OpenAI, Claude, DeepSeek and many other model services. Free switching, local storage, giving the choice back to you.",
"title": "[to be translated]:World-Class AI Models"
},
"paintings": {
"description": "[to be translated]:Integrate multiple mainstream drawing models with fine-grained parameter control. Whether it's art creation or asset generation, it's all at your fingertips.",
"title": "[to be translated]:Unlimited Creativity"
}
},
"navStyle": {
"left": "[to be translated]:Sidebar",
"title": "[to be translated]:Navigation Style",
"top": "[to be translated]:Top Navbar"
},
"startButton": "[to be translated]:Get Started",
"welcome": {
"subtitle": "[to be translated]:Your powerful AI desktop client. Let's set up your preferences before getting started.",
"title": "[to be translated]:Welcome to Cherry Studio"
}
},
"guides": {
"configureProvider": {
"description": "[to be translated]:Set up your AI provider to get started",
"steps": {
"apiKey": {
"description": "[to be translated]:Enter your API key and click the check button to verify the connection.",
"shortDescription": "[to be translated]:Enter your API key here",
"title": "[to be translated]:Connect Your AI Service"
}
},
"title": "[to be translated]:Configure Provider"
},
"sendMessage": {
"description": "[to be translated]:Start chatting with AI",
"steps": {
"input": {
"description": "[to be translated]:Type your message here and press Enter to send. Cherry Studio supports Markdown, code highlighting, and file attachments.",
"shortDescription": "[to be translated]:Type your message here and send",
"title": "[to be translated]:Send Your First Message"
}
},
"title": "[to be translated]:Chat with AI"
},
"useFreeModel": {
"description": "[to be translated]:Try our free built-in model",
"steps": {
"selector": {
"description": "[to be translated]:Click here to select a model. Try the free built-in model to get started without an API key.",
"shortDescription": "[to be translated]:Click here to select a free model",
"title": "[to be translated]:Select a Model"
}
},
"title": "[to be translated]:Use Free Model"
}
},
"taskPopover": {
"configureProvider": {
"button": "[to be translated]:Start Setup",
"features": {
"connectApi": "[to be translated]:🔑 Connect your API service",
"multiProvider": "[to be translated]:⚙️ Support multiple providers",
"secureStorage": "[to be translated]:🔒 Keys stored securely on your device"
},
"title": "[to be translated]:Configure AI Provider"
},
"sendFirstMessage": {
"button": "[to be translated]:Start Chatting",
"features": {
"fileAnalysis": "[to be translated]:📎 File analysis — supports PDF/Word",
"markdown": "[to be translated]:✨ Markdown rendering — perfect code highlighting",
"typewriter": "[to be translated]:🎨 Typewriter effect — smooth experience"
},
"title": "[to be translated]:Send Your First Message"
},
"useFreeModel": {
"button": "[to be translated]:Use Now",
"features": {
"allInOne": "[to be translated]:📝 All-in-one assistant — for Q&A, translation and coding",
"fastResponse": "[to be translated]:⚡️ Fast response — millisecond-level latency",
"noRegistration": "[to be translated]:🟢 No registration required — built-in free computing power"
},
"title": "[to be translated]:Use Free Model"
}
}
},
"warning": {
"missing_provider": "Поставщик не существует, возвращение к поставщику по умолчанию {{provider}}. Это может привести к проблемам."
},

View File

@ -651,6 +651,7 @@ export const InputbarCore: FC<InputbarCoreProps> = ({
{quickPanelElement}
<InputBarContainer
id="inputbar"
data-guide-target="chat-input"
className={classNames('inputbar-container', isDragging && 'file-dragging', isExpanded && 'expanded')}>
<DragHandle onMouseDown={handleDragStart}>
<HolderOutlined style={{ fontSize: 12 }} />

View File

@ -55,7 +55,7 @@ const SelectModelButton: FC<Props> = ({ assistant }) => {
const providerName = getProviderName(model)
return (
<DropdownButton size="small" type="text" onClick={onSelectModel}>
<DropdownButton size="small" type="text" onClick={onSelectModel} data-guide-target="model-selector">
<ButtonContent>
<ModelAvatar model={model} size={20} />
<ModelName>

View File

@ -477,7 +477,7 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
</Tooltip>
)}
</SettingSubtitle>
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
<Space.Compact style={{ width: '100%', marginTop: 5 }} data-guide-target="provider-api-key">
<Input.Password
id="api-key-input"
value={localApiKey}

View File

@ -1,11 +1,20 @@
import type { PayloadAction } from '@reduxjs/toolkit'
import { createSlice } from '@reduxjs/toolkit'
import type { OnboardingState } from '@renderer/types/onboarding'
import type { OnboardingState, UserGuideTaskStatus } from '@renderer/types/onboarding'
const initialState: OnboardingState = {
completedOnboardingVersion: null,
completedFeatureGuides: [],
onboardingSkipped: false
onboardingSkipped: false,
guidePageCompleted: false,
checklistDismissed: false,
checklistVisible: false,
taskStatus: {
useFreeModel: false,
configureProvider: false,
sendFirstMessage: false
},
completionModalShown: false
}
const onboardingSlice = createSlice({
@ -25,10 +34,59 @@ const onboardingSlice = createSlice({
state.onboardingSkipped = true
state.completedOnboardingVersion = action.payload
},
resetOnboarding: () => initialState
resetOnboarding: () => initialState,
// New actions for user guide
completeGuidePage: (state) => {
state.guidePageCompleted = true
// Auto-show checklist when guide page is completed
state.checklistVisible = true
},
dismissChecklist: (state) => {
state.checklistDismissed = true
state.checklistVisible = false
},
toggleChecklistVisible: (state) => {
state.checklistVisible = !state.checklistVisible
},
setChecklistVisible: (state, action: PayloadAction<boolean>) => {
state.checklistVisible = action.payload
},
updateTaskStatus: (state, action: PayloadAction<Partial<UserGuideTaskStatus>>) => {
state.taskStatus = { ...state.taskStatus, ...action.payload }
},
completeTask: (state, action: PayloadAction<keyof UserGuideTaskStatus>) => {
state.taskStatus[action.payload] = true
},
showCompletionModal: (state) => {
state.completionModalShown = true
},
resetUserGuide: (state) => {
state.guidePageCompleted = false
state.checklistDismissed = false
state.checklistVisible = false
state.taskStatus = {
useFreeModel: false,
configureProvider: false,
sendFirstMessage: false
}
state.completionModalShown = false
}
}
})
export const { completeOnboarding, completeFeatureGuide, skipOnboarding, resetOnboarding } = onboardingSlice.actions
export const {
completeOnboarding,
completeFeatureGuide,
skipOnboarding,
resetOnboarding,
completeGuidePage,
dismissChecklist,
toggleChecklistVisible,
setChecklistVisible,
updateTaskStatus,
completeTask,
showCompletionModal,
resetUserGuide
} = onboardingSlice.actions
export default onboardingSlice.reducer

View File

@ -12,12 +12,18 @@ export interface GuideStep {
titleKey: string
/** i18n key for description */
descriptionKey: string
/** Interpolation values for description (e.g., { imageUrl: 'path/to/image.gif' }) */
descriptionInterpolation?: Record<string, 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
/** Custom next button text (i18n key) */
nextBtnTextKey?: string
/** Custom done button text (i18n key, for last step) */
doneBtnTextKey?: string
}
/** Version-specific guide definition */
@ -36,6 +42,18 @@ export interface VersionGuide {
route?: string
/** Priority for ordering when multiple guides apply (higher = first) */
priority?: number
/** Custom popover class for styling (e.g., 'user-guide-popover') */
popoverClass?: string
}
/** Task status for user guide checklist */
export interface UserGuideTaskStatus {
/** Whether user has used a free model */
useFreeModel: boolean
/** Whether user has configured a provider */
configureProvider: boolean
/** Whether user has sent the first message */
sendFirstMessage: boolean
}
/** Redux state for onboarding */
@ -46,4 +64,14 @@ export interface OnboardingState {
completedFeatureGuides: string[]
/** Whether user has explicitly skipped onboarding */
onboardingSkipped: boolean
/** Whether the initial guide page has been completed */
guidePageCompleted: boolean
/** Whether the checklist has been dismissed by user */
checklistDismissed: boolean
/** Whether the checklist popover is currently visible */
checklistVisible: boolean
/** Status of user guide tasks */
taskStatus: UserGuideTaskStatus
/** Whether the completion modal has been shown */
completionModalShown: boolean
}