用户引导

This commit is contained in:
董宏凯 committed 2026-08-26 09:41:53 +08:00
1 parent 62a3faa7c8
commit c6fcf51e0c
12 files changed
+785 -77

No files matched your search

@@ -835,11 +835,14 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
);
const cardLayout = (
<Card className="group flex min-w-0 flex-col transition-shadow hover:shadow-md">
<Card
className="group flex min-w-0 flex-col transition-shadow hover:shadow-md"
data-agent-guide="agent-card"
>
<CardHeader className="min-w-0 pb-2">
<div className="flex min-w-0 items-start justify-between gap-2">
<div className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden">
<div className="agent-avatar bg-primary/10 text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-lg">
<div className="agent-avatar bg-primary/10 text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-lg">
<BotIcon className="h-5 w-5" />
</div>
<div className="min-w-0 flex-1 overflow-hidden">
@@ -893,8 +896,11 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
);
const listLayout = (
<div className="group flex min-w-0 items-center gap-3 rounded-lg border p-2.5 transition-shadow hover:shadow-sm dark:border-[#213D5E] dark:bg-[#02244C]">
<div className="agent-avatar bg-primary/10 text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-lg">
<div
className="group flex min-w-0 items-center gap-3 rounded-lg border p-2.5 transition-shadow hover:shadow-sm dark:border-[#213D5E] dark:bg-[#02244C]"
data-agent-guide="agent-card"
>
<div className="agent-avatar bg-primary/10 text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-lg">
<BotIcon className="h-5 w-5" />
</div>
<div className="min-w-0 flex-1 overflow-hidden">
@@ -980,7 +986,7 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
? "正在加载 SOUL.md..."
: "输入 SOUL.md 内容"
}
className="max-h-[45vh] min-h-48 resize-y text-xs"
className="max-h-[45vh] min-h-48 resize-y text-xs"
/>
</div>
<div className="space-y-2">
@@ -1,6 +1,6 @@
"use client";
import { ArrowDownUpIcon, BotIcon, Loader2, LockIcon, PlusIcon, ShieldIcon, XIcon } from "lucide-react";
import { ArrowDownUpIcon, BotIcon, Compass, Loader2, LockIcon, PlusIcon, ShieldIcon, XIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { toast } from "sonner";
@@ -21,6 +21,11 @@ import { usePrivateSquareStatus, usePublishSquares, useViewMode } from "@/core/s
import { AgentCard } from "./agent-card";
import { SentimentAgentCard } from "./sentiment-agent-card";
import {
AgentGuide,
isAgentGuideEnvironment,
startAgentGuide,
} from "./agent-guide";
const PAGE_SIZE = 12;
@@ -398,10 +403,22 @@ export function AgentGallery() {
{t.agents.description}
</p>
</div>
<Button onClick={handleNewAgent}>
<PlusIcon className="mr-1.5 h-4 w-4" />
{t.agents.newAgent}
</Button>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
className="text-muted-foreground hover:text-foreground"
title="重新播放智能体使用引导"
onClick={() => startAgentGuide("list")}
>
<Compass className="mr-1.5 h-4 w-4" />
使用引导
</Button>
<Button onClick={handleNewAgent} data-agent-guide="new-agent-btn">
<PlusIcon className="mr-1.5 h-4 w-4" />
{t.agents.newAgent}
</Button>
</div>
</div>
{/* Tabs */}
@@ -421,7 +438,10 @@ export function AgentGallery() {
>
<div className="mx-6 mt-4 flex flex-col gap-3">
<div className="flex items-center justify-between gap-3">
<TabsList className="w-auto max-w-full flex-wrap justify-start self-start">
<TabsList
className="w-auto max-w-full flex-wrap justify-start self-start"
data-agent-guide="tabs"
>
<TabsTrigger value="mine">我的</TabsTrigger>
{defaultSquare && (
<TabsTrigger
@@ -475,7 +495,8 @@ export function AgentGallery() {
</div>
</div>
{showSearchBar && (
<TagSearchBar
<div data-agent-guide="search">
<TagSearchBar
// Remount per tab so the internal keyword input / 展开 state also
// resets when the parent-owned filters are cleared on tab switch.
key={activeTab}
@@ -506,6 +527,7 @@ export function AgentGallery() {
</Select>
}
/>
</div>
)}
{publisher && (
<div className="flex items-center gap-2">
@@ -593,6 +615,7 @@ export function AgentGallery() {
</TabsContent>
)}
</Tabs>
<AgentGuide stage="list" eligible={isAgentGuideEnvironment()} />
</div>
);
}
@@ -0,0 +1,332 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { Guide, type GuideStep } from "tdesign-react";
import { env } from "@/env";
import { isEmbedActive, isHideChromeActive } from "@/core/embed/embed-session";
import { isWujieEnv } from "@/core/embed/wujie";
/**
* 智能体使用引导(三段式,基于 TDesign React 的 Guide 组件),完整覆盖
* 「浏览管理 → 创建与配置 → 发起问答」流程:
*
* - list :智能体列表页(/page/workspace/agents)——分区、搜索排序、卡片操作、
* 创建入口;最后一步「去创建智能体 →」直接跳到创建页衔接下一段。
* - create :创建页(/page/workspace/agents/new)——对话式创建、基本信息、模型、
* 知识空间、SOUL.md、技能、保存。
* - chat :智能体对话页(/page/workspace/agents/:id/chats/...)——侧栏切换与
* 历史、常用问题、输入框、右侧详情面板。
*
* 记忆语义与简洁模式引导(compact-mode-guide.tsx)一致:每个阶段各自记一个
* localStorage 标记,**只有用户显式点「完成」或「跳过」才写入**;中途刷新/离开
* 不记忆,下次进入该页面还会自动演示。各页头部的「使用引导」按钮可随时强制重放。
*
* 高亮目标通过 `data-agent-guide` 标记在各组件上;启动时按目标是否存在过滤步骤
* (如新用户无智能体卡片、无预设常用问题时自动剔除该步)——Guide 对找不到的
* 目标元素会抛错,不能盲传。
*/
export type AgentGuideStage = "list" | "create" | "chat";
const finishedKey = (stage: AgentGuideStage) =>
`deerflow.agent-guide.${stage}.finished`;
const startEventName = (stage: AgentGuideStage) =>
`agent-guide:start:${stage}`;
/** 手动触发某阶段的引导(各页面头部「使用引导」按钮调用,忽略已看过记忆)。 */
export function startAgentGuide(stage: AgentGuideStage) {
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent(startEventName(stage)));
}
}
/** 环境闸门:iframe 嵌入 / 去 chrome / 强制收起侧栏(无界)等环境不弹引导。 */
export function isAgentGuideEnvironment(): boolean {
return (
!env.FORCE_COLLAPSED_SIDEBAR &&
!isEmbedActive() &&
!isHideChromeActive() &&
!isWujieEnv()
);
}
/** 引导步骤高亮目标选择器;各目标元素由所在组件打上同名 data 标记。 */
const TARGET = {
// list 阶段(智能体列表页)
tabs: '[data-agent-guide="tabs"]',
search: '[data-agent-guide="search"]',
agentCard: '[data-agent-guide="agent-card"]',
newAgentBtn: '[data-agent-guide="new-agent-btn"]',
// create 阶段(创建页)
createChatInput: '[data-agent-guide="create-chat-input"]',
createBasic: '[data-agent-guide="create-basic"]',
createModel: '[data-agent-guide="create-model"]',
createKnowledge: '[data-agent-guide="create-knowledge"]',
createSoul: '[data-agent-guide="create-soul"]',
createSkills: '[data-agent-guide="create-skills"]',
createSaveBtn: '[data-agent-guide="create-save-btn"]',
// chat 阶段(智能体对话页)
chatSidebar: '[data-agent-guide="chat-sidebar"]',
chatQuestions: '[data-agent-guide="chat-questions"]',
chatInput: '[data-agent-guide="chat-input"]',
chatDetail: '[data-agent-guide="chat-detail"]',
} as const;
// 等布局稳定后再启动:Guide 只在步骤切换时测量目标位置,起步太早会把高亮框
// 定位到过渡中的坐标且不会自行纠正。
const START_DELAY_MS = 800;
interface StepSpec {
/** 高亮目标选择器;缺省(dialog 步骤)以 body 兜底。 */
selector?: string;
mode: "popup" | "dialog";
placement: NonNullable<GuideStep["placement"]>;
title: string;
body: string;
}
const LIST_STEPS: StepSpec[] = [
{
mode: "dialog",
placement: "center",
title: "欢迎使用智能体",
body: "这里是你的智能体中心。接下来花几分钟走完整个流程:浏览与管理智能体 → 创建与配置 → 发起问答。",
},
{
selector: TARGET.tabs,
mode: "popup",
placement: "bottom",
title: "分区浏览",
body: "「我的」放着你创建和收藏的智能体;各「广场」是他人发布共享的,可直接使用或收藏;「私密广场」需要密码访问(管理员还可以查看「内置智能体」)。",
},
{
selector: TARGET.search,
mode: "popup",
placement: "bottom",
title: "搜索与排序",
body: "支持关键词、标签搜索,可按最新创建、最近更新、名称排序;右侧按钮可在卡片 / 列表两种视图间切换。",
},
{
selector: TARGET.agentCard,
mode: "popup",
placement: "top",
title: "智能体卡片",
body: "卡片展示名称、简介、模型与技能徽章;底部操作栏支持收藏、置顶、打标签、发布到广场、复制、编辑、删除;点「对话」直接进入该智能体的问答。",
},
{
selector: TARGET.newAgentBtn,
mode: "popup",
placement: "bottom-left",
title: "创建智能体",
body: "准备好了就点这里,开始创建你的专属智能体。点「去创建智能体」直接进入下一站。",
},
];
const CREATE_STEPS: StepSpec[] = [
{
mode: "dialog",
placement: "center",
title: "创建智能体",
body: "页面分两栏:左侧直接与模型对话,让它帮你配置;右侧表单手动填写。两边内容实时联动,选顺手的方式即可。",
},
{
selector: TARGET.createChatInput,
mode: "popup",
placement: "top",
title: "对话式创建",
body: "在这里描述你想要的智能体,例如「帮我做一个周报助手,语气正式、要点条目化」。模型会追问细节并给出配置方案,你确认后方案自动填入右侧表单,可继续微调。",
},
{
selector: TARGET.createBasic,
mode: "popup",
placement: "left",
title: "基本信息",
body: "「名称」显示在卡片和对话页;「描述」用一句话说明它擅长什么,方便日后挑选。",
},
{
selector: TARGET.createModel,
mode: "popup",
placement: "left",
title: "运行模型与入库",
body: "「模型」选择该智能体被使用时调用的模型;「入库模板名」配置后,对话生成的文档可一键发布入库(不需要可留空)。",
},
{
selector: TARGET.createKnowledge,
mode: "popup",
placement: "left",
title: "知识空间",
body: "为智能体挂载知识库,回答时会优先检索其中的资料,让结果更贴合你的业务数据。",
},
{
selector: TARGET.createSoul,
mode: "popup",
placement: "left",
title: "SOUL.md · 人设",
body: "智能体的「灵魂文件」——写清角色定位、能力边界、工作步骤和输出格式。这份提示词直接决定回答质量,是最值得花时间打磨的配置。",
},
{
selector: TARGET.createSkills,
mode: "popup",
placement: "left",
title: "技能挂载",
body: "从可用技能中勾选要赋予的能力,已选技能会作为工具在对话中被调用;全部不选则是纯对话型智能体。",
},
{
selector: TARGET.createSaveBtn,
mode: "popup",
placement: "top",
title: "创建并保存",
body: "配置完成后点这里保存(左侧对话只是试用场,内容不会自动保存)。保存成功会回到智能体列表。",
},
{
mode: "dialog",
placement: "center",
title: "开始问答",
body: "回到列表页后,点智能体卡片上的「对话」按钮即可开始问答;进入对话页还会有最后一段引导。",
},
];
const CHAT_STEPS: StepSpec[] = [
{
mode: "dialog",
placement: "center",
title: "智能体问答",
body: "一切就绪!这里就是与智能体对话的工作区,快速认识一下。",
},
{
selector: TARGET.chatSidebar,
mode: "popup",
placement: "right",
title: "切换与历史",
body: "左栏顶部可在「我的 / 广场」智能体之间切换;下方是当前智能体的历史会话,点击任意一条继续上次的对话。",
},
{
selector: TARGET.chatQuestions,
mode: "popup",
placement: "bottom",
title: "常用问题",
body: "智能体预设的开场问题,点击即可直接提问,快速体验它的能力。",
},
{
selector: TARGET.chatInput,
mode: "popup",
placement: "top",
title: "输入框",
body: "输入问题后发送即可开始问答;新对话里还可以挂载知识空间,让回答优先检索知识库。",
},
{
selector: TARGET.chatDetail,
mode: "popup",
placement: "left",
title: "智能体详情",
body: "右侧面板展示当前智能体的配置(人设、技能、常用问题),可以直接修改并即时生效;里面的快捷提问也能一键发送。",
},
];
const STAGE_STEPS: Record<AgentGuideStage, StepSpec[]> = {
list: LIST_STEPS,
create: CREATE_STEPS,
chat: CHAT_STEPS,
};
function isBrowser(): boolean {
return typeof window !== "undefined";
}
function isStageFinished(stage: AgentGuideStage): boolean {
return (
isBrowser() && localStorage.getItem(finishedKey(stage)) === "1"
);
}
function markStageFinished(stage: AgentGuideStage) {
if (isBrowser()) localStorage.setItem(finishedKey(stage), "1");
}
function buildSteps(stage: AgentGuideStage): GuideStep[] {
if (!isBrowser()) return [];
return STAGE_STEPS[stage]
.filter((spec) => !spec.selector || document.querySelector(spec.selector))
.map((spec) => ({
element: spec.selector ?? (() => document.body),
mode: spec.mode,
placement: spec.placement,
title: spec.title,
body: spec.body,
}));
}
/**
* 智能体三段引导的运行组件:`stage` 决定步骤内容,`eligible` 为false 时完全
* 静默(不监听、不自动弹)。挂在三个页面各自的作用域内。
*/
export function AgentGuide({
stage,
eligible,
}: {
stage: AgentGuideStage;
eligible: boolean;
}) {
const router = useRouter();
const [current, setCurrent] = useState(-1);
const stepsRef = useRef<GuideStep[]>([]);
const start = useCallback(() => {
const steps = buildSteps(stage);
if (steps.length === 0) return;
stepsRef.current = steps;
setCurrent(0);
}, [stage]);
// 自动演示:没显式结束过(没点过完成/跳过)时,每次进入该页都演示;是否
// 「已看过」只由 finish 回调(完成/跳过)写入。
const autoTriedRef = useRef(false);
useEffect(() => {
if (!eligible || autoTriedRef.current) return;
autoTriedRef.current = true;
if (isStageFinished(stage)) return;
const timer = setTimeout(start, START_DELAY_MS);
return () => {
clearTimeout(timer);
// 组件在计时窗口内卸载(用户迅速离开页面)则下次进入再演示。
autoTriedRef.current = false;
};
}, [eligible, stage, start]);
// 手动触发:页面头部「使用引导」按钮广播事件,忽略已看过记忆强制重放。
useEffect(() => {
if (!eligible) return;
const handler = () => start();
window.addEventListener(startEventName(stage), handler);
return () =>
window.removeEventListener(startEventName(stage), handler);
}, [eligible, stage, start]);
// 完成 / 跳过是用户显式结束引导 —— 只有这两种情况才记忆「已看过」。
// list 阶段的最后一步按钮文案是「去创建智能体 →」,完成即跳转衔接下一段。
const finish = useCallback(() => {
markStageFinished(stage);
setCurrent(-1);
if (stage === "list") {
router.push("/page/workspace/agents/new");
}
}, [stage, router]);
if (!eligible) return null;
return (
<Guide
current={current}
steps={stepsRef.current}
onChange={setCurrent}
onFinish={finish}
onSkip={finish}
finishButtonProps={
stage === "list" ? { content: "去创建智能体 →" } : undefined
}
/>
);
}
@@ -152,6 +152,7 @@ export function ThemeSwitcher({ tone }: { tone: AppearanceControlTone }) {
return (
<div
className="relative"
data-compact-guide="theme-switcher"
onMouseEnter={() => {
cancelClose();
setOpen(true);
@@ -251,6 +252,7 @@ export function LayoutModeSwitcher({
className={cn("size-8 shrink-0 rounded-sm", triggerToneClass(tone))}
title="界面模式"
aria-label="界面模式"
data-compact-guide="layout-mode"
>
<TriggerIcon className="size-4" />
</Button>
@@ -0,0 +1,220 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { Guide, type GuideStep } from "tdesign-react";
/**
* 简洁模式(隐藏全局导航栏 + 页面侧边栏的布局)新手引导,基于 TDesign React 的
* Guide 组件(https://tdesign.tencent.com/react/components/guide)。
*
* 触发方式:
* - 自动:简洁模式问答落地页自动演示,直到用户显式点「完成」或「跳过」才写
* localStorage 记忆;中途刷新/离开不记忆,下次进入还会再演示。
* - 手动:左侧「设置和更多」下拉的「界面使用引导」。问答页内通过
* COMPACT_GUIDE_START_EVENT 直接启动;其它页面先落 sessionStorage 再导航到
* /chats/new,由本组件在挂载后消费。
*
* 高亮目标通过 `data-compact-guide` 标记在各组件上(见 TARGET 选择器)。启动时逐个
* 检查目标是否存在(新用户无聊天记录、侧栏被收起等场景对应区块不渲染),缺失的
* 步骤直接剔除 —— Guide 对找不到的目标元素会抛错,不能盲传。
*/
const COMPACT_GUIDE_DONE_KEY = "deerflow.compact-guide-finished";
const COMPACT_GUIDE_PENDING_KEY = "deerflow.compact-guide-pending";
/** 同页手动触发引导的窗口事件名(「设置和更多」→「界面使用引导」)。 */
export const COMPACT_GUIDE_START_EVENT = "compact-guide:start";
/** 引导步骤高亮目标选择器;各目标元素由所在组件打上同名 data 标记。 */
const TARGET = {
nav: '[data-compact-guide="sidebar-nav"]',
recentChats: '[data-compact-guide="recent-chats"]',
settingsMenu: '[data-compact-guide="settings-menu"]',
themeSwitcher: '[data-compact-guide="theme-switcher"]',
layoutMode: '[data-compact-guide="layout-mode"]',
promptInput: '[data-compact-guide="prompt-input"]',
} as const;
// 等侧栏宽度过渡 / 落地态布局稳定后再启动:Guide 只在步骤切换时测量目标位置,
// 起步太早会把高亮框定位到过渡中的坐标且不会自行纠正。
const START_DELAY_MS = 800;
function isBrowser(): boolean {
return typeof window !== "undefined";
}
export function isCompactGuideDone(): boolean {
return isBrowser() && localStorage.getItem(COMPACT_GUIDE_DONE_KEY) === "1";
}
function markCompactGuideDone() {
if (isBrowser()) localStorage.setItem(COMPACT_GUIDE_DONE_KEY, "1");
}
/** 跨页触发:非问答页点击「界面使用引导」时先记录请求,导航到 /chats/new 后消费。 */
export function requestCompactGuideStart() {
if (isBrowser()) sessionStorage.setItem(COMPACT_GUIDE_PENDING_KEY, "1");
}
function hasCompactGuideRequest(): boolean {
return (
isBrowser() &&
sessionStorage.getItem(COMPACT_GUIDE_PENDING_KEY) === "1"
);
}
function consumeCompactGuideRequest() {
if (isBrowser()) sessionStorage.removeItem(COMPACT_GUIDE_PENDING_KEY);
}
interface StepSpec {
/** 高亮目标选择器;缺省(欢迎步骤 dialog 模式)以 body 兜底。 */
selector?: string;
mode: "popup" | "dialog";
placement: NonNullable<GuideStep["placement"]>;
title: string;
body: string;
}
const STEP_SPECS: StepSpec[] = [
{
mode: "dialog",
placement: "center",
title: "欢迎使用简洁模式",
body: "当前已隐藏顶部导航栏与全局侧边栏,为您留出更大的工作区。接下来花一分钟认识一下界面布局。",
},
{
selector: TARGET.nav,
mode: "popup",
placement: "right",
title: "功能导航",
body: "新对话、智能体、技能、AI 写作、深度研究等主要入口都在左侧这里,点击即可切换(菜单项由管理员统一配置)。",
},
{
selector: TARGET.recentChats,
mode: "popup",
placement: "right",
title: "聊天记录",
body: "最近的对话会保留在这里,点击任意一条即可继续上次的问答。",
},
{
selector: TARGET.settingsMenu,
mode: "popup",
placement: "right-top",
title: "设置和更多",
body: "通知、设置、用户排行榜,以及菜单管理、技能管理等管理入口都收纳在这个菜单里。",
},
{
selector: TARGET.themeSwitcher,
mode: "popup",
placement: "bottom-left",
title: "主题换肤",
body: "在这里一键切换红色、蓝色、深蓝等多套界面主题。",
},
{
selector: TARGET.layoutMode,
mode: "popup",
placement: "bottom-left",
title: "切回全量模式",
body: "需要顶部导航栏和全局侧边栏时,从这里随时切换回「全量模式」。",
},
{
selector: TARGET.promptInput,
mode: "popup",
placement: "top",
title: "开始提问",
body: "在输入框中输入问题,可先选择智能体与知识空间,发送后即可开始问答。",
},
];
function buildSteps(): GuideStep[] {
if (!isBrowser()) return [];
return STEP_SPECS.filter(
(spec) => !spec.selector || document.querySelector(spec.selector),
).map((spec) => ({
element: spec.selector ?? (() => document.body),
mode: spec.mode,
placement: spec.placement,
title: spec.title,
body: spec.body,
}));
}
/**
* 简洁模式引导。挂在 ChatPage:`eligible` 表示当前处于简洁模式(无全局
* chrome)且非嵌入/强制收起等特殊环境;`autoStart` 表示处于新对话落地态,
* 满足「首次进入」条件时自动演示一遍。
*/
export function CompactModeGuide({
eligible,
autoStart = false,
}: {
eligible: boolean;
autoStart?: boolean;
}) {
const [current, setCurrent] = useState(-1);
const stepsRef = useRef<GuideStep[]>([]);
const start = useCallback(() => {
const steps = buildSteps();
if (steps.length === 0) return;
stepsRef.current = steps;
setCurrent(0);
}, []);
// 自动引导:没显式结束过(没点过完成/跳过)时,每次落地页都演示。是否「已看过」
// 只由下方 finish 回调(完成/跳过)写入 —— 这里启动时绝不能提前写,否则中途刷新
// 又会被当成已看过。
const autoTriedRef = useRef(false);
useEffect(() => {
if (!eligible || !autoStart || autoTriedRef.current) return;
autoTriedRef.current = true;
if (isCompactGuideDone()) return;
const timer = setTimeout(start, START_DELAY_MS);
return () => {
clearTimeout(timer);
// 组件在计时窗口内卸载(用户迅速离开页面)则下次落地再演示。
autoTriedRef.current = false;
};
}, [eligible, autoStart, start]);
// 跨页手动触发:非问答页点「界面使用引导」→ 记录请求 → 导航到 /chats/new → 此处启动。
// 请求只在定时器真正启动引导时才消费 —— StrictMode 下 effect 会 mount→cleanup→mount
// 双跑,若在检查时就删除,第二次跑就拿不到请求了。
useEffect(() => {
if (!eligible) return;
if (!hasCompactGuideRequest()) return;
const timer = setTimeout(() => {
consumeCompactGuideRequest();
start();
}, START_DELAY_MS);
return () => clearTimeout(timer);
}, [eligible, start]);
// 同页手动触发:已在问答页时「界面使用引导」直接广播事件启动。
useEffect(() => {
if (!eligible) return;
const handler = () => start();
window.addEventListener(COMPACT_GUIDE_START_EVENT, handler);
return () => window.removeEventListener(COMPACT_GUIDE_START_EVENT, handler);
}, [eligible, start]);
// 完成 / 跳过是用户显式结束引导 —— 只有这两种情况才记忆「已看过」;中途刷新或
// 离开不记忆,下次进入简洁模式问答页还会再演示。
const finish = useCallback(() => {
markCompactGuideDone();
setCurrent(-1);
}, []);
if (!eligible) return null;
return (
<Guide
current={current}
steps={stepsRef.current}
onChange={setCurrent}
onFinish={finish}
onSkip={finish}
/>
);
}
@@ -206,7 +206,10 @@ export function RecentChatList({
limit != null ? visibleThreads.slice(0, limit) : visibleThreads;
return (
<>
<SidebarGroup className="px-2 pt-3 mt-2 border-t border-sidebar-border/60">
<SidebarGroup
className="px-2 pt-3 mt-2 border-t border-sidebar-border/60"
data-compact-guide="recent-chats"
>
<SidebarGroupLabel
className={cn(
"text-muted-foreground/70 px-3 mb-1 text-[10px] font-semibold tracking-[0.12em] uppercase select-none flex items-center justify-between transition-colors",
@@ -174,7 +174,10 @@ export function WorkspaceNavChatList({
};
return (
<>
<SidebarGroup className={cn("px-2", showCollapseTrigger ? "pt-3" : "pt-2")}>
<SidebarGroup
className={cn("px-2", showCollapseTrigger ? "pt-3" : "pt-2")}
data-compact-guide="sidebar-nav"
>
<SidebarMenu className="gap-1">
{(showCollapseTrigger || newChatItem) && (
<SidebarMenuItem>
@@ -7,6 +7,7 @@ import {
BriefcaseIcon,
CalendarClockIcon,
ChevronsUpDown,
Compass,
CpuIcon,
DatabaseZapIcon,
GaugeIcon,
@@ -22,6 +23,7 @@ import {
TrophyIcon,
} from "lucide-react";
import { useEffect, useState } from "react";
import { usePathname } from "next/navigation";
import {
DropdownMenu,
@@ -46,6 +48,10 @@ import {
} from "@/core/system-settings/api";
import { NotificationDialog } from "./notification-dialog";
import {
COMPACT_GUIDE_START_EVENT,
requestCompactGuideStart,
} from "./compact-mode-guide";
import { useNavigate } from "react-router-dom";
import { SettingsDialog } from "./settings";
@@ -73,6 +79,7 @@ function NavMenuButtonContent({
export function WorkspaceNavMenu() {
const navigate = useNavigate();
const pathname = usePathname();
const isAdmin = getStoredAuth()?.system_role === "admin";
const [settingsOpen, setSettingsOpen] = useState(false);
const [settingsDefaultSection, setSettingsDefaultSection] = useState<
@@ -90,6 +97,20 @@ export function WorkspaceNavMenu() {
setMounted(true);
}, []);
// 简洁模式界面引导:已在问答页(ChatPage 挂载中)时直接广播事件启动;否则先落
// sessionStorage 请求,导航到新对话页后由 CompactModeGuide 消费启动。
const startCompactGuide = () => {
if (/^\/page\/workspace\/chats\/.+/.test(pathname)) {
window.dispatchEvent(new CustomEvent(COMPACT_GUIDE_START_EVENT));
return;
}
requestCompactGuideStart();
navigate("/page/workspace/chats/new", {
replace: true,
state: { resetAt: Date.now() },
});
};
useEffect(() => {
if (!isAdmin) return;
let cancelled = false;
@@ -116,7 +137,7 @@ export function WorkspaceNavMenu() {
open={notificationOpen}
onOpenChange={setNotificationOpen}
/>
<SidebarMenu className="w-full">
<SidebarMenu className="w-full" data-compact-guide="settings-menu">
<SidebarMenuItem>
{mounted ? (
<DropdownMenu>
@@ -155,6 +176,10 @@ export function WorkspaceNavMenu() {
</span>
)}
</DropdownMenuItem>
<DropdownMenuItem onClick={startCompactGuide}>
<Compass />
界面使用引导
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setSettingsDefaultSection("appearance");