From 562eba412cb7b6ff7ce67c795e91caaa52391c00 Mon Sep 17 00:00:00 2001 From: nieao Date: Sat, 9 May 2026 19:15:05 +0800 Subject: [PATCH 1/4] 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, +}; From 8f85b8567992edd86504499dde001e1ef8133e8b Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Thu, 21 May 2026 10:56:10 +0800 Subject: [PATCH 2/4] fix(onboarding): wire to i18n + add missing ua-fade-in keyframes - Replace hardcoded Chinese strings with t.onboarding.* via useI18n - Add `onboarding` namespace to en / zh / zh-TW / ja / ko / ru locales - Inject @keyframes ua-fade-in (was referenced in inline style but never defined, so the overlay popped in instead of fading) --- .../src/components/OnboardingOverlay.tsx | 50 ++++--------------- .../packages/dashboard/src/locales/en.ts | 34 +++++++++++++ .../packages/dashboard/src/locales/ja.ts | 34 +++++++++++++ .../packages/dashboard/src/locales/ko.ts | 34 +++++++++++++ .../packages/dashboard/src/locales/ru.ts | 34 +++++++++++++ .../packages/dashboard/src/locales/zh-TW.ts | 34 +++++++++++++ .../packages/dashboard/src/locales/zh.ts | 34 +++++++++++++ 7 files changed, 215 insertions(+), 39 deletions(-) diff --git a/understand-anything-plugin/packages/dashboard/src/components/OnboardingOverlay.tsx b/understand-anything-plugin/packages/dashboard/src/components/OnboardingOverlay.tsx index 2c05256..71b968c 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/OnboardingOverlay.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/OnboardingOverlay.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from "react"; +import { useI18n } from "../contexts/I18nContext"; /** * First-visit onboarding overlay. @@ -11,41 +12,9 @@ import { useState, useEffect } from "react"; 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 { t } = useI18n(); + const STEPS = t.onboarding.steps; const [stepIdx, setStepIdx] = useState(0); const [open, setOpen] = useState(false); @@ -77,12 +46,13 @@ export default function OnboardingOverlay() { if (e.target === e.currentTarget) dismiss(false); }} > +
0{stepIdx + 1} / 0{STEPS.length} - UNDERSTAND-ANYTHING · 入门 + {t.onboarding.header}

{step.title}

@@ -113,7 +83,7 @@ export default function OnboardingOverlay() { onClick={() => dismiss(true)} style={{ ...btnStyle, ...btnGhostStyle }} > - 不再显示 + {t.onboarding.skipForever}
{!isFirst && ( @@ -122,7 +92,7 @@ export default function OnboardingOverlay() { onClick={() => setStepIdx(stepIdx - 1)} style={{ ...btnStyle, ...btnGhostStyle }} > - 上一步 + {t.onboarding.prev} )} {!isLast ? ( @@ -131,7 +101,7 @@ export default function OnboardingOverlay() { onClick={() => setStepIdx(stepIdx + 1)} style={{ ...btnStyle, ...btnPrimaryStyle }} > - 下一步 + {t.onboarding.next} ) : ( )}
@@ -148,6 +118,8 @@ export default function OnboardingOverlay() { ); } +const KEYFRAMES = `@keyframes ua-fade-in { from { opacity: 0 } to { opacity: 1 } }`; + // ----- styles (inline 避免依赖 css 文件) ----- const overlayStyle: React.CSSProperties = { diff --git a/understand-anything-plugin/packages/dashboard/src/locales/en.ts b/understand-anything-plugin/packages/dashboard/src/locales/en.ts index 39a79e1..1ca3db2 100644 --- a/understand-anything-plugin/packages/dashboard/src/locales/en.ts +++ b/understand-anything-plugin/packages/dashboard/src/locales/en.ts @@ -267,6 +267,40 @@ export const en = { pathFinder: { title: "Find path between nodes (P)", }, + onboarding: { + header: "UNDERSTAND-ANYTHING · GET STARTED", + skipForever: "Don't show again", + prev: "Previous", + next: "Next", + finish: "Start exploring", + steps: [ + { + title: "Welcome to the knowledge graph", + body: "The dots and lines you see are entities and relations that Understand-Anything extracted from this project (code / wiki). Each node is a file, concept, entity, or claim.", + hint: "Five steps to cover the core operations", + }, + { + title: "Three views at the top", + body: "Overview shows the big picture (force-directed). Learn follows a preset learning path. Deep Dive shows type and complexity stats. Each view answers a different question.", + hint: "Decide what you're asking before you switch", + }, + { + title: "Search + click a node", + body: "The top search box fuzzy-matches node name / summary / tags. Click any node and the right panel opens with summary, neighbors, and Open Article.", + hint: "Search centers and highlights; clicking a node highlights its edges", + }, + { + title: "Layer switch + Project Tour", + body: "The layer tabs next to All filter the graph to one category, sourced from index.md. Project Tour on the right walks you through the editor's preset sequence.", + hint: "Use Layer when nodes are too dense; start Tour when you have no entry point", + }, + { + title: "More hidden features", + body: "The top bar also has Filter (by type / complexity), Export (export the graph), Path (find a path between two nodes), and Theme. Press Shift + ? for the full keyboard shortcuts.", + hint: "Expand them when you need them — no need to memorize all at once", + }, + ], + }, }; export default en; \ No newline at end of file diff --git a/understand-anything-plugin/packages/dashboard/src/locales/ja.ts b/understand-anything-plugin/packages/dashboard/src/locales/ja.ts index ea468ad..883cd14 100644 --- a/understand-anything-plugin/packages/dashboard/src/locales/ja.ts +++ b/understand-anything-plugin/packages/dashboard/src/locales/ja.ts @@ -267,6 +267,40 @@ export const ja = { pathFinder: { title: "ノード間のパスを検索 (P)", }, + onboarding: { + header: "UNDERSTAND-ANYTHING · はじめに", + skipForever: "次回から表示しない", + prev: "前へ", + next: "次へ", + finish: "探索を始める", + steps: [ + { + title: "知識グラフへようこそ", + body: "表示されているノードとエッジは、Understand-Anything がこのプロジェクト(コード / wiki)から抽出したエンティティと関係です。各ノードはファイル、概念、エンティティ、または記述を表します。", + hint: "5 ステップで主要な操作を確認します", + }, + { + title: "上部の 3 つのビュー", + body: "Overview は全体像(力学的レイアウト)、Learn はあらかじめ用意された学習パス、Deep Dive はタイプ / 複雑度の統計を表示します。それぞれ異なる問いに答えるためのビューです。", + hint: "切り替える前に、何を知りたいかを明確に", + }, + { + title: "検索 + ノードクリック", + body: "上部の検索ボックスはノード名 / summary / タグをあいまい検索します。任意のノードをクリックすると、右側のパネルに summary、隣接ノード、Open Article ボタンが表示されます。", + hint: "検索はノードを中央寄せ・ハイライト、クリックは隣接エッジをハイライトします", + }, + { + title: "Layer 切替 + Project Tour", + body: "上部 All の隣にある layer タブは index.md に基づいて 1 つのカテゴリだけを表示します。右側の Project Tour は編集者が用意した順序でガイドします。", + hint: "ノードが多すぎるときは Layer、入り口がわからないときは Tour", + }, + { + title: "その他の隠れた機能", + body: "上部バーには Filter(タイプ / 複雑度で絞り込み)、Export(グラフを書き出す)、Path(2 つのノード間のパスを検索)、Theme(テーマ切替)もあります。Shift + ? で全キーボードショートカットを確認できます。", + hint: "必要になったときに開けば十分。一度に覚える必要はありません", + }, + ], + }, }; export default ja; \ No newline at end of file diff --git a/understand-anything-plugin/packages/dashboard/src/locales/ko.ts b/understand-anything-plugin/packages/dashboard/src/locales/ko.ts index bece3a1..2af3431 100644 --- a/understand-anything-plugin/packages/dashboard/src/locales/ko.ts +++ b/understand-anything-plugin/packages/dashboard/src/locales/ko.ts @@ -267,6 +267,40 @@ edgeLabels: { pathFinder: { title: "노드 간 경로 찾기 (P)", }, + onboarding: { + header: "UNDERSTAND-ANYTHING · 시작하기", + skipForever: "다시 보지 않기", + prev: "이전", + next: "다음", + finish: "탐색 시작", + steps: [ + { + title: "지식 그래프에 오신 것을 환영합니다", + body: "보이는 점과 선은 Understand-Anything이 이 프로젝트(코드 / 위키)에서 추출한 엔티티와 관계입니다. 각 노드는 파일, 개념, 엔티티 또는 진술을 나타냅니다.", + hint: "5단계로 핵심 조작을 살펴봅니다", + }, + { + title: "상단의 세 가지 뷰", + body: "Overview는 전체 모습(포스 디렉티드), Learn은 미리 정의된 학습 경로, Deep Dive는 타입 / 복잡도 통계를 보여줍니다. 각 뷰는 서로 다른 질문에 답합니다.", + hint: "전환하기 전에 무엇을 묻고 싶은지 정하세요", + }, + { + title: "검색 + 노드 클릭", + body: "상단 검색창은 노드 이름 / summary / 태그를 퍼지 매칭합니다. 노드를 클릭하면 오른쪽 패널에 summary, 이웃 목록, Open Article 버튼이 나타납니다.", + hint: "검색은 노드를 중앙 정렬·강조하고, 클릭은 인접 엣지를 강조합니다", + }, + { + title: "Layer 전환 + Project Tour", + body: "상단 All 옆의 layer 탭은 index.md를 기반으로 한 카테고리만 표시합니다. 오른쪽의 Project Tour는 편집자가 설정한 순서대로 안내합니다.", + hint: "노드가 너무 빽빽하면 Layer, 시작점이 없으면 Tour를 사용하세요", + }, + { + title: "숨겨진 추가 기능", + body: "상단 바에는 Filter(타입 / 복잡도로 필터링), Export(그래프 내보내기), Path(두 노드 사이 경로 찾기), Theme(테마 전환)도 있습니다. Shift + ?를 누르면 전체 키보드 단축키를 볼 수 있습니다.", + hint: "필요할 때 펼쳐 보면 됩니다. 한 번에 다 외울 필요는 없습니다", + }, + ], + }, }; export default ko; \ No newline at end of file diff --git a/understand-anything-plugin/packages/dashboard/src/locales/ru.ts b/understand-anything-plugin/packages/dashboard/src/locales/ru.ts index d43cd93..c556a76 100644 --- a/understand-anything-plugin/packages/dashboard/src/locales/ru.ts +++ b/understand-anything-plugin/packages/dashboard/src/locales/ru.ts @@ -267,6 +267,40 @@ export const ru = { pathFinder: { title: "Найти путь между узлами (P)", }, + onboarding: { + header: "UNDERSTAND-ANYTHING · НАЧАЛО РАБОТЫ", + skipForever: "Больше не показывать", + prev: "Назад", + next: "Далее", + finish: "Начать исследование", + steps: [ + { + title: "Добро пожаловать в граф знаний", + body: "Точки и линии — это сущности и связи, извлечённые Understand-Anything из этого проекта (код / wiki). Каждый узел — это файл, концепция, сущность или утверждение.", + hint: "Пять шагов охватят основные операции", + }, + { + title: "Три вида сверху", + body: "Overview показывает общую картину (force-directed). Learn ведёт по заранее заданному учебному пути. Deep Dive показывает статистику по типам и сложности. Каждый вид отвечает на свой вопрос.", + hint: "Перед переключением определитесь, о чём вы спрашиваете", + }, + { + title: "Поиск + клик по узлу", + body: "Поисковая строка сверху делает нечёткое совпадение по имени узла, summary и тегам. Кликните по узлу — справа откроется панель с summary, соседями и кнопкой Open Article.", + hint: "Поиск центрирует и подсвечивает; клик подсвечивает соседние рёбра", + }, + { + title: "Переключение Layer + Project Tour", + body: "Вкладки layer рядом с All фильтруют граф по одной категории на основе index.md. Project Tour справа проводит вас по заранее заданной последовательности.", + hint: "Используйте Layer, когда узлов слишком много; запустите Tour, если непонятно с чего начать", + }, + { + title: "Другие скрытые возможности", + body: "В верхней панели также есть Filter (фильтр по типу / сложности), Export (экспорт графа), Path (поиск пути между двумя узлами) и Theme (смена темы). Нажмите Shift + ?, чтобы увидеть полный список горячих клавиш.", + hint: "Открывайте их по мере необходимости — не нужно запоминать всё сразу", + }, + ], + }, }; export default ru; diff --git a/understand-anything-plugin/packages/dashboard/src/locales/zh-TW.ts b/understand-anything-plugin/packages/dashboard/src/locales/zh-TW.ts index c3ceb06..5a881d5 100644 --- a/understand-anything-plugin/packages/dashboard/src/locales/zh-TW.ts +++ b/understand-anything-plugin/packages/dashboard/src/locales/zh-TW.ts @@ -267,6 +267,40 @@ export const zhTW = { pathFinder: { title: "尋找節點間路徑 (P)", }, + onboarding: { + header: "UNDERSTAND-ANYTHING · 入門", + skipForever: "不再顯示", + prev: "上一步", + next: "下一步", + finish: "開始探索", + steps: [ + { + 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 zhTW; \ No newline at end of file diff --git a/understand-anything-plugin/packages/dashboard/src/locales/zh.ts b/understand-anything-plugin/packages/dashboard/src/locales/zh.ts index 1bc7302..42dc24f 100644 --- a/understand-anything-plugin/packages/dashboard/src/locales/zh.ts +++ b/understand-anything-plugin/packages/dashboard/src/locales/zh.ts @@ -267,6 +267,40 @@ export const zh = { pathFinder: { title: "查找节点间路径 (P)", }, + onboarding: { + header: "UNDERSTAND-ANYTHING · 入门", + skipForever: "不再显示", + prev: "上一步", + next: "下一步", + finish: "开始探索", + steps: [ + { + 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 zh; \ No newline at end of file From a29585874e5eb528a0c77fe6367a01835a87dbec Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Thu, 21 May 2026 11:06:54 +0800 Subject: [PATCH 3/4] fix(onboarding): include class/function in node-type description Schema (packages/core/src/schema.ts) defines node types: file, function, class, module, concept, config, document, ... The welcome step body only listed "file, concept, entity, claim", missing the most common code-side types. Updated all 6 locales (en / zh / zh-TW / ja / ko / ru) to mention file / class / function from code plus concept / entity / claim from the knowledge wiki. --- understand-anything-plugin/packages/dashboard/src/locales/en.ts | 2 +- understand-anything-plugin/packages/dashboard/src/locales/ja.ts | 2 +- understand-anything-plugin/packages/dashboard/src/locales/ko.ts | 2 +- understand-anything-plugin/packages/dashboard/src/locales/ru.ts | 2 +- .../packages/dashboard/src/locales/zh-TW.ts | 2 +- understand-anything-plugin/packages/dashboard/src/locales/zh.ts | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/understand-anything-plugin/packages/dashboard/src/locales/en.ts b/understand-anything-plugin/packages/dashboard/src/locales/en.ts index 1ca3db2..8377718 100644 --- a/understand-anything-plugin/packages/dashboard/src/locales/en.ts +++ b/understand-anything-plugin/packages/dashboard/src/locales/en.ts @@ -276,7 +276,7 @@ export const en = { steps: [ { title: "Welcome to the knowledge graph", - body: "The dots and lines you see are entities and relations that Understand-Anything extracted from this project (code / wiki). Each node is a file, concept, entity, or claim.", + body: "The dots and lines you see are entities and relations Understand-Anything extracted from this project. A node can be a file, class, or function from the code — or a concept, entity, or claim from a knowledge wiki.", hint: "Five steps to cover the core operations", }, { diff --git a/understand-anything-plugin/packages/dashboard/src/locales/ja.ts b/understand-anything-plugin/packages/dashboard/src/locales/ja.ts index 883cd14..e222d26 100644 --- a/understand-anything-plugin/packages/dashboard/src/locales/ja.ts +++ b/understand-anything-plugin/packages/dashboard/src/locales/ja.ts @@ -276,7 +276,7 @@ export const ja = { steps: [ { title: "知識グラフへようこそ", - body: "表示されているノードとエッジは、Understand-Anything がこのプロジェクト(コード / wiki)から抽出したエンティティと関係です。各ノードはファイル、概念、エンティティ、または記述を表します。", + body: "表示されているノードとエッジは、Understand-Anything がこのプロジェクトから抽出したエンティティと関係です。ノードはコード側のファイル・クラス・関数のこともあれば、知識 wiki 側の概念・エンティティ・記述のこともあります。", hint: "5 ステップで主要な操作を確認します", }, { diff --git a/understand-anything-plugin/packages/dashboard/src/locales/ko.ts b/understand-anything-plugin/packages/dashboard/src/locales/ko.ts index 2af3431..de894a7 100644 --- a/understand-anything-plugin/packages/dashboard/src/locales/ko.ts +++ b/understand-anything-plugin/packages/dashboard/src/locales/ko.ts @@ -276,7 +276,7 @@ edgeLabels: { steps: [ { title: "지식 그래프에 오신 것을 환영합니다", - body: "보이는 점과 선은 Understand-Anything이 이 프로젝트(코드 / 위키)에서 추출한 엔티티와 관계입니다. 각 노드는 파일, 개념, 엔티티 또는 진술을 나타냅니다.", + body: "보이는 점과 선은 Understand-Anything이 이 프로젝트에서 추출한 엔티티와 관계입니다. 노드는 코드 쪽의 파일·클래스·함수일 수도 있고, 지식 위키 쪽의 개념·엔티티·진술일 수도 있습니다.", hint: "5단계로 핵심 조작을 살펴봅니다", }, { diff --git a/understand-anything-plugin/packages/dashboard/src/locales/ru.ts b/understand-anything-plugin/packages/dashboard/src/locales/ru.ts index c556a76..27cc554 100644 --- a/understand-anything-plugin/packages/dashboard/src/locales/ru.ts +++ b/understand-anything-plugin/packages/dashboard/src/locales/ru.ts @@ -276,7 +276,7 @@ export const ru = { steps: [ { title: "Добро пожаловать в граф знаний", - body: "Точки и линии — это сущности и связи, извлечённые Understand-Anything из этого проекта (код / wiki). Каждый узел — это файл, концепция, сущность или утверждение.", + body: "Точки и линии — это сущности и связи, извлечённые Understand-Anything из этого проекта. Узлом может быть файл, класс или функция из кода — либо концепция, сущность или утверждение из вики знаний.", hint: "Пять шагов охватят основные операции", }, { diff --git a/understand-anything-plugin/packages/dashboard/src/locales/zh-TW.ts b/understand-anything-plugin/packages/dashboard/src/locales/zh-TW.ts index 5a881d5..2ffab48 100644 --- a/understand-anything-plugin/packages/dashboard/src/locales/zh-TW.ts +++ b/understand-anything-plugin/packages/dashboard/src/locales/zh-TW.ts @@ -276,7 +276,7 @@ export const zhTW = { steps: [ { title: "歡迎進入知識圖", - body: "你看到的圓點和連線是 Understand-Anything 把這份專案(程式碼 / wiki)抽出來的實體和關係。每個節點是一個檔案、概念、實體或斷言。", + body: "你看到的圓點和連線是 Understand-Anything 把這份專案抽出來的實體和關係。節點可以是程式碼裡的檔案、類別、函式,也可以是知識 wiki 裡的概念、實體或斷言。", hint: "5 步以內帶你過完核心操作", }, { diff --git a/understand-anything-plugin/packages/dashboard/src/locales/zh.ts b/understand-anything-plugin/packages/dashboard/src/locales/zh.ts index 42dc24f..c308eb4 100644 --- a/understand-anything-plugin/packages/dashboard/src/locales/zh.ts +++ b/understand-anything-plugin/packages/dashboard/src/locales/zh.ts @@ -276,7 +276,7 @@ export const zh = { steps: [ { title: "欢迎进入知识图", - body: "你看到的圆点和连线是 Understand-Anything 把这份项目(代码 / wiki)抽出来的实体和关系。每个节点是一个文件、概念、实体或断言。", + body: "你看到的圆点和连线是 Understand-Anything 把这份项目抽出来的实体和关系。节点可以是代码里的文件、类、函数,也可以是知识 wiki 里的概念、实体或断言。", hint: "5 步以内带你过完核心操作", }, { From 9dfcce73a595e6088a29a02c27d14cac84c922fe Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Thu, 21 May 2026 11:07:02 +0800 Subject: [PATCH 4/4] refactor(onboarding): theme tokens, lifted state, a11y - Replace hardcoded hex with var(--color-*) and CJK font stacks with var(--font-sans) / var(--font-heading) so the overlay tracks the theme picker and uses the project's typography (DM Serif Display). - Lift dismiss/visibility state to Dashboard (shouldShowOnboarding + showOnboarding useState + dismissOnboarding callback). Gate the Suspense mount with a boolean so the lazy chunk is only fetched on first visit, matching the PathFinderModal / KeyboardShortcutsHelp mount pattern. - Add capture-phase Escape handler (stopPropagation prevents the global shortcut chain from also firing) and role="dialog" / aria-modal / aria-labelledby on the card for screen readers. --- .../packages/dashboard/src/App.tsx | 25 +++- .../src/components/OnboardingOverlay.tsx | 114 ++++++++++-------- 2 files changed, 82 insertions(+), 57 deletions(-) diff --git a/understand-anything-plugin/packages/dashboard/src/App.tsx b/understand-anything-plugin/packages/dashboard/src/App.tsx index cc7c0b5..4d0b396 100644 --- a/understand-anything-plugin/packages/dashboard/src/App.tsx +++ b/understand-anything-plugin/packages/dashboard/src/App.tsx @@ -36,8 +36,16 @@ const OnboardingOverlay = lazy(() => import("./components/OnboardingOverlay")); const DEMO_MODE = import.meta.env.VITE_DEMO_MODE === "true"; const SESSION_TOKEN_KEY = "understand-anything-token"; +const ONBOARDING_DISMISSED_KEY = "ua-onboarding-dismissed-v1"; type SidebarTab = "info" | "files"; +function shouldShowOnboarding(): boolean { + if (typeof window === "undefined") return false; + const params = new URLSearchParams(window.location.search); + if (params.get("onboard") === "force") return true; + return window.localStorage.getItem(ONBOARDING_DISMISSED_KEY) !== "1"; +} + /** Resolve data file URL — in demo mode, use env var URLs; otherwise use local paths with token. */ function dataUrl(fileName: string, token: string | null): string { if (DEMO_MODE) { @@ -236,6 +244,13 @@ function DashboardContent({ const toggleShowFunctionsInClassView = useDashboardStore((s) => s.toggleShowFunctionsInClassView); const [showKeyboardHelp, setShowKeyboardHelp] = useState(false); const [sidebarTab, setSidebarTab] = useState("info"); + const [showOnboarding, setShowOnboarding] = useState(shouldShowOnboarding); + const dismissOnboarding = useCallback((remember: boolean) => { + if (remember && typeof window !== "undefined") { + window.localStorage.setItem(ONBOARDING_DISMISSED_KEY, "1"); + } + setShowOnboarding(false); + }, []); const viewMode = useDashboardStore((s) => s.viewMode); const setViewMode = useDashboardStore((s) => s.setViewMode); const isKnowledgeGraph = useDashboardStore((s) => s.isKnowledgeGraph); @@ -685,10 +700,12 @@ function DashboardContent({ )} - {/* First-visit onboarding overlay — auto-hides after dismiss via localStorage. */} - - - + {/* First-visit onboarding overlay — only mounted when needed so its chunk is lazy-loaded on demand. */} + {showOnboarding && ( + + + + )}
); } diff --git a/understand-anything-plugin/packages/dashboard/src/components/OnboardingOverlay.tsx b/understand-anything-plugin/packages/dashboard/src/components/OnboardingOverlay.tsx index 71b968c..a377a12 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/OnboardingOverlay.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/OnboardingOverlay.tsx @@ -1,53 +1,59 @@ -import { useState, useEffect } from "react"; +import { useEffect, useState } from "react"; import { useI18n } from "../contexts/I18nContext"; /** - * First-visit onboarding overlay. + * First-visit onboarding overlay (controlled). * - * Renders 5 dismissible steps that teach a new user how to operate the dashboard. - * State persists in localStorage so returning users are not interrupted. + * Parent owns the visibility + persistence state (see App.tsx). This component + * only renders the modal and reports the user's intent via onDismiss: + * - onDismiss(true) → "Skip" / Finish — parent should persist. + * - onDismiss(false) → backdrop click / Escape — parent should close without persisting. * - * Force-show via `?onboard=force` URL param (useful for screenshots / demos). + * Force-show is handled by the parent (see `shouldShowOnboarding` in App.tsx). */ -const STORAGE_KEY = "ua-onboarding-dismissed-v1"; +interface Props { + onDismiss: (remember: boolean) => void; +} -export default function OnboardingOverlay() { +const TITLE_ID = "ua-onboarding-title"; + +export default function OnboardingOverlay({ onDismiss }: Props) { const { t } = useI18n(); const STEPS = t.onboarding.steps; const [stepIdx, setStepIdx] = useState(0); - const [open, setOpen] = useState(false); + // Capture-phase Escape handler — runs before the global keydown chain so we + // can stopPropagation() and prevent it from also firing. 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 handler = (e: KeyboardEvent) => { + if (e.key === "Escape") { + e.stopPropagation(); + onDismiss(false); + } + }; + document.addEventListener("keydown", handler, true); + return () => document.removeEventListener("keydown", handler, true); + }, [onDismiss]); 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); + if (e.target === e.currentTarget) onDismiss(false); }} > -
+
0{stepIdx + 1} / 0{STEPS.length} @@ -55,11 +61,13 @@ export default function OnboardingOverlay() { {t.onboarding.header}
-

{step.title}

+

+ {step.title} +

{step.body}

{step.hint && (
- · + · {step.hint}
)} @@ -70,7 +78,10 @@ export default function OnboardingOverlay() { key={i} style={{ ...dotProgressStyle, - background: i === stepIdx ? "#c8a882" : "#444", + background: + i === stepIdx + ? "var(--color-accent)" + : "var(--color-border-medium)", width: i === stepIdx ? 28 : 6, }} /> @@ -80,7 +91,7 @@ export default function OnboardingOverlay() {