用户引导

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");
+62 -19
View File
@@ -1,6 +1,6 @@
"use client";
import { BotIcon, PlusSquare } from "lucide-react";
import { BotIcon, Compass, PlusSquare } from "lucide-react";
import { useParams, useRouter } from "next/navigation";
import { useLocation } from "react-router-dom";
import { useRouteBase } from "@/core/route-base";
@@ -11,6 +11,11 @@ import { Button } from "@/components/ui/button";
import { AgentChatSidebar } from "@/components/workspace/agents/agent-chat-sidebar";
import { AgentDetailPanel } from "@/components/workspace/agents/agent-detail-panel";
import { AgentQuestionChips } from "@/components/workspace/agents/agent-question-chips";
import {
AgentGuide,
isAgentGuideEnvironment,
startAgentGuide,
} from "@/components/workspace/agents/agent-guide";
import { AgentWelcome } from "@/components/workspace/agent-welcome";
import { ArtifactTrigger } from "@/components/workspace/artifacts";
import { ChatBox, useThreadChat } from "@/components/workspace/chats";
@@ -559,6 +564,10 @@ export default function AgentChatPage({
// 3qfx/xdfx(taskBind)不展示闸门,新建对话进问答页后由右上角「开始回答」按钮手动发出开场信息。
const showTaskGate = !!taskGate && !!taskId && isNewThread && !hasStartedConversation;
const isDemoMode = env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true";
// 智能体使用引导(chat 阶段):仅普通智能体对话页 —— 排除通用问答原地嵌入
// (embeddedAgentId)、任务深链工作区(taskGate/taskBind)等复用本页的场景。
const agentGuideEligible =
!!showAgentList && !isEmbedded && !taskCtxOn && isAgentGuideEnvironment();
const status = thread.error
? "error"
@@ -575,7 +584,14 @@ export default function AgentChatPage({
panel stays rightmost. */}
<div className="flex size-full min-h-0 overflow-hidden workspace-chat-bg">
{/* 最左:智能体列表栏(仅独立整页问答开启)。顶部下拉切换 我的/广场。 */}
{showAgentList && <AgentChatSidebar activeAgentId={agent_id} />}
{showAgentList && (
<div
data-agent-guide="chat-sidebar"
className="flex h-full shrink-0"
>
<AgentChatSidebar activeAgentId={agent_id} />
</div>
)}
<div className="min-h-0 min-w-0 flex-1">
<ChatBox threadId={threadId}>
{/* Main conversation column. `relative` hosts the floating
@@ -598,6 +614,20 @@ export default function AgentChatPage({
</div>
<div className="flex items-center">
{/* 智能体使用引导(chat 阶段)手动重放入口。 */}
{agentGuideEligible && (
<Tooltip content="重新播放智能体使用引导">
<Button
size="sm"
variant="ghost"
className="text-muted-foreground hover:text-foreground mr-1"
onClick={() => startAgentGuide("chat")}
>
<Compass className="size-4" />
使用引导
</Button>
</Tooltip>
)}
{/* 「模板数据转换器」专属:选页面模板 + 渲染预览(调试该智能体效果)。 */}
{isTemplateBuilder && (
<div className="mr-2 flex items-center gap-1.5">
@@ -688,13 +718,17 @@ export default function AgentChatPage({
className="mb-7"
/>
<div className="w-full max-w-(--container-width-md)">
<AgentQuestionChips
className="mb-3"
questions={commonQuestions}
disabled={isDemoMode}
onAsk={(text) => handleSubmit({ text, files: [] })}
/>
<div className="min-w-0 flex-1">
<div data-agent-guide="chat-questions" className="mb-3">
<AgentQuestionChips
questions={commonQuestions}
disabled={isDemoMode}
onAsk={(text) => handleSubmit({ text, files: [] })}
/>
</div>
<div
className="min-w-0 flex-1"
data-agent-guide="chat-input"
>
<InputBox
className="w-full"
isNewThread
@@ -756,7 +790,10 @@ export default function AgentChatPage({
/>
<div className="from-background pointer-events-none shrink-0 bg-gradient-to-t to-transparent px-4 pb-4 pt-2">
<div className="pointer-events-auto mx-auto w-full max-w-(--container-width-lg)">
<div
className="pointer-events-auto mx-auto w-full max-w-(--container-width-lg)"
data-agent-guide="chat-input"
>
<TodoList
className="bg-background/5 mb-2"
todos={thread.values.todos ?? []}
@@ -805,16 +842,22 @@ export default function AgentChatPage({
</div>
{/* Right: agent detail panel — always the rightmost column. */}
<AgentDetailPanel
agent={agent}
agentName={agentDisplayName}
disabled={thread.isLoading || isDemoMode}
onAskQuestion={(text) => handleSubmit({ text, files: [] })}
defaultCollapsed={taskCtxOn}
showRecentChats={!taskCtxOn}
compactAppearanceActions={compactAppearanceActions}
/>
<div
data-agent-guide="chat-detail"
className="flex h-full shrink-0"
>
<AgentDetailPanel
agent={agent}
agentName={agentDisplayName}
disabled={thread.isLoading || isDemoMode}
onAskQuestion={(text) => handleSubmit({ text, files: [] })}
defaultCollapsed={taskCtxOn}
showRecentChats={!taskCtxOn}
compactAppearanceActions={compactAppearanceActions}
/>
</div>
</div>
<AgentGuide stage="chat" eligible={agentGuideEligible} />
{/* 「模板数据转换器」调试预览弹窗(仅此 agent)。 */}
{isTemplateBuilder && (
+22 -2
View File
@@ -34,11 +34,13 @@ import {
} from "@/components/workspace/appearance-controls";
import { TokenUsageIndicator } from "@/components/workspace/token-usage-indicator";
import { UserIdentityBadge } from "@/components/workspace/user-identity-badge";
import { CompactModeGuide } from "@/components/workspace/compact-mode-guide";
import { Welcome } from "@/components/workspace/welcome";
import { CompactWelcome } from "@/components/workspace/compact-welcome";
import { readSidebarEnabled } from "@/core/page-layout/sidebar-enabled";
import { isChatsIframeWorkspacePath } from "@/core/page-layout/chats-iframe-route";
import { isHideChromeActive } from "@/core/embed/embed-session";
import { isEmbedActive, isHideChromeActive } from "@/core/embed/embed-session";
import { isWujieEnv } from "@/core/embed/wujie";
import { useSidebar } from "@/components/ui/sidebar";
import { Button } from "@/components/ui/button";
import { PanelLeftOpen } from "lucide-react";
@@ -665,6 +667,17 @@ export default function ChatPage() {
chromeByPreference && !isChatsIframeWorkspacePath(pathname);
const compactLanding = isLandingState && compactChrome;
const compactActionsHidden = isHideChromeActive();
// 简洁模式新手引导:仅面向「无全局导航栏/侧边栏」的正常简洁布局。iframe 嵌入
// (embed=1)、去 chrome(hideChrome=1)、强制收起侧栏(无界部署)等环境同样没有
// 全局 chrome,但不该在外部系统里弹引导。
const compactGuideEligible =
!compactChrome &&
!fullChatMode &&
!isChatsIframeWorkspacePath(pathname) &&
!compactActionsHidden &&
!env.FORCE_COLLAPSED_SIDEBAR &&
!isEmbedActive() &&
!isWujieEnv();
return (
<ThreadContext.Provider value={{ thread: displayThread, isMock }}>
@@ -801,7 +814,10 @@ export default function ChatPage() {
/>
))}
{mounted ? (
<div className="w-full -translate-y-4">
<div
className="w-full -translate-y-4"
data-compact-guide="prompt-input"
>
<InputBox
className="w-full"
isNewThread={isLandingState}
@@ -886,6 +902,10 @@ export default function ChatPage() {
className="top-16 right-0 bottom-48 left-0"
/>
)}
<CompactModeGuide
eligible={compactGuideEligible}
autoStart={mounted && isLandingState}
/>
</div>
</ChatBox>
</ThreadContext.Provider>
+70 -41
View File
@@ -3,6 +3,7 @@
import {
ArrowLeftIcon,
CheckCircleIcon,
Compass,
SaveIcon,
} from "lucide-react";
import { useParams, useRouter } from "next/navigation";
@@ -39,6 +40,11 @@ import {
import { ThreadContext } from "@/components/workspace/messages/context";
import { SkillTransferSelector } from "@/components/workspace/agents/skill-transfer-selector";
import { SkillDetailDialog } from "@/components/workspace/skills/skill-detail-dialog";
import {
AgentGuide,
isAgentGuideEnvironment,
startAgentGuide,
} from "@/components/workspace/agents/agent-guide";
import { getAPIClient } from "@/core/api";
import type { Agent } from "@/core/agents";
import {
@@ -407,16 +413,28 @@ export default function NewAgentPage() {
</Button>
<h1 className="text-sm font-semibold">创建智能体</h1>
</div>
{agent ? (
<div className="flex items-center gap-2">
<Button
variant="outline"
variant="ghost"
size="sm"
onClick={() => router.push(`/page/workspace/agents/${agent.id}/chats/new`)}
className="text-muted-foreground hover:text-foreground"
title="重新播放创建引导"
onClick={() => startAgentGuide("create")}
>
<CheckCircleIcon className="h-4 w-4" />
开始对话
<Compass className="mr-1.5 h-4 w-4" />
使用引导
</Button>
) : null}
{agent ? (
<Button
variant="outline"
size="sm"
onClick={() => router.push(`/page/workspace/agents/${agent.id}/chats/new`)}
>
<CheckCircleIcon className="h-4 w-4" />
开始对话
</Button>
) : null}
</div>
</header>
<ResizablePanelGroup
@@ -442,7 +460,10 @@ export default function NewAgentPage() {
onSetupAgentReject={handleRejectSetupAgent}
/>
</div>
<div className="bg-background shrink-0 space-y-2 border-t px-4 py-4">
<div
className="bg-background shrink-0 space-y-2 border-t px-4 py-4"
data-agent-guide="create-chat-input"
>
<div className="flex items-center gap-2">
<span className="text-muted-foreground text-xs">
当前对话模型
@@ -525,32 +546,35 @@ export default function NewAgentPage() {
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">名称</label>
<Input
placeholder="例如:合同审查智能体"
value={nameInput}
onChange={(event) => {
setNameInput(event.currentTarget.value);
setFormError("");
}}
className={cn(formError && "border-destructive")}
/>
<div className="space-y-5" data-agent-guide="create-basic">
<div className="space-y-2">
<label className="text-sm font-medium">名称</label>
<Input
placeholder="例如:合同审查智能体"
value={nameInput}
onChange={(event) => {
setNameInput(event.currentTarget.value);
setFormError("");
}}
className={cn(formError && "border-destructive")}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">描述</label>
<Textarea
placeholder="简要说明这个智能体擅长什么"
value={descriptionInput}
onChange={(event) =>
setDescriptionInput(event.currentTarget.value)
}
className="min-h-24 resize-none"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">描述</label>
<Textarea
placeholder="简要说明这个智能体擅长什么"
value={descriptionInput}
onChange={(event) =>
setDescriptionInput(event.currentTarget.value)
}
className="min-h-24 resize-none"
/>
</div>
<div className="space-y-2">
<div className="space-y-5" data-agent-guide="create-model">
<div className="space-y-2">
<label className="text-sm font-medium">模型</label>
{isModelsLoading || models.length === 0 ? (
<div className="border-input bg-background text-muted-foreground flex h-9 w-full items-center rounded-md border px-3 text-sm shadow-xs dark:border-[#1B477E] dark:bg-[#022248]">
@@ -590,9 +614,9 @@ export default function NewAgentPage() {
<p className="text-muted-foreground text-xs">
该智能体被使用时调用的模型。
</p>
</div>
</div>
<div className="space-y-2">
<div className="space-y-2">
<label className="text-sm font-medium">入库模板名</label>
<Input
placeholder="例如:临时策略库(留空表示不支持入库)"
@@ -605,16 +629,19 @@ export default function NewAgentPage() {
配置后,该智能体对话中生成的 Markdown
文件右上角会出现"发布入库"按钮,可一键发送到知识入库接口。
</p>
</div>
</div>
<LlmWikiKnowledgeSelector
source="all"
value={selectedKnowledgeBaseIds}
onChange={setSelectedKnowledgeBaseIds}
variant="panel"
/>
<div data-agent-guide="create-knowledge">
<LlmWikiKnowledgeSelector
source="all"
value={selectedKnowledgeBaseIds}
onChange={setSelectedKnowledgeBaseIds}
variant="panel"
/>
</div>
<div className="space-y-2">
<div className="space-y-2" data-agent-guide="create-soul">
<label className="text-sm font-medium">SOUL.md</label>
<Textarea
placeholder="写下智能体的人设、边界、工作方式和输出要求"
@@ -624,7 +651,7 @@ export default function NewAgentPage() {
/>
</div>
<div className="space-y-2">
<div className="space-y-2" data-agent-guide="create-skills">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">技能</label>
<span className="text-muted-foreground text-xs">
@@ -675,6 +702,7 @@ export default function NewAgentPage() {
className="w-full"
onClick={() => void handleSaveAgent()}
disabled={busy || isSkillsLoading}
data-agent-guide="create-save-btn"
>
<SaveIcon className="h-4 w-4" />
{agentId ? "保存智能体" : "创建智能体"}
@@ -683,6 +711,7 @@ export default function NewAgentPage() {
</aside>
</ResizablePanel>
</ResizablePanelGroup>
<AgentGuide stage="create" eligible={isAgentGuideEnvironment()} />
</div>
</ArtifactsProvider>
</ThreadContext.Provider>