From 562eba412cb7b6ff7ce67c795e91caaa52391c00 Mon Sep 17 00:00:00 2001 From: nieao Date: Sat, 9 May 2026 19:15:05 +0800 Subject: [PATCH] feat(dashboard): first-visit onboarding overlay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a 5-step modal that walks new users through the dashboard's core operations on first visit. Auto-hides via localStorage after dismiss; can be force-shown with `?onboard=force` for screenshots and demos. ## What it teaches 1. What the graph represents (entities/relations from code or wiki) 2. Three view buttons (Overview / Learn / Deep Dive) — each answers a different question 3. Search + node click — find by name, click for details panel 4. Layer switch + Project Tour — drill into a category, or follow a guided walkthrough 5. Hidden features (Filter / Export / Path / Theme) and Shift+? for keyboard shortcuts ## Design - Inline styles, no extra CSS file — easier to land in the existing structure - Lazy-loaded via Suspense like the other modals (KeyboardShortcutsHelp, PathFinderModal) so it ships in a separate chunk - Architectural-minimalism dark palette consistent with the existing dashboard: off-black surface, warm accent (#c8a882), Noto Serif SC headings, generous whitespace - localStorage key `ua-onboarding-dismissed-v1` — versioned so future content changes can re-trigger - Accessible: keyboard-navigable buttons, click-outside to close (without remembering dismiss), explicit "不再显示" / "Skip" affordance ## Tested - Windows 11 + Chrome via Playwright: 5 steps render, progress bar tracks, prev/next/dismiss/finish all work, localStorage persists dismiss across reloads, `?onboard=force` re-shows for testing - No new dependencies (uses React 19 hooks already present) - No changes to data flow, store, or other components — strictly additive Co-Authored-By: Claude Opus 4.7 (1M context) --- .../packages/dashboard/src/App.tsx | 6 + .../src/components/OnboardingOverlay.tsx | 276 ++++++++++++++++++ 2 files changed, 282 insertions(+) create mode 100644 understand-anything-plugin/packages/dashboard/src/components/OnboardingOverlay.tsx diff --git a/understand-anything-plugin/packages/dashboard/src/App.tsx b/understand-anything-plugin/packages/dashboard/src/App.tsx index 8abc0e8..570c076 100644 --- a/understand-anything-plugin/packages/dashboard/src/App.tsx +++ b/understand-anything-plugin/packages/dashboard/src/App.tsx @@ -31,6 +31,7 @@ const PathFinderModal = lazy(() => import("./components/PathFinderModal")); const KeyboardShortcutsHelp = lazy( () => import("./components/KeyboardShortcutsHelp"), ); +const OnboardingOverlay = lazy(() => import("./components/OnboardingOverlay")); const DEMO_MODE = import.meta.env.VITE_DEMO_MODE === "true"; const SESSION_TOKEN_KEY = "understand-anything-token"; @@ -610,6 +611,11 @@ function Dashboard({ accessToken }: { accessToken: string }) { )} + + {/* First-visit onboarding overlay — auto-hides after dismiss via localStorage. */} + + + ); diff --git a/understand-anything-plugin/packages/dashboard/src/components/OnboardingOverlay.tsx b/understand-anything-plugin/packages/dashboard/src/components/OnboardingOverlay.tsx new file mode 100644 index 0000000..2c05256 --- /dev/null +++ b/understand-anything-plugin/packages/dashboard/src/components/OnboardingOverlay.tsx @@ -0,0 +1,276 @@ +import { useState, useEffect } from "react"; + +/** + * First-visit onboarding overlay. + * + * Renders 5 dismissible steps that teach a new user how to operate the dashboard. + * State persists in localStorage so returning users are not interrupted. + * + * Force-show via `?onboard=force` URL param (useful for screenshots / demos). + */ + +const STORAGE_KEY = "ua-onboarding-dismissed-v1"; + +interface Step { + title: string; + body: string; + hint?: string; +} + +const STEPS: Step[] = [ + { + title: "欢迎进入知识图", + body: "你看到的圆点和连线是 Understand-Anything 把这份项目(代码 / wiki)抽出来的实体和关系。每个节点是一个文件、概念、实体或断言。", + hint: "5 步以内带你过完核心操作", + }, + { + title: "顶部三个视图", + body: "Overview 看全貌(力导向图)· Learn 跟随预设学习路径 · Deep Dive 看类型 / 复杂度统计。每个视图回答一种不同的问法。", + hint: "切视图前先想清楚自己在问什么", + }, + { + title: "搜索 + 点节点", + body: "顶部搜索框模糊匹配节点名 / summary / tags。点任意节点 → 右侧详情面板出现 summary + 邻居列表 + Open Article 按钮。", + hint: "搜索高亮居中,点节点高亮邻居边", + }, + { + title: "Layer 切换 + Tour", + body: "顶部 All 旁边的 layer 标签按 index.md 分类只显示部分节点。右侧 Project Tour 自动按编辑者预设顺序导览。", + hint: "节点太密看不清就用 Layer,没头绪就启 Tour", + }, + { + title: "更多隐藏功能", + body: "顶栏还有 Filter(按类型 / 复杂度过滤)、Export(导出图)、Path(找两个节点之间的路径)、Theme(切换主题)。Shift + ? 看完整快捷键。", + hint: "需要时再展开,不要一次记完", + }, +]; + +export default function OnboardingOverlay() { + const [stepIdx, setStepIdx] = useState(0); + const [open, setOpen] = useState(false); + + useEffect(() => { + if (typeof window === "undefined") return; + const params = new URLSearchParams(window.location.search); + const force = params.get("onboard") === "force"; + const dismissed = window.localStorage.getItem(STORAGE_KEY) === "1"; + if (force || !dismissed) setOpen(true); + }, []); + + if (!open) return null; + + const isFirst = stepIdx === 0; + const isLast = stepIdx === STEPS.length - 1; + const step = STEPS[stepIdx]; + + function dismiss(remember: boolean) { + if (remember && typeof window !== "undefined") { + window.localStorage.setItem(STORAGE_KEY, "1"); + } + setOpen(false); + } + + return ( +
{ + if (e.target === e.currentTarget) dismiss(false); + }} + > +
+
+ 0{stepIdx + 1} + / 0{STEPS.length} + + UNDERSTAND-ANYTHING · 入门 +
+ +

