feat: 优化对话历史与会商引导体验
列表引导改为手动触发且不跳转创建页;历史 Tab 展示会商步骤/链条与深度研究模板;Step2 增加暂停与内容高度跟随滚动,并修复旧草稿恢复兜底。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
832e180c52
commit
4ac7bc21cd
18 files changed
+184
-88
No files matched your search
@@ -84,7 +84,7 @@ The frontend (`frontend-web/`) is a React 19 + Vite SPA:
|
|||||||
- **菜单管理 (Menu Management, admin-only)**: an **overlay** over the static sidebar menu — `MenuManagementPage` (`pages/MenuManagementPage.tsx`, route `/page/strategy/admin/menu-management`, opened from the admin-only 「菜单管理」 item in the **bottom-left settings dropdown** of `components/page-sidebar.tsx`). Admins rename / 停用·启用 / reorder (↑↓ or @dnd-kit drag) / re-parent (horizontal drag **or** the edit dialog's 「移动到」parent selector) any node across 一级/二级/三级, **including injected iframe light-app nodes** (`light-app-<appId>`). The static tree (`core/page-layout/sidebar-menu.ts`) stays the source of icons/routes; only per-node deltas (`label`/`disabled`/`parentId`/`sortOrder`) persist to the backend. Merge logic in `core/page-layout/menu-overrides.ts` (`applyMenuOverrides`, applied in `use-sidebar-menu.ts` after light-app injection, before role filter; disabling a node hides its whole subtree) — strict **3-level cap** (`wouldExceedDepth`). Client `strategy-components/api/menu-overrides.ts`. Backend: `/api/menu-overrides` (global/shared; open read, admin atomic replace-all). **简介模式** uses a separate overlay: every compact workspace menu item can be placed in `sidebar`(左侧菜单)、`settings`(设置和更多)or `toolbox`(右侧工具箱); `CompactToolbox` (`components/workspace/compact-toolbox.tsx`, mounted by `PageRoutes.tsx`) opens only when the square right-edge trigger is clicked, then slides a compact, content-height right-side drawer over a backdrop above global dropdowns; configured items render top-to-bottom as centered square cards with a large icon above the label while the effective layout is compact. `/api/compact-menu-overrides` stores two independent sets (type `A` / `B`); each website picks one via `runtime-config.js` `VITE_COMPACT_MENU_TYPE`.
|
- **菜单管理 (Menu Management, admin-only)**: an **overlay** over the static sidebar menu — `MenuManagementPage` (`pages/MenuManagementPage.tsx`, route `/page/strategy/admin/menu-management`, opened from the admin-only 「菜单管理」 item in the **bottom-left settings dropdown** of `components/page-sidebar.tsx`). Admins rename / 停用·启用 / reorder (↑↓ or @dnd-kit drag) / re-parent (horizontal drag **or** the edit dialog's 「移动到」parent selector) any node across 一级/二级/三级, **including injected iframe light-app nodes** (`light-app-<appId>`). The static tree (`core/page-layout/sidebar-menu.ts`) stays the source of icons/routes; only per-node deltas (`label`/`disabled`/`parentId`/`sortOrder`) persist to the backend. Merge logic in `core/page-layout/menu-overrides.ts` (`applyMenuOverrides`, applied in `use-sidebar-menu.ts` after light-app injection, before role filter; disabling a node hides its whole subtree) — strict **3-level cap** (`wouldExceedDepth`). Client `strategy-components/api/menu-overrides.ts`. Backend: `/api/menu-overrides` (global/shared; open read, admin atomic replace-all). **简介模式** uses a separate overlay: every compact workspace menu item can be placed in `sidebar`(左侧菜单)、`settings`(设置和更多)or `toolbox`(右侧工具箱); `CompactToolbox` (`components/workspace/compact-toolbox.tsx`, mounted by `PageRoutes.tsx`) opens only when the square right-edge trigger is clicked, then slides a compact, content-height right-side drawer over a backdrop above global dropdowns; configured items render top-to-bottom as centered square cards with a large icon above the label while the effective layout is compact. `/api/compact-menu-overrides` stores two independent sets (type `A` / `B`); each website picks one via `runtime-config.js` `VITE_COMPACT_MENU_TYPE`.
|
||||||
- **舆情分析智能体(前端虚拟智能体)**: 智能体管理页新增「舆情分析」类型 tab,固定一张虚拟智能体卡片(名称/简介可编辑,存 localStorage)。对话页 `pages/SentimentAgentChatPage.tsx`(路由 `agents/sentiment-analysis/chats/:thread_id`)复用普通问答布局 + `MessageList`,**输入框只有发送按钮**;问答经 Gateway `POST /api/sentiment-agent/stream` 转发外部 AG-UI 网关(不经过 LangGraph),会话仅存 sessionStorage。接口地址 + Basic Auth 在 `runtime-config.js` 配置(`VITE_SENTIMENT_AGENT_API_URL` / `_AUTH_USERNAME` / `_AUTH_PASSWORD`)并随请求传给后台;后台按传入地址转发、关闭 TLS 校验,失败时把完整错误回传。核心模块 `src/core/sentiment-agent/`。
|
- **舆情分析智能体(前端虚拟智能体)**: 智能体管理页新增「舆情分析」类型 tab,固定一张虚拟智能体卡片(名称/简介可编辑,存 localStorage)。对话页 `pages/SentimentAgentChatPage.tsx`(路由 `agents/sentiment-analysis/chats/:thread_id`)复用普通问答布局 + `MessageList`,**输入框只有发送按钮**;问答经 Gateway `POST /api/sentiment-agent/stream` 转发外部 AG-UI 网关(不经过 LangGraph),会话仅存 sessionStorage。接口地址 + Basic Auth 在 `runtime-config.js` 配置(`VITE_SENTIMENT_AGENT_API_URL` / `_AUTH_USERNAME` / `_AUTH_PASSWORD`)并随请求传给后台;后台按传入地址转发、关闭 TLS 校验,失败时把完整错误回传。核心模块 `src/core/sentiment-agent/`。
|
||||||
- **简洁模式用户引导 (Compact-mode onboarding tour)**: 简洁模式(外观设置的「全量/简洁」= compact,无全局导航栏 + 页面侧边栏)新增 TDesign React `Guide` 组件驱动的分步引导。组件 `components/workspace/compact-mode-guide.tsx`(挂在 `ChatPage`):9 步 —— 欢迎弹窗(dialog 模式)→ 左侧功能导航 → 聊天记录 → 设置和更多 → 换肤 → 界面模式(切回全量)→ 常用提示词栏 → 提示词管理弹窗 → 输入框;高亮目标由各组件上的 `data-compact-guide` 标记定位(`workspace-nav-chat-list` / `recent-chat-list` / `workspace-nav-menu` / `appearance-controls` / `custom-prompt-menu.tsx` 的 `CustomPromptBar`(提示词栏,仅新对话落地态渲染)与其 `ManagePromptsDialog`(`prompt-dialog` 锚点)/ ChatPage 输入框容器),**启动时按目标是否存在过滤步骤**(如新用户无聊天记录、侧栏收起时自动剔除该步 —— Guide 对缺失元素会抛错,不能盲传)。**提示词管理弹窗步骤是动态目标**:弹窗平时不渲染,引导切到该步前先广播 `PROMPT_DIALOG_GUIDE_EVENT`(`custom-prompt-menu.tsx` 导出)让 `CustomPromptBar` 打开 `ManagePromptsDialog` 并**接管**(`owner: true` 期间 ESC/点遮罩不关闭,防止高亮目标消失导致 Guide 报错),轮询锚点真正挂载后才切步;离开该步 / 跳过 / 完成 / 中途卸载时以 `owner: false` 收回弹窗,4 秒打不开则跳过该步防卡死。**TDesign Guide × Radix 模态互操作**:Radix 模态弹窗打开时会把 `body` 置 `pointer-events:none`(滚动锁定),Guide 直接挂在 body 下的遮罩/高亮/弹层会一并继承失活(引导按钮点不动),`styles/tdesign-theme.css` 末尾对 `.t-guide__overlay/__highlight/__popup/__wrapper` 显式 `pointer-events:auto` 兜底。触发:① 自动 —— 简洁模式新对话落地页自动演示,直到用户显式点「完成」或「跳过」才写 localStorage `deerflow.compact-guide-finished` 记忆(浏览器级);**中途刷新/离开不记忆,下次进入还会再演示**。② 手动 —— 「设置和更多」下拉新增「界面使用引导」(该下拉仅存在于简洁模式工作区侧栏 footer):已在问答页时广播 `compact-guide:start` 窗口事件直接启动,否则写 sessionStorage 请求并导航 `/chats/new` 由挂载的引导消费(消费在定时器真正启动时才执行,规避 StrictMode effect 双跑吞请求)。`eligible` 排除 embed=1 / hideChrome=1 / chats_iframe / FORCE_COLLAPSED_SIDEBAR(无界)等同样无全局 chrome 的特殊环境;启动延迟 800ms 等侧栏宽度过渡稳定后再测量高亮框。
|
- **简洁模式用户引导 (Compact-mode onboarding tour)**: 简洁模式(外观设置的「全量/简洁」= compact,无全局导航栏 + 页面侧边栏)新增 TDesign React `Guide` 组件驱动的分步引导。组件 `components/workspace/compact-mode-guide.tsx`(挂在 `ChatPage`):9 步 —— 欢迎弹窗(dialog 模式)→ 左侧功能导航 → 聊天记录 → 设置和更多 → 换肤 → 界面模式(切回全量)→ 常用提示词栏 → 提示词管理弹窗 → 输入框;高亮目标由各组件上的 `data-compact-guide` 标记定位(`workspace-nav-chat-list` / `recent-chat-list` / `workspace-nav-menu` / `appearance-controls` / `custom-prompt-menu.tsx` 的 `CustomPromptBar`(提示词栏,仅新对话落地态渲染)与其 `ManagePromptsDialog`(`prompt-dialog` 锚点)/ ChatPage 输入框容器),**启动时按目标是否存在过滤步骤**(如新用户无聊天记录、侧栏收起时自动剔除该步 —— Guide 对缺失元素会抛错,不能盲传)。**提示词管理弹窗步骤是动态目标**:弹窗平时不渲染,引导切到该步前先广播 `PROMPT_DIALOG_GUIDE_EVENT`(`custom-prompt-menu.tsx` 导出)让 `CustomPromptBar` 打开 `ManagePromptsDialog` 并**接管**(`owner: true` 期间 ESC/点遮罩不关闭,防止高亮目标消失导致 Guide 报错),轮询锚点真正挂载后才切步;离开该步 / 跳过 / 完成 / 中途卸载时以 `owner: false` 收回弹窗,4 秒打不开则跳过该步防卡死。**TDesign Guide × Radix 模态互操作**:Radix 模态弹窗打开时会把 `body` 置 `pointer-events:none`(滚动锁定),Guide 直接挂在 body 下的遮罩/高亮/弹层会一并继承失活(引导按钮点不动),`styles/tdesign-theme.css` 末尾对 `.t-guide__overlay/__highlight/__popup/__wrapper` 显式 `pointer-events:auto` 兜底。触发:① 自动 —— 简洁模式新对话落地页自动演示,直到用户显式点「完成」或「跳过」才写 localStorage `deerflow.compact-guide-finished` 记忆(浏览器级);**中途刷新/离开不记忆,下次进入还会再演示**。② 手动 —— 「设置和更多」下拉新增「界面使用引导」(该下拉仅存在于简洁模式工作区侧栏 footer):已在问答页时广播 `compact-guide:start` 窗口事件直接启动,否则写 sessionStorage 请求并导航 `/chats/new` 由挂载的引导消费(消费在定时器真正启动时才执行,规避 StrictMode effect 双跑吞请求)。`eligible` 排除 embed=1 / hideChrome=1 / chats_iframe / FORCE_COLLAPSED_SIDEBAR(无界)等同样无全局 chrome 的特殊环境;启动延迟 800ms 等侧栏宽度过渡稳定后再测量高亮框。
|
||||||
- **智能体使用引导 (Agent onboarding tour, 三段式)**: 智能体「浏览管理 → 创建与配置 → 发起问答」全流程分步引导,组件 `components/workspace/agents/agent-guide.tsx`(同一 TDesign `Guide` 封装模式,与简洁模式引导共享记忆语义:**仅显式点「完成/跳过」才写 localStorage** `deerflow.agent-guide.<stage>.finished`,中途刷新/离开不记忆)。三段各自挂载、各自记忆:① `list`(`agent-gallery.tsx`,/page/workspace/agents)——分区 Tabs、搜索排序、智能体卡片操作、右上「创建智能体」按钮;最后一步按钮文案改为「去创建智能体 →」,点完成即 `router.push` 到创建页衔接下一段;② `create`(`NewAgentPage.tsx`,/page/workspace/agents/new)——左侧对话式创建输入区、基本信息、运行模型+入库模板名、知识空间、SOUL.md、技能穿梭框、保存按钮(9 步);③ `chat`(`AgentChatPage.tsx`,agents/:id/chats)——左栏切换与历史、常用问题 chips、输入框(落地态/会话态两个挂点同一 `chat-input` 标记)、右侧详情面板;`eligible` 限定普通智能体对话页(`showAgentList && !isEmbedded && !taskCtxOn`,即排除通用问答原地嵌入与任务深链工作区),三段共用 `isAgentGuideEnvironment()` 排除 embed/hideChrome/无界环境。高亮锚点 `data-agent-guide`:列表页 `tabs`/`search`/`agent-card`(卡片+列表两种布局的根元素)/`new-agent-btn`,创建页 `create-chat-input`/`create-basic`/`create-model`/`create-knowledge`/`create-soul`/`create-skills`/`create-save-btn`,对话页 `chat-sidebar`/`chat-questions`/`chat-input`/`chat-detail`;同样**启动时按目标存在性过滤步骤**(新用户无卡片/无预设问题时自动剔除)。手动重放:三个页面头部各有「使用引导」按钮(列表页创建按钮左侧、创建页 header 右侧、对话页 header),广播 `agent-guide:start:<stage>` 窗口事件强制重放(忽略已看过记忆)。
|
- **智能体使用引导 (Agent onboarding tour, 三段式)**: 智能体「浏览管理 → 创建与配置 → 发起问答」全流程分步引导,组件 `components/workspace/agents/agent-guide.tsx`(同一 TDesign `Guide` 封装模式,与简洁模式引导共享记忆语义:**仅显式点「完成/跳过」才写 localStorage** `deerflow.agent-guide.<stage>.finished`,中途刷新/离开不记忆)。**自动演示仅 create / chat 两段**(进智能体列表页不弹引导,点「创建智能体」进入新建页时才自动演示;list 段的 `AgentGuide` 传 `autoStart={false}`,只能由头部「使用引导」按钮手动触发)。三段各自挂载、各自记忆:① `list`(`agent-gallery.tsx`,/page/workspace/agents)——分区 Tabs、搜索排序、智能体卡片操作、右上「创建智能体」按钮;**完成/跳过就地结束(不跳转创建页,与 create 段无联动)**,最后一步仅提示进入创建页后会有配置引导;② `create`(`NewAgentPage.tsx`,/page/workspace/agents/new)——左侧对话式创建输入区、基本信息、运行模型+入库模板名、知识空间、SOUL.md、技能穿梭框、保存按钮(9 步);③ `chat`(`AgentChatPage.tsx`,agents/:id/chats)——左栏切换与历史、常用问题 chips、输入框(落地态/会话态两个挂点同一 `chat-input` 标记)、右侧详情面板;`eligible` 限定普通智能体对话页(`showAgentList && !isEmbedded && !taskCtxOn`,即排除通用问答原地嵌入与任务深链工作区),三段共用 `isAgentGuideEnvironment()` 排除 embed/hideChrome/无界环境。高亮锚点 `data-agent-guide`:列表页 `tabs`/`search`/`agent-card`(卡片+列表两种布局的根元素)/`new-agent-btn`,创建页 `create-chat-input`/`create-basic`/`create-model`/`create-knowledge`/`create-soul`/`create-skills`/`create-save-btn`,对话页 `chat-sidebar`/`chat-questions`/`chat-input`/`chat-detail`;同样**启动时按目标存在性过滤步骤**(新用户无卡片/无预设问题时自动剔除)。手动重放:三个页面头部各有「使用引导」按钮(列表页创建按钮左侧、创建页 header 右侧、对话页 header),广播 `agent-guide:start:<stage>` 窗口事件强制重放(忽略已看过记忆)。
|
||||||
- **敏感词管理 (Sensitive Word Management)**: configures the **展示层脱敏映射** (原始词 → 替换词). **Gated on the username `lqq`** (not the admin role) — `getAccountDisplayName().toLowerCase() === "lqq"` (username = email `@` prefix, else `user_id`, same as the profile page); the dropdown entry, the route guard, and the page-level guard all use this check. `src/lib/desensitize.ts` ships a frozen seed map `SEED_DESENSITIZE_MAP` (the deployment seed) plus a live mutable `DESENSITIZE_MAP` (initialized from the seed so `desensitize()` works before any fetch). `DesensitizeWordsLoader` (mounted in `App.tsx` `AppProviders`) fetches `/api/sensitive-words` on startup and, if non-empty, calls `replaceDesensitizeMap(...)` to make the backend config the source of truth (empty/failed → keep seed). Page `SensitiveWordsPage.tsx` (route `/page/strategy/admin/sensitive-words`, opened from the 「敏感词管理」 item in the **bottom-left settings dropdown** of `components/page-sidebar.tsx`, present in **both** full + collapsed sidebar modes) lists/adds/edits/启停/deletes words and saves the **whole set atomically** (replace-all); a 「导入种子词」 button merges any missing `SEED_DESENSITIZE_MAP` entries into the editor. Client + hooks in `strategy-components/api/sensitive-words.ts` (`wordsToMap` folds enabled rows into the runtime map). Backend: `/api/sensitive-words` (global/shared; open read, write allowed for admin **or** the `lqq` account), mirroring 菜单管理's store shape. **侧边栏 + 导航栏 menu labels are desensitized** at the data source: `useSidebarMenuItems`/`useActiveThirdLevel` (`core/page-layout/use-sidebar-menu.ts`) map every node's `label` through a desensitizer built **directly from the `/api/sensitive-words` query** (`makeDesensitizer(wordsToMap(words))`, falling back to the global seed `desensitize` while empty) — this sidesteps the `replaceDesensitizeMap` effect-timing race. Only `label` is rewritten; `path`/`id`/`icon` are untouched so routing/highlight are unaffected. All nav surfaces (`page-sidebar.tsx`, `page-sidebar-dark.tsx`, `page-sidebar-v1.tsx`, `strategy-components/Header.tsx`, `PageRoutes.tsx`) consume this hook and are covered; `MenuManagementPage` uses the raw tree so admins still edit real labels.
|
- **敏感词管理 (Sensitive Word Management)**: configures the **展示层脱敏映射** (原始词 → 替换词). **Gated on the username `lqq`** (not the admin role) — `getAccountDisplayName().toLowerCase() === "lqq"` (username = email `@` prefix, else `user_id`, same as the profile page); the dropdown entry, the route guard, and the page-level guard all use this check. `src/lib/desensitize.ts` ships a frozen seed map `SEED_DESENSITIZE_MAP` (the deployment seed) plus a live mutable `DESENSITIZE_MAP` (initialized from the seed so `desensitize()` works before any fetch). `DesensitizeWordsLoader` (mounted in `App.tsx` `AppProviders`) fetches `/api/sensitive-words` on startup and, if non-empty, calls `replaceDesensitizeMap(...)` to make the backend config the source of truth (empty/failed → keep seed). Page `SensitiveWordsPage.tsx` (route `/page/strategy/admin/sensitive-words`, opened from the 「敏感词管理」 item in the **bottom-left settings dropdown** of `components/page-sidebar.tsx`, present in **both** full + collapsed sidebar modes) lists/adds/edits/启停/deletes words and saves the **whole set atomically** (replace-all); a 「导入种子词」 button merges any missing `SEED_DESENSITIZE_MAP` entries into the editor. Client + hooks in `strategy-components/api/sensitive-words.ts` (`wordsToMap` folds enabled rows into the runtime map). Backend: `/api/sensitive-words` (global/shared; open read, write allowed for admin **or** the `lqq` account), mirroring 菜单管理's store shape. **侧边栏 + 导航栏 menu labels are desensitized** at the data source: `useSidebarMenuItems`/`useActiveThirdLevel` (`core/page-layout/use-sidebar-menu.ts`) map every node's `label` through a desensitizer built **directly from the `/api/sensitive-words` query** (`makeDesensitizer(wordsToMap(words))`, falling back to the global seed `desensitize` while empty) — this sidesteps the `replaceDesensitizeMap` effect-timing race. Only `label` is rewritten; `path`/`id`/`icon` are untouched so routing/highlight are unaffected. All nav surfaces (`page-sidebar.tsx`, `page-sidebar-dark.tsx`, `page-sidebar-v1.tsx`, `strategy-components/Header.tsx`, `PageRoutes.tsx`) consume this hook and are covered; `MenuManagementPage` uses the raw tree so admins still edit real labels.
|
||||||
- **技能地址管理 (Skill Address Management, admin-only)**: 扫描运行中的技能(`skills/{public,custom}` 下所有 `.md`/`.py`)里的 **http(s) URL 与 IP/IP:端口**,按唯一地址聚合全部出现位置(跨 .md/.py),支持**批量替换**(如把 UAT 地址一次性改成生产)。主体抽成可复用组件 `SkillAddressesPanel`(`pages/SkillAddressesPage.tsx` 导出,默认导出 `SkillAddressesPage` 仍保留旧路由 `/page/strategy/admin/skill-addresses` 作直链回退),**作为「技能地址管理」标签页嵌入管理员「技能管理」页 `pages/AdminSkillsPage.tsx`**(该页加 Tabs:「技能列表」+「技能地址管理」;入口在「设置和更多」下拉的「技能管理」`workspace-nav-menu.tsx` → `/page/workspace/admin/skills`,不再单列于 `components/page-sidebar.tsx` 左下角设置下拉):打开自动扫描并带**进度条**(扫描逐技能 / 应用逐文件 X/N,应对 150+ 技能),URL/IP 类型过滤 + 搜索 + 「片段批量套用」(如所有 `uat.4.cn` → `prod.4.cn`)、每地址「替换为」输入框(可展开看 `file:line` 片段)、内联**预览**、**应用**、**多关键词检索**(空格分隔全部命中),以及**修改记录**面板——每次应用生成一条记录(显示时间/改了哪些地址 from→to/文件数/处数),可**针对单次记录回退**(二次确认)。client + NDJSON 流式解析在 `strategy-components/api/skill-addresses.ts`。后端 `/api/skill-addresses`(**仅管理员**):所有文件 IO 走 `asyncio.to_thread` 不阻塞事件循环;`GET /scan`(+`/scan/stream`)、`POST /preview`、`POST /apply`(+`/apply/stream`)、`GET /backups`、`POST /rollback/{id}`;**子串安全替换**(基于 token span,不会误伤子串)、`expected_total` 乐观锁(409)、写前自动磁盘备份到 `{skills_root}/.address-edits/`(同时写一份 `{id}.manifest.json` 记录本次替换明细,供修改记录展示)、应用后刷新技能系统提示缓存。纯引擎 `deerflow/skills/address_scan.py`,router `app/gateway/routers/skill_addresses.py`,tests `tests/test_skill_addresses.py`。范围仅运行中技能,不动 `deploy/minimal/skills/`。
|
- **技能地址管理 (Skill Address Management, admin-only)**: 扫描运行中的技能(`skills/{public,custom}` 下所有 `.md`/`.py`)里的 **http(s) URL 与 IP/IP:端口**,按唯一地址聚合全部出现位置(跨 .md/.py),支持**批量替换**(如把 UAT 地址一次性改成生产)。主体抽成可复用组件 `SkillAddressesPanel`(`pages/SkillAddressesPage.tsx` 导出,默认导出 `SkillAddressesPage` 仍保留旧路由 `/page/strategy/admin/skill-addresses` 作直链回退),**作为「技能地址管理」标签页嵌入管理员「技能管理」页 `pages/AdminSkillsPage.tsx`**(该页加 Tabs:「技能列表」+「技能地址管理」;入口在「设置和更多」下拉的「技能管理」`workspace-nav-menu.tsx` → `/page/workspace/admin/skills`,不再单列于 `components/page-sidebar.tsx` 左下角设置下拉):打开自动扫描并带**进度条**(扫描逐技能 / 应用逐文件 X/N,应对 150+ 技能),URL/IP 类型过滤 + 搜索 + 「片段批量套用」(如所有 `uat.4.cn` → `prod.4.cn`)、每地址「替换为」输入框(可展开看 `file:line` 片段)、内联**预览**、**应用**、**多关键词检索**(空格分隔全部命中),以及**修改记录**面板——每次应用生成一条记录(显示时间/改了哪些地址 from→to/文件数/处数),可**针对单次记录回退**(二次确认)。client + NDJSON 流式解析在 `strategy-components/api/skill-addresses.ts`。后端 `/api/skill-addresses`(**仅管理员**):所有文件 IO 走 `asyncio.to_thread` 不阻塞事件循环;`GET /scan`(+`/scan/stream`)、`POST /preview`、`POST /apply`(+`/apply/stream`)、`GET /backups`、`POST /rollback/{id}`;**子串安全替换**(基于 token span,不会误伤子串)、`expected_total` 乐观锁(409)、写前自动磁盘备份到 `{skills_root}/.address-edits/`(同时写一份 `{id}.manifest.json` 记录本次替换明细,供修改记录展示)、应用后刷新技能系统提示缓存。纯引擎 `deerflow/skills/address_scan.py`,router `app/gateway/routers/skill_addresses.py`,tests `tests/test_skill_addresses.py`。范围仅运行中技能,不动 `deploy/minimal/skills/`。
|
||||||
- **按钮管理 (Task Button Management, admin-only)**: configures the **可配置跳转按钮** rendered in the left task panel of the task deep-link workspaces (rwfx / 3qfx / xdfx, see `TaskFxWorkspace`). Each button = `{business, label, linkType, target, appendTaskId, idParam, idKind, enabled, sortOrder}`; `linkType` is `url` (外链 `window.open`, optional append of `?{idParam}={id}` — **`idParam` is the editable query-param name**, e.g. `id`/`task_id`), `business` (内部切换到另一业务, reuses `resolveTaskDeeplinkTarget`, always task id), or `purpose` (弹出当前任务的「任务目的」表格 via the shared `TaskPurposeDialog`, no target — configurable for any page incl. xdfx). **`host`(宿主路由)** asks the embedding parent to navigate: `runTaskButton` sends `window.parent.postMessage({ type:'HOST_NAVIGATE', path, queryParams:{ [idParam]: Number(taskId) } }, hostOrigin)` — `target` is the host route, `appendTaskId`/`idParam` become numeric `queryParams` (task id only), origin from `runtime-config.js` `VITE_HOST_NAVIGATE_ORIGIN` (else ancestorOrigins/referrer; never `*`). **`idKind` (`task`/`action`) only matters for xdfx**: its deep-link `?taskId=` is an 行动id and the 任务id is resolved at login (`fetchTaskIdFromAction`) — `TaskContext` now also stashes the raw `actionId`, so a url button can append either (default `task`). Page `TaskButtonsPage.tsx` (route `/page/strategy/admin/task-buttons`, **admin-gated**, entry「按钮管理」in the bottom-left settings dropdown of `components/page-sidebar.tsx`, both full + collapsed modes) has per-business tabs, edits name/type/target/启停/排序/增删, saves the **whole set atomically** (replace-all), plus 「导入默认」 to merge code-seed defaults. **Seed/fallback** (like 敏感词管理): only when the whole table is empty (fresh deploy, never saved) do the workspace + page fall back to `defaultTaskButtons(cfg)` — rwfx's url buttons (编辑/下一步/3q详情) built from the **live** `/api/public/config/task-deeplink` plus cross-business switch buttons for **all three** businesses (each switches to the other two — rwfx→三情/行动, 3qfx→任务/行动, xdfx→任务/三情). Once saved, the store is the sole source of truth. The old hardcoded jump buttons were migrated out: `TaskFxWorkspace` no longer hardcodes 切换到三情分析, `TaskActionsPanel` keeps only the non-jump 清除记录, and `TaskPurposePanel` dropped its 3q详情 button (now configurable) **and defaults the 目的树 table to collapsed** (chevron toggle). Helpers `core/auth/task-buttons.ts` (`TaskButton`, `defaultTaskButtons`, `resolveTaskButtons`, `buttonsForBusiness`, `runTaskButton`), client+hooks `strategy-components/api/task-buttons.ts`. Backend: `/api/task-buttons` (global/shared; open read, admin replace-all), store `deerflow.persistence.task_buttons` (table `task_buttons`, auto-created by `create_all`), wired `app.state.task_button_store`. Tests: `tests/test_task_buttons.py`.
|
- **按钮管理 (Task Button Management, admin-only)**: configures the **可配置跳转按钮** rendered in the left task panel of the task deep-link workspaces (rwfx / 3qfx / xdfx, see `TaskFxWorkspace`). Each button = `{business, label, linkType, target, appendTaskId, idParam, idKind, enabled, sortOrder}`; `linkType` is `url` (外链 `window.open`, optional append of `?{idParam}={id}` — **`idParam` is the editable query-param name**, e.g. `id`/`task_id`), `business` (内部切换到另一业务, reuses `resolveTaskDeeplinkTarget`, always task id), or `purpose` (弹出当前任务的「任务目的」表格 via the shared `TaskPurposeDialog`, no target — configurable for any page incl. xdfx). **`host`(宿主路由)** asks the embedding parent to navigate: `runTaskButton` sends `window.parent.postMessage({ type:'HOST_NAVIGATE', path, queryParams:{ [idParam]: Number(taskId) } }, hostOrigin)` — `target` is the host route, `appendTaskId`/`idParam` become numeric `queryParams` (task id only), origin from `runtime-config.js` `VITE_HOST_NAVIGATE_ORIGIN` (else ancestorOrigins/referrer; never `*`). **`idKind` (`task`/`action`) only matters for xdfx**: its deep-link `?taskId=` is an 行动id and the 任务id is resolved at login (`fetchTaskIdFromAction`) — `TaskContext` now also stashes the raw `actionId`, so a url button can append either (default `task`). Page `TaskButtonsPage.tsx` (route `/page/strategy/admin/task-buttons`, **admin-gated**, entry「按钮管理」in the bottom-left settings dropdown of `components/page-sidebar.tsx`, both full + collapsed modes) has per-business tabs, edits name/type/target/启停/排序/增删, saves the **whole set atomically** (replace-all), plus 「导入默认」 to merge code-seed defaults. **Seed/fallback** (like 敏感词管理): only when the whole table is empty (fresh deploy, never saved) do the workspace + page fall back to `defaultTaskButtons(cfg)` — rwfx's url buttons (编辑/下一步/3q详情) built from the **live** `/api/public/config/task-deeplink` plus cross-business switch buttons for **all three** businesses (each switches to the other two — rwfx→三情/行动, 3qfx→任务/行动, xdfx→任务/三情). Once saved, the store is the sole source of truth. The old hardcoded jump buttons were migrated out: `TaskFxWorkspace` no longer hardcodes 切换到三情分析, `TaskActionsPanel` keeps only the non-jump 清除记录, and `TaskPurposePanel` dropped its 3q详情 button (now configurable) **and defaults the 目的树 table to collapsed** (chevron toggle). Helpers `core/auth/task-buttons.ts` (`TaskButton`, `defaultTaskButtons`, `resolveTaskButtons`, `buttonsForBusiness`, `runTaskButton`), client+hooks `strategy-components/api/task-buttons.ts`. Backend: `/api/task-buttons` (global/shared; open read, admin replace-all), store `deerflow.persistence.task_buttons` (table `task_buttons`, auto-created by `create_all`), wired `app.state.task_button_store`. Tests: `tests/test_task_buttons.py`.
|
||||||
|
|||||||
@@ -615,7 +615,11 @@ export function AgentGallery() {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
)}
|
)}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<AgentGuide stage="list" eligible={isAgentGuideEnvironment()} />
|
<AgentGuide
|
||||||
|
stage="list"
|
||||||
|
eligible={isAgentGuideEnvironment()}
|
||||||
|
autoStart={false}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { Guide, type GuideStep } from "tdesign-react";
|
import { Guide, type GuideStep } from "tdesign-react";
|
||||||
|
|
||||||
import { env } from "@/env";
|
import { env } from "@/env";
|
||||||
@@ -13,7 +12,8 @@ import { isWujieEnv } from "@/core/embed/wujie";
|
|||||||
* 「浏览管理 → 创建与配置 → 发起问答」流程:
|
* 「浏览管理 → 创建与配置 → 发起问答」流程:
|
||||||
*
|
*
|
||||||
* - list :智能体列表页(/page/workspace/agents)——分区、搜索排序、卡片操作、
|
* - list :智能体列表页(/page/workspace/agents)——分区、搜索排序、卡片操作、
|
||||||
* 创建入口;最后一步「去创建智能体 →」直接跳到创建页衔接下一段。
|
* 创建入口;完成 / 跳过就地结束,不跳转创建页(创建页有自己的一段
|
||||||
|
* 引导,点「创建智能体」进入时自动演示)。
|
||||||
* - create :创建页(/page/workspace/agents/new)——对话式创建、基本信息、模型、
|
* - create :创建页(/page/workspace/agents/new)——对话式创建、基本信息、模型、
|
||||||
* 知识空间、SOUL.md、技能、保存。
|
* 知识空间、SOUL.md、技能、保存。
|
||||||
* - chat :智能体对话页(/page/workspace/agents/:id/chats/...)——侧栏切换与
|
* - chat :智能体对话页(/page/workspace/agents/:id/chats/...)——侧栏切换与
|
||||||
@@ -23,6 +23,10 @@ import { isWujieEnv } from "@/core/embed/wujie";
|
|||||||
* localStorage 标记,**只有用户显式点「完成」或「跳过」才写入**;中途刷新/离开
|
* localStorage 标记,**只有用户显式点「完成」或「跳过」才写入**;中途刷新/离开
|
||||||
* 不记忆,下次进入该页面还会自动演示。各页头部的「使用引导」按钮可随时强制重放。
|
* 不记忆,下次进入该页面还会自动演示。各页头部的「使用引导」按钮可随时强制重放。
|
||||||
*
|
*
|
||||||
|
* 自动演示范围:只有 create 段(进入新建页)与 chat 段自动弹;list 段挂在
|
||||||
|
* 智能体列表页但不自动演示(`autoStart={false}`,避免一进列表页就弹引导),
|
||||||
|
* 仅由头部「使用引导」按钮手动触发。
|
||||||
|
*
|
||||||
* 高亮目标通过 `data-agent-guide` 标记在各组件上;启动时按目标是否存在过滤步骤
|
* 高亮目标通过 `data-agent-guide` 标记在各组件上;启动时按目标是否存在过滤步骤
|
||||||
* (如新用户无智能体卡片、无预设常用问题时自动剔除该步)——Guide 对找不到的
|
* (如新用户无智能体卡片、无预设常用问题时自动剔除该步)——Guide 对找不到的
|
||||||
* 目标元素会抛错,不能盲传。
|
* 目标元素会抛错,不能盲传。
|
||||||
@@ -121,7 +125,7 @@ const LIST_STEPS: StepSpec[] = [
|
|||||||
mode: "popup",
|
mode: "popup",
|
||||||
placement: "bottom-left",
|
placement: "bottom-left",
|
||||||
title: "创建智能体",
|
title: "创建智能体",
|
||||||
body: "准备好了就点这里,开始创建你的专属智能体。点「去创建智能体」直接进入下一站。",
|
body: "准备好了就点这里,开始创建你的专属智能体;进入创建页后还会有一段配置引导。",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -260,17 +264,19 @@ function buildSteps(stage: AgentGuideStage): GuideStep[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 智能体三段引导的运行组件:`stage` 决定步骤内容,`eligible` 为false 时完全
|
* 智能体三段引导的运行组件:`stage` 决定步骤内容,`eligible` 为 false 时完全
|
||||||
* 静默(不监听、不自动弹)。挂在三个页面各自的作用域内。
|
* 静默(不监听、不自动弹)。挂在三个页面各自的作用域内;`autoStart` 为 false
|
||||||
|
* 时仅响应手动触发(列表页不自动弹,进新建页才自动演示)。
|
||||||
*/
|
*/
|
||||||
export function AgentGuide({
|
export function AgentGuide({
|
||||||
stage,
|
stage,
|
||||||
eligible,
|
eligible,
|
||||||
|
autoStart = true,
|
||||||
}: {
|
}: {
|
||||||
stage: AgentGuideStage;
|
stage: AgentGuideStage;
|
||||||
eligible: boolean;
|
eligible: boolean;
|
||||||
|
autoStart?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
|
||||||
const [current, setCurrent] = useState(-1);
|
const [current, setCurrent] = useState(-1);
|
||||||
const stepsRef = useRef<GuideStep[]>([]);
|
const stepsRef = useRef<GuideStep[]>([]);
|
||||||
|
|
||||||
@@ -285,7 +291,7 @@ export function AgentGuide({
|
|||||||
// 「已看过」只由 finish 回调(完成/跳过)写入。
|
// 「已看过」只由 finish 回调(完成/跳过)写入。
|
||||||
const autoTriedRef = useRef(false);
|
const autoTriedRef = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!eligible || autoTriedRef.current) return;
|
if (!eligible || !autoStart || autoTriedRef.current) return;
|
||||||
autoTriedRef.current = true;
|
autoTriedRef.current = true;
|
||||||
if (isStageFinished(stage)) return;
|
if (isStageFinished(stage)) return;
|
||||||
const timer = setTimeout(start, START_DELAY_MS);
|
const timer = setTimeout(start, START_DELAY_MS);
|
||||||
@@ -294,7 +300,7 @@ export function AgentGuide({
|
|||||||
// 组件在计时窗口内卸载(用户迅速离开页面)则下次进入再演示。
|
// 组件在计时窗口内卸载(用户迅速离开页面)则下次进入再演示。
|
||||||
autoTriedRef.current = false;
|
autoTriedRef.current = false;
|
||||||
};
|
};
|
||||||
}, [eligible, stage, start]);
|
}, [eligible, autoStart, stage, start]);
|
||||||
|
|
||||||
// 手动触发:页面头部「使用引导」按钮广播事件,忽略已看过记忆强制重放。
|
// 手动触发:页面头部「使用引导」按钮广播事件,忽略已看过记忆强制重放。
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -305,15 +311,11 @@ export function AgentGuide({
|
|||||||
window.removeEventListener(startEventName(stage), handler);
|
window.removeEventListener(startEventName(stage), handler);
|
||||||
}, [eligible, stage, start]);
|
}, [eligible, stage, start]);
|
||||||
|
|
||||||
// 完成 / 跳过是用户显式结束引导 —— 只有这两种情况才记忆「已看过」。
|
// 完成 / 跳过都是用户显式结束引导,就地关闭并记忆「已看过」,不跳转任何页面。
|
||||||
// list 阶段的最后一步按钮文案是「去创建智能体 →」,完成即跳转衔接下一段。
|
|
||||||
const finish = useCallback(() => {
|
const finish = useCallback(() => {
|
||||||
markStageFinished(stage);
|
markStageFinished(stage);
|
||||||
setCurrent(-1);
|
setCurrent(-1);
|
||||||
if (stage === "list") {
|
}, [stage]);
|
||||||
router.push("/page/workspace/agents/new");
|
|
||||||
}
|
|
||||||
}, [stage, router]);
|
|
||||||
|
|
||||||
if (!eligible) return null;
|
if (!eligible) return null;
|
||||||
|
|
||||||
@@ -324,9 +326,6 @@ export function AgentGuide({
|
|||||||
onChange={setCurrent}
|
onChange={setCurrent}
|
||||||
onFinish={finish}
|
onFinish={finish}
|
||||||
onSkip={finish}
|
onSkip={finish}
|
||||||
finishButtonProps={
|
|
||||||
stage === "list" ? { content: "去创建智能体 →" } : undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -19,17 +19,16 @@ const TOOLBOX_ICON_TONES = [
|
|||||||
* 简洁模式右侧工具箱。
|
* 简洁模式右侧工具箱。
|
||||||
*
|
*
|
||||||
* 菜单来源与左侧简洁菜单、设置和更多完全一致,由管理员在菜单管理中把项目
|
* 菜单来源与左侧简洁菜单、设置和更多完全一致,由管理员在菜单管理中把项目
|
||||||
* 放到 ``toolbox`` 泳道。点击右侧竖排按钮后展开单列竖向抽屉。
|
* 放到 ``toolbox`` 泳道。默认展开常显(不再藏进右侧按钮后面);点 X 或 Esc
|
||||||
|
* 收起后右侧出现竖排按钮,可再次展开。
|
||||||
*/
|
*/
|
||||||
export function CompactToolbox() {
|
export function CompactToolbox() {
|
||||||
const { toolboxItems } = useWorkspaceCompactNavItems();
|
const { toolboxItems } = useWorkspaceCompactNavItems();
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(true);
|
||||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||||
const closeRef = useRef<HTMLButtonElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => setOpen(false), [pathname]);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
|
|
||||||
@@ -41,7 +40,6 @@ export function CompactToolbox() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("keydown", handleKeyDown);
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
closeRef.current?.focus();
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("keydown", handleKeyDown);
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
};
|
};
|
||||||
@@ -73,22 +71,8 @@ export function CompactToolbox() {
|
|||||||
<span className="text-xs font-semibold leading-4">工具箱</span>
|
<span className="text-xs font-semibold leading-4">工具箱</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
tabIndex={-1}
|
|
||||||
aria-label="关闭工具箱抽屉"
|
|
||||||
aria-hidden={!open}
|
|
||||||
onClick={closeDrawer}
|
|
||||||
className={cn(
|
|
||||||
"fixed inset-0 z-[90] bg-black/20 transition-opacity duration-200",
|
|
||||||
open ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<aside
|
<aside
|
||||||
id="compact-toolbox-panel"
|
id="compact-toolbox-panel"
|
||||||
role="dialog"
|
|
||||||
aria-modal={open}
|
|
||||||
aria-label="工具箱"
|
aria-label="工具箱"
|
||||||
aria-hidden={!open}
|
aria-hidden={!open}
|
||||||
inert={!open}
|
inert={!open}
|
||||||
@@ -102,10 +86,9 @@ export function CompactToolbox() {
|
|||||||
<header className="relative flex h-12 shrink-0 items-center justify-center px-2">
|
<header className="relative flex h-12 shrink-0 items-center justify-center px-2">
|
||||||
<h2 className="text-sm font-semibold">工具箱</h2>
|
<h2 className="text-sm font-semibold">工具箱</h2>
|
||||||
<button
|
<button
|
||||||
ref={closeRef}
|
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="关闭工具箱"
|
aria-label="收起工具箱"
|
||||||
title="关闭"
|
title="收起"
|
||||||
onClick={closeDrawer}
|
onClick={closeDrawer}
|
||||||
className="absolute right-2 flex size-7 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
className="absolute right-2 flex size-7 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
>
|
>
|
||||||
@@ -131,10 +114,7 @@ export function CompactToolbox() {
|
|||||||
? "bg-primary/12 text-primary ring-1 ring-inset ring-primary/25"
|
? "bg-primary/12 text-primary ring-1 ring-inset ring-primary/25"
|
||||||
: "bg-muted/55 text-card-foreground hover:bg-primary/8 hover:text-primary",
|
: "bg-muted/55 text-card-foreground hover:bg-primary/8 hover:text-primary",
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => navigate(item.path ?? "/page/workspace/chats/new")}
|
||||||
setOpen(false);
|
|
||||||
navigate(item.path ?? "/page/workspace/chats/new");
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
useResearchSessions,
|
useResearchSessions,
|
||||||
type DeepResearchSession,
|
type DeepResearchSession,
|
||||||
} from "@/strategy-components/api/deep-research";
|
} from "@/strategy-components/api/deep-research";
|
||||||
|
import { useReportStructures } from "@/strategy-components/api/report-structures";
|
||||||
import { formatRelative } from "@/open-canvas/components/ai-writing/AIWritingHistoryDropdown";
|
import { formatRelative } from "@/open-canvas/components/ai-writing/AIWritingHistoryDropdown";
|
||||||
import { desensitize } from "@/lib/desensitize";
|
import { desensitize } from "@/lib/desensitize";
|
||||||
|
|
||||||
@@ -48,6 +49,25 @@ const RESEARCH_STATUS: Record<
|
|||||||
cancelled: { label: "已取消", className: "text-muted-foreground" },
|
cancelled: { label: "已取消", className: "text-muted-foreground" },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 深度研究会话把选中的报告模板快照为 custom_outline;用结构库里的内容反查标题。
|
||||||
|
* 内容不再存在或用户输入了自定义大纲时,明确展示为「自定义报告结构」,避免冒充模板名。
|
||||||
|
*/
|
||||||
|
function reportTemplateLabel(
|
||||||
|
session: DeepResearchSession,
|
||||||
|
structures: { title: string; content: string }[],
|
||||||
|
): string {
|
||||||
|
const config = session.config ?? {};
|
||||||
|
const outline = typeof config.custom_outline === "string"
|
||||||
|
? config.custom_outline.trim()
|
||||||
|
: typeof config.customOutline === "string"
|
||||||
|
? config.customOutline.trim()
|
||||||
|
: "";
|
||||||
|
|
||||||
|
if (!outline) return "默认报告结构";
|
||||||
|
return structures.find((structure) => structure.content.trim() === outline)?.title ?? "自定义报告结构";
|
||||||
|
}
|
||||||
|
|
||||||
export function ResearchHistoryTab({
|
export function ResearchHistoryTab({
|
||||||
query,
|
query,
|
||||||
onTotalChange,
|
onTotalChange,
|
||||||
@@ -57,6 +77,7 @@ export function ResearchHistoryTab({
|
|||||||
}) {
|
}) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { data, isLoading } = useResearchSessions(100);
|
const { data, isLoading } = useResearchSessions(100);
|
||||||
|
const structuresQuery = useReportStructures();
|
||||||
const actions = useResearchActions();
|
const actions = useResearchActions();
|
||||||
const [renameId, setRenameId] = useState<string | null>(null);
|
const [renameId, setRenameId] = useState<string | null>(null);
|
||||||
const [renameTitle, setRenameTitle] = useState("");
|
const [renameTitle, setRenameTitle] = useState("");
|
||||||
@@ -124,6 +145,7 @@ export function ResearchHistoryTab({
|
|||||||
className: "text-muted-foreground",
|
className: "text-muted-foreground",
|
||||||
};
|
};
|
||||||
const timestamp = session.updatedAt ?? session.createdAt ?? null;
|
const timestamp = session.updatedAt ?? session.createdAt ?? null;
|
||||||
|
const template = reportTemplateLabel(session, structuresQuery.data ?? []);
|
||||||
return (
|
return (
|
||||||
<HistoryRow
|
<HistoryRow
|
||||||
key={session.id}
|
key={session.id}
|
||||||
@@ -141,6 +163,10 @@ export function ResearchHistoryTab({
|
|||||||
<span>{session.sourceCount} 条来源</span>
|
<span>{session.sourceCount} 条来源</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<span className="text-muted-foreground/60">·</span>
|
||||||
|
<span className="max-w-56 truncate" title={`模板:${template}`}>
|
||||||
|
模板:{template}
|
||||||
|
</span>
|
||||||
{timestamp && (
|
{timestamp && (
|
||||||
<>
|
<>
|
||||||
<span className="text-muted-foreground/60">·</span>
|
<span className="text-muted-foreground/60">·</span>
|
||||||
|
|||||||
@@ -33,19 +33,36 @@ import {
|
|||||||
|
|
||||||
const ROUNDTABLE_PATH = "/page/workspace/roundtable/planning";
|
const ROUNDTABLE_PATH = "/page/workspace/roundtable/planning";
|
||||||
|
|
||||||
/** 草稿后台作业状态 → 小标签(映射与原侧栏列表一致)。 */
|
const ROUNDTABLE_STEP_LABEL: Record<DraftMeta["furthestStep"], string> = {
|
||||||
function statusOf(draft: DraftMeta): { label: string; className: string } {
|
1: "议题梳理",
|
||||||
if (draft.status === "running" || draft.status === "queued") {
|
2: "多智能体会商",
|
||||||
return { label: "进行中", className: "text-primary" };
|
3: "报告生成",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列表接口会返回草稿已推进到的工作台步骤以及最新后台作业状态。两者须同时展示:
|
||||||
|
* `furthestStep` 告诉用户做到哪一环,`status` 告诉用户是否已真正完成或被中断。
|
||||||
|
*/
|
||||||
|
function progressOf(draft: DraftMeta): {
|
||||||
|
label: string;
|
||||||
|
className: string;
|
||||||
|
stepLabel: string;
|
||||||
|
} {
|
||||||
|
const stepLabel = `第 ${draft.furthestStep} 步 · ${ROUNDTABLE_STEP_LABEL[draft.furthestStep]}`;
|
||||||
|
|
||||||
|
if (draft.status === "done" || (!draft.status && draft.furthestStep === 3)) {
|
||||||
|
return { label: "已完成", className: "text-emerald-600", stepLabel };
|
||||||
|
}
|
||||||
|
if (draft.status === "error") {
|
||||||
|
return { label: "执行失败", className: "text-rose-500", stepLabel: `停在${stepLabel}` };
|
||||||
|
}
|
||||||
|
if (draft.status === "cancelled") {
|
||||||
|
return { label: "已取消", className: "text-muted-foreground", stepLabel: `停在${stepLabel}` };
|
||||||
}
|
}
|
||||||
if (draft.status === "awaiting_input") {
|
if (draft.status === "awaiting_input") {
|
||||||
return { label: "待澄清", className: "text-amber-500" };
|
return { label: "等待确认", className: "text-amber-500", stepLabel: `当前${stepLabel}` };
|
||||||
}
|
}
|
||||||
if (draft.status === "error" || draft.status === "cancelled") {
|
return { label: "进行中", className: "text-primary", stepLabel: `当前${stepLabel}` };
|
||||||
return { label: "出错", className: "text-rose-500" };
|
|
||||||
}
|
|
||||||
// 无进行中作业时展示本次会商所用业务链(无链条 → 自由模式)。
|
|
||||||
return { label: draft.chainTitle || "自由模式", className: "text-muted-foreground" };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RoundtableHistoryTab({
|
export function RoundtableHistoryTab({
|
||||||
@@ -122,7 +139,7 @@ export function RoundtableHistoryTab({
|
|||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
{filtered.map((draft) => {
|
{filtered.map((draft) => {
|
||||||
const status = statusOf(draft);
|
const progress = progressOf(draft);
|
||||||
return (
|
return (
|
||||||
<HistoryRow
|
<HistoryRow
|
||||||
key={draft.id}
|
key={draft.id}
|
||||||
@@ -131,10 +148,18 @@ export function RoundtableHistoryTab({
|
|||||||
meta={
|
meta={
|
||||||
<>
|
<>
|
||||||
<HistoryStatusPill
|
<HistoryStatusPill
|
||||||
label={status.label}
|
label={progress.label}
|
||||||
className={status.className}
|
className={progress.className}
|
||||||
/>
|
/>
|
||||||
<span className="text-muted-foreground/60">·</span>
|
<span className="text-muted-foreground/60">·</span>
|
||||||
|
<span className="max-w-48 truncate" title={progress.stepLabel}>
|
||||||
|
{progress.stepLabel}
|
||||||
|
</span>
|
||||||
|
<span className="text-muted-foreground/60">·</span>
|
||||||
|
<span className="max-w-40 truncate" title={draft.chainTitle || "自由模式"}>
|
||||||
|
{draft.chainTitle || "自由模式"}
|
||||||
|
</span>
|
||||||
|
<span className="text-muted-foreground/60">·</span>
|
||||||
<span>{formatRelative(draft.updatedAt)}</span>
|
<span>{formatRelative(draft.updatedAt)}</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,9 @@ import type { JobStatus } from "@/roundtable-planning/lib/job-types";
|
|||||||
export interface DraftStep1Snapshot {
|
export interface DraftStep1Snapshot {
|
||||||
intentThreadId: string | null;
|
intentThreadId: string | null;
|
||||||
intentReady: IntentSummary | null;
|
intentReady: IntentSummary | null;
|
||||||
interactiveMessages: unknown[];
|
/** 早期草稿可能缺这两个数组字段 —— hydrate 按空数组兜底。 */
|
||||||
timelineEntries: unknown[];
|
interactiveMessages?: unknown[];
|
||||||
|
timelineEntries?: unknown[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DraftStep2Snapshot {
|
export interface DraftStep2Snapshot {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
* 卡片席位名优先用实时 agent 数据(`listAgents()`,与 `BusinessChainPicker` 一致),
|
* 卡片席位名优先用实时 agent 数据(`listAgents()`,与 `BusinessChainPicker` 一致),
|
||||||
* agent 改名后此弹窗也能同步显示;解析不到才回退到落库快照(`seat.name`)。
|
* agent 改名后此弹窗也能同步显示;解析不到才回退到落库快照(`seat.name`)。
|
||||||
*/
|
*/
|
||||||
import { Fragment, useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
ArrowRightIcon,
|
ArrowRightIcon,
|
||||||
ClockIcon,
|
ClockIcon,
|
||||||
@@ -593,8 +593,9 @@ function ChainSeatsFlow({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-3.5 flex flex-wrap items-center gap-1.5">
|
<div className="mt-3.5 flex flex-wrap items-center gap-1.5">
|
||||||
|
{/* 同 DispatchChainModal:div.contents 代替 <Fragment>,规避 dev tagger 注入 data-lov-id 的警告。 */}
|
||||||
{stages.map((stageIds, stageIdx) => (
|
{stages.map((stageIds, stageIdx) => (
|
||||||
<Fragment key={stageIdx}>
|
<div className="contents" key={stageIdx}>
|
||||||
{stageIdx > 0 && (
|
{stageIdx > 0 && (
|
||||||
<ArrowRightIcon className="size-3.5 shrink-0 text-muted-foreground/50" />
|
<ArrowRightIcon className="size-3.5 shrink-0 text-muted-foreground/50" />
|
||||||
)}
|
)}
|
||||||
@@ -609,7 +610,7 @@ function ChainSeatsFlow({
|
|||||||
) : (
|
) : (
|
||||||
seatTag(stageIds[0])
|
seatTag(stageIds[0])
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -234,6 +234,16 @@ export function DashboardChatPanel({
|
|||||||
}
|
}
|
||||||
className="min-h-8 max-h-56 flex-1 resize-none bg-transparent py-1.5 text-xs leading-relaxed text-slate-800 placeholder:text-slate-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60"
|
className="min-h-8 max-h-56 flex-1 resize-none bg-transparent py-1.5 text-xs leading-relaxed text-slate-800 placeholder:text-slate-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
/>
|
/>
|
||||||
|
{busy && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onPause}
|
||||||
|
title="中止当前绘制(之后可输入修改要求继续)"
|
||||||
|
className="mb-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-amber-100 border border-amber-200/60 text-amber-700 hover:bg-amber-200 transition active:scale-95"
|
||||||
|
>
|
||||||
|
<Pause className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={inputLocked || text.trim().length === 0}
|
disabled={inputLocked || text.trim().length === 0}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
* (business-chain.ts)。连线动画 CSS 在 `styles/roundtable-planning.css` 全局段
|
* (business-chain.ts)。连线动画 CSS 在 `styles/roundtable-planning.css` 全局段
|
||||||
* (Radix Dialog portal 到 body,不在 .roundtable-planning 作用域内,故用全局类 + 语义 token)。
|
* (Radix Dialog portal 到 body,不在 .roundtable-planning 作用域内,故用全局类 + 语义 token)。
|
||||||
*/
|
*/
|
||||||
import { Fragment, useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { CheckIcon, ChevronRightIcon, EyeIcon, Loader2Icon, SendIcon } from "lucide-react";
|
import { CheckIcon, ChevronRightIcon, EyeIcon, Loader2Icon, SendIcon } from "lucide-react";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -229,9 +229,10 @@ export function DispatchChainModal({
|
|||||||
state={coordinatorState}
|
state={coordinatorState}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 席位节点 */}
|
{/* 席位节点(div.contents 而非 <Fragment>:dev 期 tagger 会往具名 Fragment 注入
|
||||||
|
data-lov-id 触发 React 警告,contents 不生成盒子、布局不变)。 */}
|
||||||
{nodes.map((node) => (
|
{nodes.map((node) => (
|
||||||
<Fragment key={node.agentId}>
|
<div className="contents" key={node.agentId}>
|
||||||
<Connector
|
<Connector
|
||||||
active={node.state === "active"}
|
active={node.state === "active"}
|
||||||
done={node.state === "done"}
|
done={node.state === "done"}
|
||||||
@@ -242,7 +243,7 @@ export function DispatchChainModal({
|
|||||||
state={node.state}
|
state={node.state}
|
||||||
order={node.order}
|
order={node.order}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* 共识里程碑 */}
|
{/* 共识里程碑 */}
|
||||||
|
|||||||
@@ -221,8 +221,13 @@ export function Step2Panel(props: Step2PanelProps) {
|
|||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const step2ScrollRef = useRef<HTMLDivElement>(null);
|
const step2ScrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
// contentRef 与 Step1Panel 同款:消息内容(markdown/代码高亮/折叠卡/write_file 预览)在
|
||||||
|
// 状态更新之后才继续长高,deps 效果抓不到,靠 ResizeObserver 跟随实际内容高度。
|
||||||
|
const step2ContentRef = useRef<HTMLDivElement>(null);
|
||||||
const { atBottom: step2AtBottom, scrollToBottom: scrollStep2ToBottom } =
|
const { atBottom: step2AtBottom, scrollToBottom: scrollStep2ToBottom } =
|
||||||
useAutoScroll(step2ScrollRef, [step2RoundtableDialogues]);
|
useAutoScroll(step2ScrollRef, [step2RoundtableDialogues], {
|
||||||
|
contentRef: step2ContentRef,
|
||||||
|
});
|
||||||
|
|
||||||
// 「回答速度指示器」—— 与主页面共用全局开关 streamSpeedMeterEnabled。
|
// 「回答速度指示器」—— 与主页面共用全局开关 streamSpeedMeterEnabled。
|
||||||
// 度量始终计算(成本低、便于中途开启时也有时长基线),仅展示按开关门控。
|
// 度量始终计算(成本低、便于中途开启时也有时长基线),仅展示按开关门控。
|
||||||
@@ -601,14 +606,15 @@ export function Step2Panel(props: Step2PanelProps) {
|
|||||||
// 竖滚条滑块时极易误触发横向拖动、抓不住滑块。宽内容(代码块/表格)在各自气泡内
|
// 竖滚条滑块时极易误触发横向拖动、抓不住滑块。宽内容(代码块/表格)在各自气泡内
|
||||||
// 已有 max-w-full + overflow-x-auto 自带横滚,故此处裁掉横向不影响查看。
|
// 已有 max-w-full + overflow-x-auto 自带横滚,故此处裁掉横向不影响查看。
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-1 min-h-0 overflow-y-auto overflow-x-hidden p-6 space-y-4",
|
"flex-1 min-h-0 overflow-y-auto overflow-x-hidden",
|
||||||
useStep1InputAppearance
|
useStep1InputAppearance
|
||||||
? "eightbf-roundtable-step2-log"
|
? "eightbf-roundtable-step2-log"
|
||||||
: "bg-white border-t border-slate-150 border-b border-slate-150 shadow-inner",
|
: "bg-white border-t border-slate-150 border-b border-slate-150 shadow-inner",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<div ref={step2ContentRef} className="flex min-h-full flex-col space-y-4 p-6">
|
||||||
{isInitializing && step2RoundtableDialogues.length === 0 && (
|
{isInitializing && step2RoundtableDialogues.length === 0 && (
|
||||||
<div className="flex h-full flex-col items-center justify-center gap-3 py-12 text-slate-500">
|
<div className="flex flex-1 flex-col items-center justify-center gap-3 py-12 text-slate-500">
|
||||||
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
||||||
<div className="text-sm font-medium text-slate-700">正在初始化圆桌...</div>
|
<div className="text-sm font-medium text-slate-700">正在初始化圆桌...</div>
|
||||||
<div className="text-xs text-slate-400">正在为各席位建立独立线程,请稍候</div>
|
<div className="text-xs text-slate-400">正在为各席位建立独立线程,请稍候</div>
|
||||||
@@ -705,6 +711,7 @@ export function Step2Panel(props: Step2PanelProps) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{!step2AtBottom && (
|
{!step2AtBottom && (
|
||||||
<ScrollToBottomButton
|
<ScrollToBottomButton
|
||||||
onClick={scrollStep2ToBottom}
|
onClick={scrollStep2ToBottom}
|
||||||
@@ -848,6 +855,8 @@ export function Step2Panel(props: Step2PanelProps) {
|
|||||||
onAddFiles={addInjectFiles}
|
onAddFiles={addInjectFiles}
|
||||||
onRemoveFile={removeInjectFile}
|
onRemoveFile={removeInjectFile}
|
||||||
onSend={handleInject}
|
onSend={handleInject}
|
||||||
|
busy={isStep2Responding}
|
||||||
|
onPause={onPauseRoundtable}
|
||||||
hasConsensus={hasConsensus}
|
hasConsensus={hasConsensus}
|
||||||
onProceed={onProceedToStep3}
|
onProceed={onProceedToStep3}
|
||||||
/>
|
/>
|
||||||
@@ -888,6 +897,8 @@ function Step2LegacyComposer({
|
|||||||
onAddFiles,
|
onAddFiles,
|
||||||
onRemoveFile,
|
onRemoveFile,
|
||||||
onSend,
|
onSend,
|
||||||
|
busy,
|
||||||
|
onPause,
|
||||||
hasConsensus,
|
hasConsensus,
|
||||||
onProceed,
|
onProceed,
|
||||||
}: {
|
}: {
|
||||||
@@ -897,6 +908,9 @@ function Step2LegacyComposer({
|
|||||||
onAddFiles: (files: File[]) => void;
|
onAddFiles: (files: File[]) => void;
|
||||||
onRemoveFile: (index: number) => void;
|
onRemoveFile: (index: number) => void;
|
||||||
onSend: () => void;
|
onSend: () => void;
|
||||||
|
/** 编排中或席位仍在流式输出 —— 此时显示输入框「暂停」键(与第一步输入框一致)。 */
|
||||||
|
busy: boolean;
|
||||||
|
onPause: () => void;
|
||||||
hasConsensus: boolean;
|
hasConsensus: boolean;
|
||||||
onProceed: () => void;
|
onProceed: () => void;
|
||||||
}) {
|
}) {
|
||||||
@@ -958,6 +972,19 @@ function Step2LegacyComposer({
|
|||||||
>
|
>
|
||||||
<Paperclip className="size-4" />
|
<Paperclip className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
|
{busy && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onPause}
|
||||||
|
title="暂停当前会商编排,可稍后恢复"
|
||||||
|
className="shrink-0 cursor-pointer rounded-xl bg-amber-100 px-3 py-2.5 text-xs font-bold text-amber-800 shadow-sm transition hover:bg-amber-200 active:scale-95"
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
<Pause className="size-3" />
|
||||||
|
暂停
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onSend}
|
onClick={onSend}
|
||||||
|
|||||||
@@ -388,6 +388,7 @@ export function Step3ReportMode({
|
|||||||
error={error}
|
error={error}
|
||||||
themeMismatch={themeMismatch}
|
themeMismatch={themeMismatch}
|
||||||
onRetry={() => redraw()}
|
onRetry={() => redraw()}
|
||||||
|
onPause={pause}
|
||||||
onRevisionSubmit={interject}
|
onRevisionSubmit={interject}
|
||||||
onArtifactStepClick={openArtifactFromStep}
|
onArtifactStepClick={openArtifactFromStep}
|
||||||
onArtifactPreview={openArtifactPreview}
|
onArtifactPreview={openArtifactPreview}
|
||||||
@@ -513,6 +514,7 @@ function AgentProgressPanel({
|
|||||||
error,
|
error,
|
||||||
themeMismatch,
|
themeMismatch,
|
||||||
onRetry,
|
onRetry,
|
||||||
|
onPause,
|
||||||
onRevisionSubmit,
|
onRevisionSubmit,
|
||||||
onArtifactStepClick,
|
onArtifactStepClick,
|
||||||
onArtifactPreview,
|
onArtifactPreview,
|
||||||
@@ -527,6 +529,8 @@ function AgentProgressPanel({
|
|||||||
error: string | null;
|
error: string | null;
|
||||||
themeMismatch: boolean;
|
themeMismatch: boolean;
|
||||||
onRetry: () => void;
|
onRetry: () => void;
|
||||||
|
/** 输入框「暂停」键 —— 绘制中显示,中断在途请求并把当前气泡收尾为「已暂停」。 */
|
||||||
|
onPause: () => void;
|
||||||
onRevisionSubmit: (revisionInstruction: string) => void;
|
onRevisionSubmit: (revisionInstruction: string) => void;
|
||||||
onArtifactStepClick: UseStep3ReportReturn["openArtifactFromStep"];
|
onArtifactStepClick: UseStep3ReportReturn["openArtifactFromStep"];
|
||||||
onArtifactPreview: UseStep3ReportReturn["openArtifactPreview"];
|
onArtifactPreview: UseStep3ReportReturn["openArtifactPreview"];
|
||||||
@@ -700,6 +704,16 @@ function AgentProgressPanel({
|
|||||||
}
|
}
|
||||||
className="min-h-8 max-h-56 flex-1 resize-none bg-transparent py-1.5 text-xs leading-relaxed text-slate-800 placeholder:text-slate-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60"
|
className="min-h-8 max-h-56 flex-1 resize-none bg-transparent py-1.5 text-xs leading-relaxed text-slate-800 placeholder:text-slate-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
/>
|
/>
|
||||||
|
{busy && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onPause}
|
||||||
|
title="暂停当前绘制(沙箱保留已生成内容,之后可输入修改要求继续)"
|
||||||
|
className="mb-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-amber-100 border border-amber-200/60 text-amber-700 hover:bg-amber-200 transition active:scale-95"
|
||||||
|
>
|
||||||
|
<Pause className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={inputLocked || revisionText.trim().length === 0}
|
disabled={inputLocked || revisionText.trim().length === 0}
|
||||||
|
|||||||
@@ -283,8 +283,8 @@ export function useDraftPersistence(
|
|||||||
|
|
||||||
/** 当前会话是否有值得保存的内容。 */
|
/** 当前会话是否有值得保存的内容。 */
|
||||||
const hasSavableContent = useCallback((step1: Step1Snapshot, step2: DraftStep2Snapshot): boolean => {
|
const hasSavableContent = useCallback((step1: Step1Snapshot, step2: DraftStep2Snapshot): boolean => {
|
||||||
const hasUserMessage = step1.interactiveMessages.some((m) => m.sender === "user");
|
const hasUserMessage = step1.interactiveMessages?.some((m) => m.sender === "user") ?? false;
|
||||||
return hasUserMessage || step2.step2RoundtableDialogues.length > 0;
|
return hasUserMessage || (step2.step2RoundtableDialogues?.length ?? 0) > 0;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
} from "@/roundtable-planning/components/IntentClarificationCard";
|
} from "@/roundtable-planning/components/IntentClarificationCard";
|
||||||
import type { ClarificationPayload } from "@/roundtable-planning/api/multi-agent";
|
import type { ClarificationPayload } from "@/roundtable-planning/api/multi-agent";
|
||||||
import {
|
import {
|
||||||
|
normalizeIntentReadySummary,
|
||||||
nowTime,
|
nowTime,
|
||||||
splitIntentReadyBlock,
|
splitIntentReadyBlock,
|
||||||
} from "@/roundtable-planning/lib/roundtable-constants";
|
} from "@/roundtable-planning/lib/roundtable-constants";
|
||||||
@@ -82,8 +83,9 @@ export type TimelineEntry = {
|
|||||||
export type Step1Snapshot = {
|
export type Step1Snapshot = {
|
||||||
intentThreadId: string | null;
|
intentThreadId: string | null;
|
||||||
intentReady: IntentSummary | null;
|
intentReady: IntentSummary | null;
|
||||||
interactiveMessages: InteractiveMessage[];
|
/** 早期草稿可能缺这两个数组字段 —— hydrate 按空数组兜底,避免 undefined 流进渲染层。 */
|
||||||
timelineEntries: TimelineEntry[];
|
interactiveMessages?: InteractiveMessage[];
|
||||||
|
timelineEntries?: TimelineEntry[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface UseStep1IntentOptions {
|
export interface UseStep1IntentOptions {
|
||||||
@@ -699,11 +701,14 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
|
|||||||
setPendingIntentClarificationMsgId(null);
|
setPendingIntentClarificationMsgId(null);
|
||||||
setIntentClarificationSelections(new Map());
|
setIntentClarificationSelections(new Map());
|
||||||
setIntentThreadId(snapshot.intentThreadId);
|
setIntentThreadId(snapshot.intentThreadId);
|
||||||
setIntentReady(snapshot.intentReady);
|
// 旧草稿存的 summary 可能缺 constraints/assumptions(实时流两条入口都过
|
||||||
|
// normalizeIntentReadySummary,唯独这里直接落 state)—— 先归一化再恢复,
|
||||||
|
// 保证下游 IntentSummaryCard 等消费者拿到的一定是数组。
|
||||||
|
setIntentReady(normalizeIntentReadySummary(snapshot.intentReady));
|
||||||
setIntentInitError(null);
|
setIntentInitError(null);
|
||||||
setIntentInitErrorDetail(null);
|
setIntentInitErrorDetail(null);
|
||||||
setInteractiveMessages(snapshot.interactiveMessages);
|
setInteractiveMessages(snapshot.interactiveMessages ?? []);
|
||||||
setTimelineEntries(snapshot.timelineEntries);
|
setTimelineEntries(snapshot.timelineEntries ?? []);
|
||||||
step1InitRef.current = true;
|
step1InitRef.current = true;
|
||||||
// 让任何在途的旧 init 在 resolve 时被识别为过期,丢弃 setState。
|
// 让任何在途的旧 init 在 resolve 时被识别为过期,丢弃 setState。
|
||||||
step1InitGenRef.current += 1;
|
step1InitGenRef.current += 1;
|
||||||
|
|||||||
@@ -214,7 +214,8 @@ export type Step2Snapshot = {
|
|||||||
selectedAgents?: SelectedAgent[];
|
selectedAgents?: SelectedAgent[];
|
||||||
threadIds: ThreadIdMap | null;
|
threadIds: ThreadIdMap | null;
|
||||||
coordinatorName: string | null;
|
coordinatorName: string | null;
|
||||||
step2RoundtableDialogues: Step2Dialogue[];
|
/** 早期草稿 / 旧版后台作业写入的 run 可能缺这字段 —— hydrate 按空数组兜底。 */
|
||||||
|
step2RoundtableDialogues?: Step2Dialogue[];
|
||||||
lastLeaderContent: string;
|
lastLeaderContent: string;
|
||||||
hasConsensus: boolean;
|
hasConsensus: boolean;
|
||||||
consensusPercentage: number;
|
consensusPercentage: number;
|
||||||
@@ -3408,7 +3409,7 @@ export function useStep2Orchestration(
|
|||||||
// 气泡仍显示「调度中 / 等待响应 / 正在输出…」+ 加载光标,让人误以为还在进行(其实早已结束)。
|
// 气泡仍显示「调度中 / 等待响应 / 正在输出…」+ 加载光标,让人误以为还在进行(其实早已结束)。
|
||||||
// streaming===true 与「加载中」文案一一对应(finalizeDialogue 收尾时必置 false),故据此即可精确识别。
|
// streaming===true 与「加载中」文案一一对应(finalizeDialogue 收尾时必置 false),故据此即可精确识别。
|
||||||
setStep2RoundtableDialogues(
|
setStep2RoundtableDialogues(
|
||||||
snapshot.step2RoundtableDialogues.map((d) => {
|
(snapshot.step2RoundtableDialogues ?? []).map((d) => {
|
||||||
if (!d.streaming) return d;
|
if (!d.streaming) return d;
|
||||||
const hasText = (d.content ?? "").trim().length > 0;
|
const hasText = (d.content ?? "").trim().length > 0;
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
*
|
*
|
||||||
* 详见 `docs/roundtable-dag-orchestration.md` §2(Phase 2,分层编辑器)。
|
* 详见 `docs/roundtable-dag-orchestration.md` §2(Phase 2,分层编辑器)。
|
||||||
*/
|
*/
|
||||||
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
@@ -1238,7 +1238,8 @@ export default function BusinessChainEditorPage() {
|
|||||||
{ n: 1 as const, label: "链条编排" },
|
{ n: 1 as const, label: "链条编排" },
|
||||||
{ n: 2 as const, label: "基本信息" },
|
{ n: 2 as const, label: "基本信息" },
|
||||||
]).map((s, i) => (
|
]).map((s, i) => (
|
||||||
<Fragment key={s.n}>
|
// div.contents 代替 <Fragment>:dev tagger 注入 data-lov-id 会触发 React 警告。
|
||||||
|
<div className="contents" key={s.n}>
|
||||||
{i > 0 && <span className="h-px w-10 bg-border" />}
|
{i > 0 && <span className="h-px w-10 bg-border" />}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1261,7 +1262,7 @@ export default function BusinessChainEditorPage() {
|
|||||||
</span>
|
</span>
|
||||||
{s.label}
|
{s.label}
|
||||||
</button>
|
</button>
|
||||||
</Fragment>
|
</div>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
* 仍走旧实现,本页不复用它,避免互相牵连。
|
* 仍走旧实现,本页不复用它,避免互相牵连。
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
Fragment,
|
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
@@ -853,8 +852,9 @@ function ChainFlow({ chain }: { chain: BusinessChain }) {
|
|||||||
style={{ maxHeight: 220 }}
|
style={{ maxHeight: 220 }}
|
||||||
>
|
>
|
||||||
<div className="flex min-w-max items-stretch gap-3 py-1">
|
<div className="flex min-w-max items-stretch gap-3 py-1">
|
||||||
|
{/* div.contents 代替 <Fragment>:dev tagger 注入 data-lov-id 会触发 React 警告。 */}
|
||||||
{stages.map((stage, sIdx) => (
|
{stages.map((stage, sIdx) => (
|
||||||
<Fragment key={sIdx}>
|
<div className="contents" key={sIdx}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex shrink-0 flex-col rounded-xl border p-2.5",
|
"flex shrink-0 flex-col rounded-xl border p-2.5",
|
||||||
@@ -897,7 +897,7 @@ function ChainFlow({ chain }: { chain: BusinessChain }) {
|
|||||||
<ArrowRightIcon className="size-4 text-zinc-300 dark:text-[#4A6491]" />
|
<ArrowRightIcon className="size-4 text-zinc-300 dark:text-[#4A6491]" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1054,8 +1054,9 @@ function TableFlowSummary({ chain }: { chain: BusinessChain }) {
|
|||||||
const { stages, seatById } = useChainStages(chain);
|
const { stages, seatById } = useChainStages(chain);
|
||||||
return (
|
return (
|
||||||
<div className="custom-scrollbar flex max-w-[280px] select-none items-center gap-1.5 overflow-x-auto py-1">
|
<div className="custom-scrollbar flex max-w-[280px] select-none items-center gap-1.5 overflow-x-auto py-1">
|
||||||
|
{/* div.contents 代替 <Fragment>:dev tagger 注入 data-lov-id 会触发 React 警告。 */}
|
||||||
{stages.map((stage, sIdx) => (
|
{stages.map((stage, sIdx) => (
|
||||||
<Fragment key={sIdx}>
|
<div className="contents" key={sIdx}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex shrink-0 items-center rounded border px-2 py-1",
|
"flex shrink-0 items-center rounded border px-2 py-1",
|
||||||
@@ -1073,7 +1074,7 @@ function TableFlowSummary({ chain }: { chain: BusinessChain }) {
|
|||||||
{sIdx < stages.length - 1 && (
|
{sIdx < stages.length - 1 && (
|
||||||
<span className="shrink-0 text-zinc-300 dark:text-[#4A6491]">→</span>
|
<span className="shrink-0 text-zinc-300 dark:text-[#4A6491]">→</span>
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4343,7 +4343,7 @@ function IntentSummaryCard({
|
|||||||
)}
|
)}
|
||||||
{showFullDetail && (
|
{showFullDetail && (
|
||||||
<ul className="text-xs text-slate-700 space-y-2.5 font-normal">
|
<ul className="text-xs text-slate-700 space-y-2.5 font-normal">
|
||||||
{intentReady.constraints.map((c, i) => (
|
{(intentReady.constraints ?? []).map((c, i) => (
|
||||||
<li key={`c-${i}`} className="flex items-baseline gap-2 leading-relaxed">
|
<li key={`c-${i}`} className="flex items-baseline gap-2 leading-relaxed">
|
||||||
<span className="text-primary text-base leading-none">•</span>
|
<span className="text-primary text-base leading-none">•</span>
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
Reference in new issue
Block a user