From b8327462f9be241082508c398cc6e759c4295bd1 Mon Sep 17 00:00:00 2001 From: SiYue-ZO <2835601846@qq.com> Date: Tue, 31 Mar 2026 00:43:35 +0800 Subject: [PATCH] feat: add first-time tour guide for new users - Add tour guide component with floating bubbles - Guide users through: Welcome -> Configure Models -> Start Gateway -> View Docs - Use localStorage to persist tour state - Support i18n (Chinese and English) - Highlight target elements with spotlight mask - Allow skipping tour at any time --- web/frontend/src/components/app-header.tsx | 10 +- web/frontend/src/components/app-layout.tsx | 2 + web/frontend/src/components/app-sidebar.tsx | 1 + .../src/components/tour/tour-guide.tsx | 242 ++++++++++++++++++ web/frontend/src/i18n/locales/en.json | 22 ++ web/frontend/src/i18n/locales/zh.json | 22 ++ web/frontend/src/store/index.ts | 1 + web/frontend/src/store/tour.ts | 69 +++++ 8 files changed, 368 insertions(+), 1 deletion(-) create mode 100644 web/frontend/src/components/tour/tour-guide.tsx create mode 100644 web/frontend/src/store/tour.ts diff --git a/web/frontend/src/components/app-header.tsx b/web/frontend/src/components/app-header.tsx index 4f0688008..fa1b5a488 100644 --- a/web/frontend/src/components/app-header.tsx +++ b/web/frontend/src/components/app-header.tsx @@ -163,6 +163,7 @@ export function AppHeader() { variant="destructive" size="icon-sm" className="size-8" + data-tour="gateway-button" onClick={handleGatewayToggle} disabled={gwLoading} aria-label={t("header.gateway.action.stop")} @@ -178,6 +179,7 @@ export function AppHeader() { isStarting || isRestarting || isStopping ? "secondary" : "default" } size="sm" + data-tour="gateway-button" className={`h-8 gap-2 px-3 ${ isStopped ? "bg-green-500 text-white hover:bg-green-600" : "" }`} @@ -209,7 +211,13 @@ export function AppHeader() { /> {/* Docs Link */} - + )} + + + + + {currentStepIndex < totalSteps - 1 && ( + + )} + + + ) +} diff --git a/web/frontend/src/i18n/locales/en.json b/web/frontend/src/i18n/locales/en.json index 9d170a4c8..69e256758 100644 --- a/web/frontend/src/i18n/locales/en.json +++ b/web/frontend/src/i18n/locales/en.json @@ -550,5 +550,27 @@ "clear": "Clear logs", "empty": "Waiting for logs..." } + }, + "tour": { + "skip": "Skip tour", + "prev": "Previous", + "next": "Next", + "finish": "Finish", + "welcome": { + "title": "Welcome to PicoClaw", + "description": "PicoClaw is a powerful AI assistant platform. Let's take a few seconds to help you complete the basic setup." + }, + "models": { + "title": "Configure Models", + "description": "Click the \"Models\" menu on the left to configure API keys for AI providers. Only configured models can be used for chat." + }, + "gateway": { + "title": "Start Gateway", + "description": "After configuring models, click the \"Start Gateway\" button at the top to begin chatting with AI." + }, + "docs": { + "title": "View Documentation", + "description": "Need more help? Click the documentation button in the top right corner to view detailed guides and configuration docs." + } } } diff --git a/web/frontend/src/i18n/locales/zh.json b/web/frontend/src/i18n/locales/zh.json index b214753ca..e7aca1918 100644 --- a/web/frontend/src/i18n/locales/zh.json +++ b/web/frontend/src/i18n/locales/zh.json @@ -550,5 +550,27 @@ "clear": "清空日志", "empty": "等待日志中..." } + }, + "tour": { + "skip": "跳过引导", + "prev": "上一步", + "next": "下一步", + "finish": "完成", + "welcome": { + "title": "欢迎使用 PicoClaw", + "description": "PicoClaw 是一个强大的 AI 助手平台。让我们花几秒钟时间,帮您完成基础配置。" + }, + "models": { + "title": "配置模型", + "description": "点击左侧「模型」菜单,为 AI 服务商配置 API Key。只有配置好的模型才能用于对话。" + }, + "gateway": { + "title": "启动服务", + "description": "配置好模型后,点击顶部的「启动服务」按钮,即可开始与 AI 对话。" + }, + "docs": { + "title": "查看文档", + "description": "需要更多帮助?点击右上角的文档按钮,查看详细的使用文档和配置指南。" + } } } diff --git a/web/frontend/src/store/index.ts b/web/frontend/src/store/index.ts index d377cdace..a13b7b161 100644 --- a/web/frontend/src/store/index.ts +++ b/web/frontend/src/store/index.ts @@ -1,2 +1,3 @@ export * from "./gateway" export * from "./chat" +export * from "./tour" diff --git a/web/frontend/src/store/tour.ts b/web/frontend/src/store/tour.ts new file mode 100644 index 000000000..40fe697e2 --- /dev/null +++ b/web/frontend/src/store/tour.ts @@ -0,0 +1,69 @@ +import { atom } from "jotai" +import { atomWithStorage } from "jotai/utils" + +export type TourStep = "welcome" | "models" | "gateway" | "docs" | "completed" + +export interface TourState { + currentStep: TourStep + isActive: boolean +} + +const STORAGE_KEY = "picoclaw-tour-state" + +const DEFAULT_TOUR_STATE: TourState = { + currentStep: "welcome", + isActive: true, +} + +export const tourAtom = atomWithStorage( + STORAGE_KEY, + DEFAULT_TOUR_STATE, +) + +export const tourIsActiveAtom = atom( + (get) => get(tourAtom).isActive, + (get, set, isActive: boolean) => { + set(tourAtom, { ...get(tourAtom), isActive }) + }, +) + +export const tourCurrentStepAtom = atom( + (get) => get(tourAtom).currentStep, + (get, set, step: TourStep) => { + set(tourAtom, { ...get(tourAtom), currentStep: step }) + }, +) + +export function useTourActions() { + const goToNextStep = (currentStep: TourStep): TourStep => { + const steps: TourStep[] = [ + "welcome", + "models", + "gateway", + "docs", + "completed", + ] + const currentIndex = steps.indexOf(currentStep) + if (currentIndex < steps.length - 1) { + return steps[currentIndex + 1] + } + return "completed" + } + + const goToPrevStep = (currentStep: TourStep): TourStep => { + const steps: TourStep[] = [ + "welcome", + "models", + "gateway", + "docs", + "completed", + ] + const currentIndex = steps.indexOf(currentStep) + if (currentIndex > 0) { + return steps[currentIndex - 1] + } + return currentStep + } + + return { goToNextStep, goToPrevStep } +}