{step.title}

+

{step.body}

+ {step.hint && ( +
+ · + {step.hint} +
+ )} + +
+ {STEPS.map((_, i) => ( +
+ ))} +
+ +
+ +
+ {!isFirst && ( + + )} + {!isLast ? ( + + ) : ( + + )} +
+
+
+ ); +} + +// ----- styles (inline 避免依赖 css 文件) ----- + +const overlayStyle: React.CSSProperties = { + position: "fixed", + inset: 0, + background: "rgba(0, 0, 0, 0.78)", + backdropFilter: "blur(6px)", + zIndex: 9999, + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: 16, + fontFamily: + '"Noto Sans SC", "Microsoft YaHei", system-ui, -apple-system, sans-serif', + animation: "ua-fade-in 0.4s cubic-bezier(0.22, 1, 0.36, 1)", +}; + +const cardStyle: React.CSSProperties = { + background: "#1a1a1a", + color: "#fafafa", + maxWidth: 580, + width: "100%", + padding: "48px 48px 36px", + border: "1px solid #2a2a2a", + borderTop: "2px solid #c8a882", + position: "relative", +}; + +const tagStyle: React.CSSProperties = { + fontSize: "0.72rem", + letterSpacing: "0.3em", + color: "#888", + textTransform: "uppercase", + marginBottom: 24, + display: "flex", + alignItems: "center", + flexWrap: "wrap", + gap: 4, +}; + +const numStyle: React.CSSProperties = { + fontFamily: '"Noto Serif SC", Georgia, serif', + color: "#c8a882", + fontSize: "0.9rem", + letterSpacing: "0.1em", + marginRight: 4, +}; + +const dotStyle: React.CSSProperties = { + width: 4, + height: 4, + background: "#c8a882", + borderRadius: "50%", + margin: "0 12px", +}; + +const titleStyle: React.CSSProperties = { + fontFamily: '"Noto Serif SC", Georgia, serif', + fontSize: "1.7rem", + fontWeight: 400, + letterSpacing: "0.02em", + lineHeight: 1.3, + marginBottom: 16, + color: "#fafafa", +}; + +const bodyStyle: React.CSSProperties = { + fontSize: "0.98rem", + lineHeight: 1.7, + color: "#bbb", + marginBottom: 0, +}; + +const hintStyle: React.CSSProperties = { + margin: "20px 0 0", + padding: "12px 18px", + borderLeft: "2px solid #5a4a3a", + background: "rgba(200, 168, 130, 0.06)", + fontSize: "0.86rem", + color: "#c8a882", + fontStyle: "italic", +}; + +const progressTrackStyle: React.CSSProperties = { + display: "flex", + gap: 6, + marginTop: 36, + marginBottom: 28, +}; + +const dotProgressStyle: React.CSSProperties = { + height: 4, + borderRadius: 2, + transition: "width 0.5s cubic-bezier(0.22, 1, 0.36, 1), background 0.3s", +}; + +const btnRowStyle: React.CSSProperties = { + display: "flex", + alignItems: "center", + gap: 10, +}; + +const btnStyle: React.CSSProperties = { + padding: "10px 22px", + fontSize: "0.82rem", + letterSpacing: "0.12em", + textTransform: "uppercase", + border: "1px solid", + cursor: "pointer", + fontFamily: "inherit", + transition: "all 0.3s cubic-bezier(0.22, 1, 0.36, 1)", + fontWeight: 400, +}; + +const btnGhostStyle: React.CSSProperties = { + background: "transparent", + borderColor: "#444", + color: "#888", +}; + +const btnPrimaryStyle: React.CSSProperties = { + background: "#c8a882", + borderColor: "#c8a882", + color: "#1a1a1a", + fontWeight: 500, +};