feat: user guide fully
@ -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>
|
||||
)
|
||||
|
||||
BIN
src/renderer/src/assets/images/guide/Carousel_1.png
Normal file
|
After Width: | Height: | Size: 650 KiB |
BIN
src/renderer/src/assets/images/guide/Carousel_1_dark.png
Normal file
|
After Width: | Height: | Size: 697 KiB |
BIN
src/renderer/src/assets/images/guide/Carousel_2.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
src/renderer/src/assets/images/guide/Carousel_2_dark.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
src/renderer/src/assets/images/guide/Carousel_3.png
Normal file
|
After Width: | Height: | Size: 797 KiB |
BIN
src/renderer/src/assets/images/guide/Carousel_3_dark.png
Normal file
|
After Width: | Height: | Size: 915 KiB |
BIN
src/renderer/src/assets/images/guide/assistant_background.png
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
BIN
src/renderer/src/assets/images/guide/chat.mp4
Normal file
BIN
src/renderer/src/assets/images/guide/chat_dark.mp4
Normal file
BIN
src/renderer/src/assets/images/guide/cherryai_3d.png
Normal file
|
After Width: | Height: | Size: 278 KiB |
BIN
src/renderer/src/assets/images/guide/free_model.gif
Normal file
|
After Width: | Height: | Size: 4.7 MiB |
BIN
src/renderer/src/assets/images/guide/layout_nav.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
src/renderer/src/assets/images/guide/layout_nav_dark.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
src/renderer/src/assets/images/guide/layout_side.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
src/renderer/src/assets/images/guide/layout_side_dark.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
@ -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 () => {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
116
src/renderer/src/components/UserGuide/CompletionModal/index.tsx
Normal 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
|
||||
198
src/renderer/src/components/UserGuide/CompletionModal/styles.ts
Normal 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;
|
||||
}
|
||||
`
|
||||
@ -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
|
||||
@ -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
|
||||
127
src/renderer/src/components/UserGuide/GuidePage/index.tsx
Normal 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
|
||||
313
src/renderer/src/components/UserGuide/GuidePage/styles.ts
Normal 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;
|
||||
}
|
||||
`
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -0,0 +1 @@
|
||||
export { default as ChecklistContent } from './ChecklistContent'
|
||||
@ -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;
|
||||
`
|
||||
@ -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
|
||||
}
|
||||
51
src/renderer/src/components/UserGuide/hooks/useUserGuide.ts
Normal 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
|
||||
}
|
||||
}
|
||||
5
src/renderer/src/components/UserGuide/index.ts
Normal 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'
|
||||
@ -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 ? (
|
||||
|
||||
@ -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'
|
||||
|
||||
115
src/renderer/src/config/onboarding/guides/userGuideSteps.ts
Normal 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'
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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'
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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."
|
||||
},
|
||||
|
||||
@ -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}}。这可能导致问题。"
|
||||
},
|
||||
|
||||
@ -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}}。這可能導致問題。"
|
||||
},
|
||||
|
||||
@ -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."
|
||||
},
|
||||
|
||||
@ -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}}. Αυτό μπορεί να προκαλέσει προβλήματα."
|
||||
},
|
||||
|
||||
@ -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."
|
||||
},
|
||||
|
||||
@ -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 n’existe pas, retour au fournisseur par défaut {{provider}}. Cela peut entraîner des problèmes."
|
||||
},
|
||||
|
||||
@ -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}} にロールバックされました。これにより問題が発生する可能性があります。"
|
||||
},
|
||||
|
||||
@ -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."
|
||||
},
|
||||
|
||||
@ -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."
|
||||
},
|
||||
|
||||
@ -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}}. Это может привести к проблемам."
|
||||
},
|
||||
|
||||
@ -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 }} />
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||