From 6cf4404243af57e9b15d0d59c7fa68e334ddd040 Mon Sep 17 00:00:00 2001 From: fengxiangdebaozi <10845145+fengxiangdebaozi@user.noreply.gitee.com> Date: Tue, 26 May 2026 14:48:06 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E6=8F=90=E7=A4=BA=E8=AF=8D=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend-web/src/components/workspace/input-box.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend-web/src/components/workspace/input-box.tsx b/frontend-web/src/components/workspace/input-box.tsx index d547bd75..986c5423 100644 --- a/frontend-web/src/components/workspace/input-box.tsx +++ b/frontend-web/src/components/workspace/input-box.tsx @@ -63,6 +63,7 @@ import { useI18n } from "@/core/i18n/hooks"; import { useModels } from "@/core/models/hooks"; import type { AgentThreadContext } from "@/core/threads"; import { textOfMessage } from "@/core/threads/utils"; +import { desensitize } from "@/lib/desensitize"; import { cn } from "@/lib/utils"; import { AgentSelector } from "@/components/workspace/agents/agent-selector"; @@ -500,7 +501,7 @@ export function InputBox({ setTimeout(() => requestFormSubmit(), 0); }} > - {q.title} + {desensitize(q.title)} ))} From 45eb46c3f383c81768960128fb0c1fcc8c0606ae Mon Sep 17 00:00:00 2001 From: fengxiangdebaozi <10845145+fengxiangdebaozi@user.noreply.gitee.com> Date: Tue, 26 May 2026 19:31:59 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E6=99=BA=E8=83=BD=E4=BD=93=E5=88=97?= =?UTF-8?q?=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend-web/docs/ui修改-深蓝.md | 670 ++++++++++++++++++ frontend-web/src/App.tsx | 4 +- frontend-web/src/components/page-sidebar.tsx | 14 +- frontend-web/src/contexts/ThemeContext.tsx | 6 +- frontend-web/src/lib/desensitize.ts | 12 + .../roundtable-planning/api/multi-agent.ts | 39 +- .../components/RecommendAgentsDialog.tsx | 305 ++++++-- .../hooks/useAutoScroll.ts | 78 +- .../pages/RoundtablePlanningPage.tsx | 471 +++++++++--- .../app/gateway/routers/multi_agent.py | 27 +- offline-backend-20260512/backend/config.yaml | 2 +- 11 files changed, 1457 insertions(+), 171 deletions(-) diff --git a/frontend-web/docs/ui修改-深蓝.md b/frontend-web/docs/ui修改-深蓝.md index a2d6a432..54fec496 100644 --- a/frontend-web/docs/ui修改-深蓝.md +++ b/frontend-web/docs/ui修改-深蓝.md @@ -1535,3 +1535,673 @@ Hover 弹出 Tooltip(沿用 §9.3.3 Tooltip 配色),承载完整说明文 --history-sep: #4A6491; } ``` + +--- + +## 15. 选择器(Select / Multi-Select / Cascader) + +> 对应设计稿「多项选择.png」。覆盖**多项选择**与**多项带搜索**两种主要变体(基础单选选择器同规格,仅触发器不渲染 Tag、面板项不带复选框;级联选择器原则相同,差异在面板分栏横向铺开)。配色全部从 §1.1.3 渐蓝系(ZS-002)与 §1.1.5 深蓝系(ZS-004)取,与 §14 搜索框完全对齐。 + +### 15.1 公共规格 + +#### 15.1.1 触发器(Trigger) + +触发器即「请选择 ▾」或「已选 Tag … ▾」那一行。尺寸、边框、状态机与 §14.1 输入框完全对齐: + +| 属性 | 取值 | 来源 | +|------|------|------| +| 高度 | `32px` | §14.1 | +| 圆角 | `2px` | §4.1 | +| 水平 padding | `--spacing-3` (12px) | §7.1 | +| 背景 | `#0C1836` (ZS-004-09) | — | +| 边框 | `1px solid #1A2F5A` (ZS-004-07) | — | +| 占位文字 "请选择" | `14px Regular`,`#6E88B0` (ZS-004-04) | — | +| 右侧箭头 `▾` | `12px`,`#6E88B0`,hover `#4096FF` | ZS-002-05 | + +边框状态机(默认 / hover / focus / 失效 / 错误)直接复用 §14.1.1。 + +#### 15.1.2 下拉面板(Panel) + +面板沿用 §14.4.4 联想下拉的容器规格,仅项内容不同: + +| 属性 | 取值 | +|------|------| +| 背景 | `#122347` (ZS-004-08) | +| 边框 | `1px solid #1A2F5A` (ZS-004-07) | +| 阴影 | `0 4px 16px rgba(6, 14, 36, 0.6)`(§3.1.4 三级阴影) | +| 圆角 | `2px` | +| 项高度 | `36px` | +| 项水平 padding | `--spacing-3` (12px) | +| 项字号 | `14px Regular`,`#C5CFE0` (ZS-004-02) | +| 面板宽度 | 与触发器等宽(搜索框、底部按钮均内嵌等宽) | +| 面板与触发器纵向间距 | `--spacing-1` (4px) | + +#### 15.1.3 项状态 + +| 状态 | 背景 | 文字 | +|------|------|------| +| 默认 | `transparent` | `#C5CFE0` (ZS-004-02) | +| Hover / 键盘焦点 | `rgba(22,119,255,0.16)` 或 `#1A2F5A` | `#FFFFFF` | +| 已选中 | `transparent`(依赖左侧复选框示态,不改行底色) | `#FFFFFF` | +| 禁用 | `transparent` | `#4A6491` (ZS-004-05) | + +点击行的任意位置等价于点击复选框(命中区 = 整行)。 + +--- + +### 15.2 多项选择 + +> 触发器内渲染**已选 Tag 列表**而不是单行文字;面板项左侧带**复选框**,多选不关闭面板。 + +``` +┌─────────────────────────────────────┐ +│ n1 × n2 × +1 × ▾ │ ← 触发器(已选 Tag) +└─────────────────────────────────────┘ +┌─────────────────────────────────────┐ ← 下拉面板 +│ ☑ n1 │ +│ ☐ n2 │ +│ ☐ n3 │ +│ ☐ n4 │ +│ ☐ n5 │ +├─────────────────────────────────────┤ +│ [+ 添加项] │ ← 可选:底部「添加」按钮(§15.2.4) +└─────────────────────────────────────┘ +``` + +#### 15.2.1 已选 Tag + +触发器内每个已选项渲染为一颗 Tag。**实心主色蓝底 + 白字**,右侧带关闭按钮 `×`。 + +| 元素 | 色值 / 规格 | 来源 | +|------|-------------|------| +| 背景 | `#1677FF` (ZS-002-06) | — | +| 文字 | `12px Regular`,`#FFFFFF` | — | +| 圆角 | `2px` | — | +| 内边距 | `2px 6px` | — | +| Tag 间距 | `--spacing-1` (4px) | §7.1 | +| 关闭按钮 `×` | `12px`,`rgba(255,255,255,0.72)`,hover `#FFFFFF` | — | +| 折叠展示 `+N ×` | 当选中超过宽度可容纳时,把剩余 N 项收成一颗 `+N ×` Tag。Hover 显示 Tooltip 列出剩余项名;点击 `×` 清空溢出选项 | — | + +#### 15.2.2 复选项 + +| 元素 | 色值 / 规格 | 来源 | +|------|-------------|------| +| 复选框尺寸 | `14px × 14px` | — | +| 复选框圆角 | `2px` | — | +| 未选边框 | `1px solid #6E88B0` (ZS-004-04) | — | +| 未选背景 | `transparent` | — | +| 已选填充 | `#1677FF` (ZS-002-06) | — | +| 已选勾 ✓ | `#FFFFFF`,描边 `2px` | — | +| 半选填充 | `#1677FF` | — | +| 半选指示 | 中央横线 `width 8px / height 2px / #FFFFFF` | — | +| Hover 边框 | `#4096FF` (ZS-002-05) | — | +| 禁用边框 | `#4A6491` (ZS-004-05) + 背景 `rgba(22,119,255,0.04)` | — | +| 复选框 → 文字间距 | `--spacing-2` (8px) | §7.1 | + +#### 15.2.3 全选模式(顶部「全部」节点) + +面板首行可选地渲染一个「全部」聚合节点,作用类似一次性勾 / 取消所有项: + +``` +┌─────────────────────────────────────┐ +│ ▢ 全部 │ ← 半选 / 未选 / 全选三态 +│ ☑ n1 │ +│ ☐ n2 │ +└─────────────────────────────────────┘ +``` + +| 子状态 | 复选框外观 | 触发动作 | +|--------|-----------|----------| +| 一个都没选 | 空心,边框 `#6E88B0` | 点击 → 勾上**全部**子项 | +| 部分选中 | 实心 `#1677FF` + 中央 `2px` 白色横线(半选) | 点击 → 勾上**全部**剩余未选 | +| 全部选中 | 实心 `#1677FF` + 白色勾 ✓ | 点击 → 清空全部 | + +文字 "全部" 与下方项同色 `#C5CFE0`,不加粗。 + +#### 15.2.4 底部「+ 添加项」按钮 + +面板底部可选地嵌入一颗虚线主色按钮,常用于「在已知池子里允许用户**就地新增**一项」(如标签、自定义分类): + +``` +┌─────────────────────────────────────┐ +│ [+ 正常] │ +└─────────────────────────────────────┘ +``` + +| 元素 | 色值 / 规格 | 来源 | +|------|-------------|------| +| 高度 | `32px`(与复选项行齐) | — | +| 边框 | `1px dashed #1677FF` (ZS-002-06) | — | +| 背景 | `transparent` | — | +| 文字 | `14px Regular`,`#4096FF` (ZS-002-05) | — | +| 前缀图标 `+` | `12px`,与文字同色 | — | +| Hover | 背景 `rgba(22,119,255,0.16)`,文字 `#1677FF` | ZS-002-05 → ZS-002-06 | +| Active | 背景 `rgba(22,119,255,0.24)`,文字 `#FFFFFF` | — | +| 与最后一条复选项纵向分隔 | 上方分割线 `1px solid #1A2F5A` + `--spacing-1` (4px) 间距 | §6.1 | + +点击行为由业务自行实现(如弹出输入框、内联编辑)。 + +--- + +### 15.3 多项带搜索 + +> 在 §15.2 基础上,**面板顶部嵌入一行搜索框**,输入时实时过滤复选项列表。其它结构(已选 Tag / 复选项 / 全选 / 底部添加)完全沿用 §15.2。 + +``` +┌─────────────────────────────────────┐ +│ n1 × +1 × ▾ │ +└─────────────────────────────────────┘ +┌─────────────────────────────────────┐ +│ [ 搜索 🔍 ] │ ← 嵌入式搜索框(§15.3.1) +├─────────────────────────────────────┤ +│ ▢ 全部 │ +│ ☑ n1 │ +│ ☐ n2 │ +│ ☐ n3 │ +└─────────────────────────────────────┘ +``` + +#### 15.3.1 嵌入式搜索框 + +搜索框尺寸采用 §14.1 公共规格的**小号**(高度 28px),保持与面板视觉上的"嵌入感": + +| 属性 | 取值 | +|------|------| +| 高度 | `28px`(小号) | +| 水平 padding | `--spacing-2` (8px) | +| 圆角 | `2px` | +| 背景 | `#0C1836` (ZS-004-09,比面板背景深一档) | +| 边框 | `1px solid #1A2F5A` (ZS-004-07) | +| 文字 | `14px Regular`,`#C5CFE0` | +| 占位符 | "搜索",`#6E88B0` | +| 右侧 🔍 / ⊗ 图标 | `14px`,`#6E88B0`,hover `#4096FF` | +| 搜索框 → 列表项纵向间距 | `--spacing-2` (8px) + 分割线 `1px solid #1A2F5A` | + +输入非空时右侧图标切换为 `⊗`(清空),点击清空输入并恢复完整列表。 + +#### 15.3.2 加载与空状态 + +异步获取选项(如远程搜索)时,列表区按以下规则渲染: + +| 状态 | 渲染 | +|------|------| +| 加载中 | 文字 "加载中…",单行居左,`14px Regular`,`#6E88B0`,行高 `32px` | +| 无匹配 | 文字 "无匹配项",同上 | +| 网络错误 | 文字 "加载失败,点击重试",`#FF7875` (ZS-001-04),可点击 | + +--- + +### 15.4 选用决策 + +| 场景 | 选择 | +|------|------| +| 候选池 ≤ 10 项,单值 | §15.1 公共规格 + 去掉复选框(基础单选) | +| 候选池 ≤ 10 项,多值 | §15.2 多项选择 | +| 候选池 > 10 项 / 不可枚举 | §15.3 多项带搜索 | +| 候选池有层级关系(如部门 / 区域) | 级联选择器:每级独立面板横向铺开,单级内部规格同 §15.1~15.3 | +| 允许用户**新增不在候选池**的项 | 在 §15.2 / §15.3 末尾启用 §15.2.4「+ 添加项」按钮 | + +--- + +### 15.5 CSS Token 参考 + +```css +:root { + /* 触发器 —— 复用 §14 搜索框 */ + --select-trigger-bg: var(--search-bg); /* #0C1836 */ + --select-trigger-border: var(--search-border); /* #1A2F5A */ + --select-trigger-placeholder: var(--search-placeholder); /* #6E88B0 */ + --select-trigger-arrow: #6E88B0; + --select-trigger-arrow-hover: #4096FF; + + /* 下拉面板 —— 复用 §14.4.4 联想 */ + --select-panel-bg: var(--suggest-bg); /* #122347 */ + --select-panel-border: #1A2F5A; + --select-item-text: var(--suggest-item-text); /* #C5CFE0 */ + --select-item-hover-bg: var(--suggest-item-hover); /* rgba(22,119,255,0.16) */ + --select-item-hover-text: var(--suggest-item-hover-text); /* #FFFFFF */ + --select-item-disabled: #4A6491; /* ZS-004-05 */ + + /* 已选 Tag */ + --select-tag-bg: #1677FF; /* ZS-002-06 */ + --select-tag-text: #FFFFFF; + --select-tag-close: rgba(255,255,255,0.72); + --select-tag-close-hover: #FFFFFF; + + /* 复选框 */ + --check-border: #6E88B0; /* ZS-004-04 */ + --check-border-hover: #4096FF; /* ZS-002-05 */ + --check-fill: #1677FF; /* ZS-002-06 */ + --check-mark: #FFFFFF; + --check-disabled-border: #4A6491; /* ZS-004-05 */ + --check-disabled-bg: rgba(22,119,255,0.04); + + /* 底部「+ 添加项」 */ + --select-add-border: #1677FF; /* ZS-002-06 */ + --select-add-text: #4096FF; /* ZS-002-05 */ + --select-add-hover-bg: rgba(22,119,255,0.16); + --select-add-hover-text: #1677FF; + --select-add-active-bg: rgba(22,119,255,0.24); + --select-add-active-text: #FFFFFF; + + /* 加载 / 错误 */ + --select-loading-text: #6E88B0; + --select-error-text: #FF7875; /* ZS-001-04 */ +} +``` + +--- + +## 16. 单选与多选(Radio / Checkbox) + +> 对应设计稿「多选框.png」。本章定义独立作为表单字段使用的**单选(Radio)**和**多选(Checkbox)**两类原子控件,包含**未选 / 已选 / 未选禁用 / 已选禁用**四种状态。§15.2.2「多项选择的复选项」中的复选框直接复用本章 §16.3 规格,本章是单一真源。 + +### 16.1 公共规格 + +| 属性 | 取值 | +|------|------| +| 控件尺寸 | `14px × 14px` | +| 控件描边宽度 | `1px` | +| 控件 → 文字间距 | `--spacing-2` (8px)(§7.1) | +| 文字字号 | `14px Regular` | +| 行高 | `20px`(点击命中区与文字垂直对齐) | +| 同一组**横向相邻**项的间距 | `--spacing-6` (24px)(§7.1) | +| 同一组**纵向相邻**项的间距 | `--spacing-3` (12px)(§7.1) | + +整行(控件 + 文字)任意位置可点击 = 等效切换该项状态。 + +--- + +### 16.2 单选(Radio) + +> 一组互斥选项中只能选中一个。视觉为圆形描边 + 内圆实心点。 + +``` +○ 未选 ● 已选 ○ 未选禁用 ● 已选禁用 + (整组降饱和) +``` + +#### 16.2.1 外观规格 + +| 状态 | 外环 | 内圆 | 文字 | +|------|------|------|------| +| 未选 | `1px solid #4096FF` (ZS-002-05) + 背景 `transparent` | — | `#C5CFE0` (ZS-004-02) | +| 未选 Hover | `1px solid #1677FF` (ZS-002-06) | — | `#FFFFFF` | +| 已选 | `1px solid #1677FF` (ZS-002-06) | 中心实心圆 `6px × 6px`,`#1677FF` | `#FFFFFF` | +| 已选 Focus | 外环外加 `0 0 0 2px rgba(22,119,255,0.16)` 焦点环 | — | `#FFFFFF` | +| 未选禁用 | `1px solid #4A6491` (ZS-004-05) + 背景 `rgba(22,119,255,0.04)` | — | `#4A6491` | +| 已选禁用 | `1px solid #4A6491` (ZS-004-05) | 中心实心圆 `6px × 6px`,`#4A6491` | `#4A6491` | + +外环与内圆均为正圆(`border-radius: 50%`)。 + +#### 16.2.2 与 label 组合使用 + +「基本使用-组合」场景:选中某 Radio 项后,**下方展开关联输入字段**(如 Antd 的 Form.Item 嵌入式组合)。 + +``` +● 已选 + + lable: [ ] + lable: [ ] +``` + +| 元素 | 规格 | +|------|------| +| Radio → 下方第一行 label 纵向间距 | `--spacing-3` (12px)(§7.1) | +| 嵌入字段左缩进 | 与 Radio 文字左边缘对齐(即外环宽 14px + 间距 8px = **22px 左缩进**) | +| 嵌入字段的 label 与 input | 沿用 §14.1 输入框规格 | +| 嵌入区分组背景 | 可选地用 `#1A2F5A` (ZS-004-07) 作浅色卡片底色,强化"属于该 Radio"的视觉归属 | + +--- + +### 16.3 多选(Checkbox) + +> 多个选项可同时选中,互不影响。视觉为方形描边 + 内填勾 ✓。 + +``` +☐ 未选 ☑ 已选 ☐ 未选禁用 ☑ 已选禁用 + (整组降饱和) +``` + +#### 16.3.1 外观规格 + +| 状态 | 边框 / 背景 | 勾 ✓ | 文字 | +|------|------------|------|------| +| 未选 | `1px solid #4096FF` (ZS-002-05) + 背景 `transparent` | — | `#C5CFE0` (ZS-004-02) | +| 未选 Hover | `1px solid #1677FF` (ZS-002-06) | — | `#FFFFFF` | +| 已选 | 实心填充 `#1677FF` (ZS-002-06) | `#FFFFFF`,描边 `2px` | `#FFFFFF` | +| 已选 Focus | 外加 `0 0 0 2px rgba(22,119,255,0.16)` 焦点环 | `#FFFFFF` | `#FFFFFF` | +| 半选 | 实心填充 `#1677FF` (ZS-002-06) | 中央横线 `8px × 2px`,`#FFFFFF` | `#FFFFFF` | +| 未选禁用 | `1px solid #4A6491` (ZS-004-05) + 背景 `rgba(22,119,255,0.04)` | — | `#4A6491` | +| 已选禁用 | 实心填充 `#4A6491` (ZS-004-05) | `#C5CFE0` 或 `rgba(255,255,255,0.4)` | `#4A6491` | + +复选框统一圆角 `2px`(§4.1)。 + +#### 16.3.2 与 label 组合使用 + +与 §16.2.2 完全一致 —— Checkbox 选中后下方展开嵌入式输入字段,缩进与间距规格相同(左缩进 22px、纵向间距 12px)。 + +``` +☑ 已选 + + lable: [ ] + lable: [ ] +``` + +--- + +### 16.4 状态对照表 + +| 状态 | Radio 外环 | Radio 内圆 | Checkbox 外框 | Checkbox 勾 | 文字 | +|------|-----------|-----------|--------------|------------|------| +| 未选 | `#4096FF` 描边 | — | `#4096FF` 描边 | — | `#C5CFE0` | +| Hover | `#1677FF` 描边 | — | `#1677FF` 描边 | — | `#FFFFFF` | +| 已选 | `#1677FF` 描边 | `#1677FF` 6px 圆 | `#1677FF` 填充 | `#FFFFFF` ✓ | `#FFFFFF` | +| 半选(仅 Checkbox) | — | — | `#1677FF` 填充 | `#FFFFFF` 横线 | `#FFFFFF` | +| Focus | + `0 0 0 2px rgba(22,119,255,0.16)` | — | + `0 0 0 2px rgba(22,119,255,0.16)` | — | `#FFFFFF` | +| 未选禁用 | `#4A6491` 描边 | — | `#4A6491` 描边 | — | `#4A6491` | +| 已选禁用 | `#4A6491` 描边 | `#4A6491` 6px 圆 | `#4A6491` 填充 | `#C5CFE0` ✓ | `#4A6491` | + +--- + +### 16.5 与 §15.2.2 的关系 + +§15.2.2「多项选择面板内的复选项」直接复用本章 §16.3 的复选框外观规格,**唯一差异**: + +- 在多项选择下拉面板里,**未选状态边框**是 `#6E88B0` (ZS-004-04,更"灰"的占位色),而非本章 §16.3 默认的 `#4096FF`(更"蓝"的强调色) +- 原因:下拉面板里项数多,所有复选框同时蓝色描边会过于喧闹;独立表单字段里数量少(通常 2–4 项),用强调色更醒目 + +如需统一也可以全面采用本章规格 —— 二选一即可,但**一个项目内部应保持一致**。 + +--- + +### 16.6 CSS Token 参考 + +```css +:root { + /* 公共 */ + --field-size: 14px; + --field-border-w: 1px; + --field-radius: 2px; /* 仅 Checkbox 用 */ + --field-text: #C5CFE0; /* ZS-004-02 */ + --field-text-hover: #FFFFFF; + --field-text-disabled: #4A6491; /* ZS-004-05 */ + --field-focus-ring: 0 0 0 2px rgba(22,119,255,0.16); + + /* Radio */ + --radio-border: #4096FF; /* ZS-002-05 */ + --radio-border-hover: #1677FF; /* ZS-002-06 */ + --radio-border-checked: #1677FF; + --radio-dot: #1677FF; /* 中心 6×6 实心 */ + --radio-border-disabled: #4A6491; /* ZS-004-05 */ + --radio-dot-disabled: #4A6491; + --radio-bg-disabled: rgba(22,119,255,0.04); + + /* Checkbox */ + --checkbox-border: #4096FF; /* ZS-002-05 */ + --checkbox-border-hover: #1677FF; /* ZS-002-06 */ + --checkbox-fill-checked: #1677FF; /* ZS-002-06 */ + --checkbox-mark: #FFFFFF; + --checkbox-border-disabled: #4A6491; /* ZS-004-05 */ + --checkbox-fill-disabled: #4A6491; + --checkbox-mark-disabled: #C5CFE0; + --checkbox-bg-disabled: rgba(22,119,255,0.04); + + /* 组合:嵌入式 label + 输入框(§16.2.2 / §16.3.2) */ + --field-group-indent: 22px; /* 14px 控件 + 8px 间距 */ + --field-group-gap-y: var(--spacing-3); /* 12px */ +} +``` + +--- + +## 17. 日期选择器(DatePicker) + +> 对应设计稿「日期时间筛选框.png」。覆盖**单日期选择**与**日期范围选择**两种场景。面板包含**左侧快捷模式**(按天 / 按周 / 按月 / 自定义)、**起止输入区**、**双月日历主体**、**底部确认按钮**四块。配色全部从 §1.1.3 渐蓝系(ZS-002)与 §1.1.5 深蓝系(ZS-004)取,与 §14 搜索框 / §15 选择器完全对齐。 + +### 17.1 触发器(Trigger) + +触发器即「2021-12-09 📅」那一行。规格与 §14.1 输入框完全一致: + +| 属性 | 取值 | 来源 | +|------|------|------| +| 高度 | `32px` | §14.1 | +| 圆角 | `2px` | §4.1 | +| 水平 padding | `--spacing-3` (12px) | §7.1 | +| 背景 | `#0C1836` (ZS-004-09) | — | +| 边框 | `1px solid #1A2F5A` (ZS-004-07) | — | +| 默认日期文字 | `14px Regular`,`#C5CFE0` (ZS-004-02);占位时 "请选择日期" 用 `#6E88B0` (ZS-004-04) | — | +| 右侧 📅 图标 | `14px`,`#6E88B0`,hover `#4096FF` | ZS-002-05 | + +边框状态机(默认 / hover / focus / 失效 / 错误)直接复用 §14.1.1。 + +**范围选择**触发器变体:宽度自动延展,内显示 `2021-12-09 至 2022-01-09`。 + +--- + +### 17.2 下拉面板(Panel) + +面板为**左 + 右**两栏组合: + +``` +┌──────────┬───────────────────────────────────────────────────────┐ +│ 按天 › │ [ 2021-12-09 ⊗ ] 至 [ 2021-12-09 ⊗ ] │ +│ 按周 › ├───────────────────────────────────────────────────────┤ +│ 按月 › │ « < 2021 年 12 月 > » « < 2022 年 1 月 > » │ +│ 自定义 │ 日 一 二 三 四 五 六 日 一 二 三 四 五 六 │ +│ │ 29 30 1 2 3 4 5 29 30 1 2 3 4 5 │ +│ │ 6 7 8 ⓿ 10 11 12 6 7 8 ⓿ 10 11 12 │ +│ │ 13 14 ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ │ +│ │ 20 ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ │ +│ │ 27 ▓▓ ▓▓ ▓▓ ▓▓ 1 2 3 4 5 6 7 8 9 │ +│ │ 3 4 5 6 7 8 9 …… │ +│ ├───────────────────────────────────────────────────────┤ +│ │ [确定] [取消] │ +└──────────┴───────────────────────────────────────────────────────┘ + ⓿ = 起止日(实心蓝圆) ▓▓ = 范围内(浅蓝底矩形) +``` + +#### 17.2.1 面板容器规格 + +| 属性 | 取值 | +|------|------| +| 背景 | `#122347` (ZS-004-08) | +| 边框 | `1px solid #1A2F5A` (ZS-004-07) | +| 阴影 | `0 4px 16px rgba(6, 14, 36, 0.6)`(§3.1.4 三级阴影) | +| 圆角 | `2px` | +| 与触发器纵向间距 | `--spacing-1` (4px) | +| 整体宽度 | 由左栏宽 `80px` + 右栏(双月并列)自适应得出 | +| 左右栏分隔线 | `1px solid #1A2F5A`(ZS-004-07) | + +--- + +### 17.3 左侧快捷模式(Sidebar) + +| 属性 | 取值 | +|------|------| +| 宽度 | `80px` | +| 项高度 | `36px` | +| 水平 padding | `--spacing-3` (12px) | +| 字号 | `14px Regular` | +| 右侧 `›` 图标(除"自定义"外) | `12px`,与文字同色 | + +#### 17.3.1 项状态 + +| 状态 | 背景 | 文字 | +|------|------|------| +| 默认 | `transparent` | `#C5CFE0` (ZS-004-02) | +| Hover | `rgba(22,119,255,0.16)` | `#FFFFFF` | +| 已选 | `#1677FF` (ZS-002-06) | `#FFFFFF` | + +#### 17.3.2 四种模式语义 + +| 模式 | 行为 | +|------|------| +| 按天 | 点击日期格 → 立即选中单日,关闭面板,触发器显示该日 | +| 按周 | hover 任意日期格 → **整周高亮**,点击 → 选中该周 | +| 按月 | 切换到月视图(日期格变成 12 个月名),点击 → 选中该月 | +| 自定义 | 默认显示双月日历,支持选中起止日做范围(详见 §17.5) | + +--- + +### 17.4 起止日期输入区(Range Inputs) + +仅在**自定义模式**下显示在右栏顶部: + +``` +[ 2021-12-09 ⊗ ] 至 [ 2021-12-09 ⊗ ] +``` + +| 元素 | 色值 / 规格 | 来源 | +|------|-------------|------| +| 输入框 | 沿用 §14.1 公共规格,宽度 `132px`,**只读**(点击日历格回填,不直接输入) | §14.1 | +| 中间 "至" 文字 | `14px Regular`,`#6E88B0` (ZS-004-04),左右 padding `--spacing-3` (12px) | — | +| 右侧 `⊗` 清空按钮 | `14px`,`#6E88B0`,hover `#4096FF` | ZS-002-05 | +| 输入区 → 日历主体纵向间距 | `--spacing-3` (12px) + 分割线 `1px solid #1A2F5A` | §6.1 | +| 整行右内 padding | `--spacing-4` (16px) | §7.1 | + +未选起 / 止时对应输入框留空显示占位 "开始日期" / "结束日期"(`#6E88B0`)。 + +--- + +### 17.5 双月日历主体(Calendar Grid) + +并列展示**当月**与**下月**两张日历,便于跨月选范围。 + +#### 17.5.1 标题栏 + +``` +« < 2021 年 12 月 > » +``` + +| 元素 | 色值 / 规格 | +|------|-------------| +| 年月文字 | `14px Medium`,`#C5CFE0` (ZS-004-02) | +| 翻页箭头 `‹ ›` | `14px`,`#6E88B0`,hover `#4096FF` | +| 翻年箭头 `« »` | `14px`,`#6E88B0`,hover `#4096FF` | +| 标题栏高度 | `36px` | +| 双月之间间距 | `--spacing-6` (24px)(§7.1) | + +行为:`«` `»` 翻年;`‹` `›` 翻月。左日历翻动时右日历同步前移一月,保持"双月连续"。 + +#### 17.5.2 星期表头 + +| 属性 | 取值 | +|------|------| +| 文字 | `日 一 二 三 四 五 六` | +| 字号 | `12px Regular` | +| 颜色 | `#6E88B0` (ZS-004-04) | +| 行高 | `28px` | + +#### 17.5.3 日期格规格 + +| 属性 | 取值 | +|------|------| +| 单元尺寸 | `28px × 28px`(点击命中区) | +| 数字字号 | `14px Regular` | +| 圆角 | `50%`(已选时实心圆) | +| 行间距 | `0`(紧凑排列) | + +#### 17.5.4 日期格状态 + +| 状态 | 背景 | 文字 | +|------|------|------| +| 当月普通 | `transparent` | `#C5CFE0` (ZS-004-02) | +| 上下月填充(如 12 月日历里显示的 11/29、11/30、1/1、1/2) | `transparent` | `#4A6491` (ZS-004-05) | +| 今天(无边框样式) | `transparent` + `1px solid #1677FF` | `#C5CFE0` | +| Hover | `rgba(22,119,255,0.16)` | `#FFFFFF` | +| 已选起止日 | 实心圆 `#1677FF` (ZS-002-06) | `#FFFFFF` | +| 范围内(起止之间) | `rgba(22,119,255,0.16)` 矩形(无圆角,左右相连成连续色带) | `#FFFFFF` | +| 范围内 + 端点 | 矩形底色 + 端点实心圆叠加(视觉上"圆形扣"在色带头尾) | `#FFFFFF` | +| 禁用(超出 min/max) | `transparent` | `#4A6491`,鼠标 `not-allowed` | + +> ⚠️ **范围色带的实现要点**:范围内日期格不能各自圆角,否则中间会出现空隙。推荐方案:每行整体作为容器,范围内的日期用同色底向左右各延伸 `4px` 模拟连续;端点处再叠加 `28×28` 实心圆。 + +--- + +### 17.6 底部「确定 / 取消」按钮 + +| 元素 | 规格 | 来源 | +|------|------|------| +| 区域高度 | `48px` | +| 顶部分割线 | `1px solid #1A2F5A` (ZS-004-07) | §6.1 | +| 右内 padding | `--spacing-4` (16px) | §7.1 | +| 按钮对齐 | 右对齐 | +| 「确定」 | 沿用 §5.2.1 主按钮(实心 `#1677FF`),尺寸"小号"`28px` | §5.2.1 | +| 「取消」 | 沿用 §5.2.2 默认按钮(描边 + 透明底),尺寸"小号"`28px` | §5.2.2 | +| 两按钮间距 | `--spacing-2` (8px) | §7.1 | + +行为: +- 「确定」 → 提交当前选择,关闭面板,触发器显示选中值 +- 「取消」 → 丢弃临时选择,关闭面板,触发器值不变 + +按 `Esc` 等价于「取消」;按 `Enter` 等价于「确定」(当选择合法时)。 + +--- + +### 17.7 选用决策与变体 + +| 场景 | 选择 | +|------|------| +| 单日期 | 触发器 + 单月日历(无左侧快捷栏,无起止输入区) | +| 日期范围(常见筛选) | 完整组合:左侧快捷栏 + 双月日历 + 起止输入区 | +| 仅按周 / 按月 / 按季度 | 左侧只保留对应模式项;右侧日历切换为相应粒度 | +| 带时间(小时 / 分) | 在 §17.4 起止输入区下方追加时分选择列(24h 滚轮,未在本节展开) | +| 多日期独立选(非连续) | 不推荐用本组件 —— 改用 §15 多项选择 + 日期值列表 | + +--- + +### 17.8 CSS Token 参考 + +```css +:root { + /* 触发器 —— 复用 §14 搜索框 */ + --date-trigger-bg: var(--search-bg); /* #0C1836 */ + --date-trigger-border: var(--search-border); /* #1A2F5A */ + --date-trigger-text: var(--search-text); /* #C5CFE0 */ + --date-trigger-placeholder: var(--search-placeholder); /* #6E88B0 */ + --date-trigger-icon: #6E88B0; + --date-trigger-icon-hover: #4096FF; /* ZS-002-05 */ + + /* 面板 —— 复用 §14.4.4 联想 */ + --date-panel-bg: var(--suggest-bg); /* #122347 */ + --date-panel-border: #1A2F5A; + --date-panel-divider: #1A2F5A; /* 左右栏 + 顶/底分割线 */ + + /* 左侧快捷栏 */ + --date-sidebar-w: 80px; + --date-sidebar-item-h: 36px; + --date-sidebar-text: #C5CFE0; /* ZS-004-02 */ + --date-sidebar-text-hover: #FFFFFF; + --date-sidebar-bg-hover: rgba(22,119,255,0.16); + --date-sidebar-bg-active: #1677FF; /* ZS-002-06 */ + --date-sidebar-text-active: #FFFFFF; + + /* 起止输入区 */ + --date-range-sep: #6E88B0; /* "至" 文字 */ + --date-range-clear: #6E88B0; + --date-range-clear-hover: #4096FF; + + /* 日历单元 */ + --date-cell-size: 28px; + --date-cell-text: #C5CFE0; /* 当月 */ + --date-cell-text-outside: #4A6491; /* 上下月填充, ZS-004-05 */ + --date-cell-text-disabled: #4A6491; + --date-cell-bg-hover: rgba(22,119,255,0.16); + --date-cell-text-hover: #FFFFFF; + --date-cell-bg-selected: #1677FF; /* 起止实心圆 */ + --date-cell-text-selected: #FFFFFF; + --date-cell-bg-in-range: rgba(22,119,255,0.16); /* 范围色带 */ + --date-cell-border-today: #1677FF; /* 今天 1px 边框 */ + + /* 标题栏 */ + --date-title-text: #C5CFE0; + --date-title-arrow: #6E88B0; + --date-title-arrow-hover: #4096FF; + --date-weekday-text: #6E88B0; /* 日一二…六 */ + + /* 底部按钮区 */ + --date-footer-h: 48px; + --date-footer-divider: #1A2F5A; +} +``` diff --git a/frontend-web/src/App.tsx b/frontend-web/src/App.tsx index 1f7554d8..155c3f64 100644 --- a/frontend-web/src/App.tsx +++ b/frontend-web/src/App.tsx @@ -20,13 +20,13 @@ import { NotFoundPage } from "./pages/NotFoundPage"; function AppProviders({ children }: { children: React.ReactNode }) { const locale = useMemo(() => detectLocale(), []); const location = useLocation(); - const forcedTheme = location.pathname === "/landing" ? "dark" : undefined; + const forcedTheme = location.pathname === "/landing" ? "light" : undefined; return ( = ({ children }) => { if (current !== 'light' && current !== 'dark') { const fallback = - legacy === 'light' || legacy === 'dark' ? legacy : 'dark'; + legacy === 'light' || legacy === 'dark' ? legacy : 'light'; setNextTheme(fallback); } else if (!current && legacy && (legacy === 'light' || legacy === 'dark')) { setNextTheme(legacy); diff --git a/frontend-web/src/lib/desensitize.ts b/frontend-web/src/lib/desensitize.ts index 963da195..3778cc98 100644 --- a/frontend-web/src/lib/desensitize.ts +++ b/frontend-web/src/lib/desensitize.ts @@ -5,6 +5,17 @@ */ export const DESENSITIZE_MAP: Record = { 态势: "TS", + 滨海作战体系: "第一张网", + 滨海作战部队: "第一张网", + 'Taiwan Air Force Flight Units': "第二张网", + 台海空军飞行部队: "第二张网", + 空军: "KJ", + 海军: "HJ", + 陆军: "LJ", + 陆战队: "LZD", + 海巡: "HX", + 憲兵: "XB", + 資通電軍: "ZTDJ", 情报: "QB", 指挥: "ZH", 信息准备: "XX准备", @@ -30,6 +41,7 @@ export const DESENSITIZE_MAP: Record = { 军: "J", 美军: "MJ", 台军: "TJ", + }; /** diff --git a/frontend-web/src/roundtable-planning/api/multi-agent.ts b/frontend-web/src/roundtable-planning/api/multi-agent.ts index 3d61d6d7..7a46211e 100644 --- a/frontend-web/src/roundtable-planning/api/multi-agent.ts +++ b/frontend-web/src/roundtable-planning/api/multi-agent.ts @@ -193,6 +193,13 @@ export interface StreamMultiAgentRequest { agentType: StreamAgentType; dAgentThreadId: ThreadIdMap; agentName: string; + /** + * 人类可读名称(中文/英文)。仅 special 走广播时使用 —— 后端把它拼进 + * 「子智能体X完成Y工作,交付内容为:...」里,这样总控 LLM 在下一轮看到自己 + * 历史里的交付摘要时能用"语义化的名字"识别,而不是 UUID 字符串(LLM 对 + * UUID 不友好,会导致重复派活)。后端缺失时 fallback 到 agentName。 + */ + displayName?: string; newMessage: string; model?: string; skillStopNames?: string[]; @@ -209,8 +216,14 @@ export interface StreamMultiAgentRequest { * Fires whenever the inferred SSE phase changes. The UI can use this to * show progressive status messages during the inevitable first-token wait. * `name` is the tool name when phase is `tool_calling` (or empty otherwise). + * `args` 是 tool_calling 阶段的工具入参(取自上游 LangGraph messages-tuple + * 帧的 tool_calls[0].args),供 UI 渲染路径 chip / 代码块等子节点。 */ - onPhaseChange?: (phase: StreamPhase, name?: string) => void; + onPhaseChange?: ( + phase: StreamPhase, + name?: string, + args?: Record, + ) => void; } export interface ClarificationPayload { @@ -327,6 +340,8 @@ export async function streamMultiAgent( agent_type: req.agentType, d_agent_thread_id: req.dAgentThreadId, agent_name: req.agentName, + // 仅 special 用到;后端缺失时 fallback 到 agent_name,旧前端也不破坏。 + display_name: req.displayName, new_message: req.newMessage, model: req.model, skill_stop_names: req.skillStopNames ?? [], @@ -366,10 +381,17 @@ export async function streamMultiAgent( // value actually changes (the reader loop sees many frames per phase). let currentPhase: StreamPhase = "connecting"; req.onPhaseChange?.("connecting"); - const setPhase = (next: StreamPhase, name?: string) => { + const setPhase = ( + next: StreamPhase, + name?: string, + args?: Record, + ) => { + // 同一个 phase + 同一个 toolName 才认为是"重复",此时不再回调上层; + // 但 args 单独是新的(如同一工具被多次 call 携带不同入参)时也不回调 —— 上层 + // appendStepIfNew 用 `phase:toolName` 做 key 去重,本来也不会因 args 变化产生新步骤。 if (next === currentPhase && !name) return; currentPhase = next; - req.onPhaseChange?.(next, name); + req.onPhaseChange?.(next, name, args); }; while (true) { @@ -426,13 +448,22 @@ export async function streamMultiAgent( setPhase("tool_result"); } else if (hasToolCall) { // Pick the most informative name we can find in either field. + // 优先用 tool_calls(完整对象,含解析好的 args),tool_call_chunks 的 args + // 通常是局部 JSON 字符串(LangGraph 流式期间逐字符发),没法直接用。 const firstCall = Array.isArray(toolCalls) && toolCalls.length > 0 ? (toolCalls[0] as Record) : Array.isArray(toolCallChunks) && toolCallChunks.length > 0 ? (toolCallChunks[0] as Record) : null; const toolName = firstCall && typeof firstCall.name === "string" ? firstCall.name : ""; - setPhase("tool_calling", toolName); + // 仅当来自 tool_calls(完整对象)时透传 args;tool_call_chunks 的 args 是 + // string 拼接中,不传上去给 UI,等下一个完整 chunk 再带。 + const fromCompleteToolCalls = Array.isArray(toolCalls) && toolCalls.length > 0; + const args = + fromCompleteToolCalls && firstCall && typeof firstCall.args === "object" && firstCall.args !== null + ? (firstCall.args as Record) + : undefined; + setPhase("tool_calling", toolName, args); } else if ( (chunkType === "ai" || chunkType === "aimessagechunk" || chunkType === "aimessage") && ((typeof content === "string" && content.length > 0) || (Array.isArray(content) && content.length > 0)) diff --git a/frontend-web/src/roundtable-planning/components/RecommendAgentsDialog.tsx b/frontend-web/src/roundtable-planning/components/RecommendAgentsDialog.tsx index a096824a..e1ac8dc4 100644 --- a/frontend-web/src/roundtable-planning/components/RecommendAgentsDialog.tsx +++ b/frontend-web/src/roundtable-planning/components/RecommendAgentsDialog.tsx @@ -1,7 +1,16 @@ "use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { CheckIcon, Loader2Icon, RefreshCwIcon, SparklesIcon } from "lucide-react"; +import { + CheckIcon, + ChevronDownIcon, + ChevronUpIcon, + DotIcon, + LightbulbIcon, + Loader2Icon, + RefreshCwIcon, + SparklesIcon, +} from "lucide-react"; import { Dialog, @@ -12,7 +21,15 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; +import { + ChainOfThought, + ChainOfThoughtContent, + ChainOfThoughtHeader, + ChainOfThoughtStep, +} from "@/components/ai-elements/chain-of-thought"; +import { MarkdownContent } from "@/components/workspace/messages/markdown-content"; import { listAgents, type Agent } from "@/core/agents"; +import { useRehypeSplitWordsIntoSpans } from "@/core/rehype"; import { cn } from "@/lib/utils"; import type { IntentSummary } from "../api/intent"; @@ -43,7 +60,7 @@ export interface RecommendAgentsDialogProps { model?: string; onCancel: () => void; /** Called with the user's final selection of agent ids (in display order). */ - onConfirm: (agentIds: string[]) => void; + onConfirm: (agents: Array<{ agent_id: string; name: string }>) => void; } const MIN_SELECTED = 2; @@ -71,12 +88,21 @@ type DialogPhase = | "fallback" | "error"; +interface RecommendStep { + id: string; + key: string; + label: string; + ts: string; +} + interface InternalState { phase: DialogPhase; /** Streaming + final rationale text from the recommender. */ rationale: string; /** Streaming SSE phase from the recommender helper. */ streamPhase: RecommendStreamPhase | null; + /** 推荐流程阶段时间线 —— 调度 / 已连接 / 流式生成 …… 用 ChainOfThought 折叠展示。 */ + steps: RecommendStep[]; /** Pre-filtered candidate pool fetched from `/api/agents`. */ candidates: RecommendCandidate[]; /** Reason-by-agent_id, only for picks returned by the recommender. */ @@ -93,6 +119,7 @@ const INITIAL_STATE: InternalState = { phase: "idle", rationale: "", streamPhase: null, + steps: [], candidates: [], reasons: {}, recommendedIds: [], @@ -100,6 +127,24 @@ const INITIAL_STATE: InternalState = { errorMessage: null, }; +/** 推荐流程的 phase → 中文标签;与 RoundtablePlanningPage::phaseLabel 风格保持一致。 */ +function recommendPhaseLabel(phase: RecommendStreamPhase): string { + switch (phase) { + case "connecting": + return "调度推荐智能体"; + case "connected": + return "已连接,等待响应"; + case "streaming": + return "正在生成推荐"; + default: + return phase; + } +} + +function nowTimeStr(): string { + return new Date().toLocaleTimeString("zh-CN", { hour12: false }); +} + /** * Resolve which candidate ids should be checked by default given a set of * recommended ids. The result preserves recommendation order and silently @@ -193,6 +238,10 @@ export function RecommendAgentsDialog({ candidates, rationale: "", streamPhase: "connecting", + // 推荐开始,清空上一轮的步骤时间线并打第一条点。 + steps: [ + { id: "0", key: "connecting", label: recommendPhaseLabel("connecting"), ts: nowTimeStr() }, + ], })); try { @@ -227,7 +276,24 @@ export function RecommendAgentsDialog({ }, onPhaseChange: (phase) => { if (myGen !== generationRef.current) return; - setState((prev) => ({ ...prev, streamPhase: phase })); + setState((prev) => { + const key = phase; + const last = prev.steps[prev.steps.length - 1]; + // 连续重复阶段不重复 push,只在阶段切换时打点。 + const steps = + last && last.key === key + ? prev.steps + : [ + ...prev.steps, + { + id: String(prev.steps.length), + key, + label: recommendPhaseLabel(phase), + ts: nowTimeStr(), + }, + ]; + return { ...prev, streamPhase: phase, steps }; + }); }, }); if (myGen !== generationRef.current) return; @@ -346,12 +412,15 @@ export function RecommendAgentsDialog({ // Order: recommended-and-selected first (in recommender order), then // user-added picks (in pool order). Keeps the leader's "可调度智能体" // list visually stable. - const ordered: string[] = []; + // + // 传完整对象(含 name)而不是只传 agent_id —— 非内置 agent 的 id 是 UUID, + // 上游若只拿 id,在左侧席位列表就会显示 UUID 而非中文名。 + const ordered: Array<{ agent_id: string; name: string }> = []; for (const c of recommendedCards) { - if (selectedIds.has(c.agent_id)) ordered.push(c.agent_id); + if (selectedIds.has(c.agent_id)) ordered.push({ agent_id: c.agent_id, name: c.name }); } for (const c of otherCards) { - if (selectedIds.has(c.agent_id)) ordered.push(c.agent_id); + if (selectedIds.has(c.agent_id)) ordered.push({ agent_id: c.agent_id, name: c.name }); } onConfirm(ordered); }, [canConfirm, recommendedCards, otherCards, selectedIds, onConfirm]); @@ -390,7 +459,7 @@ export function RecommendAgentsDialog({ ) : state.phase === "recommending" ? ( <> {recommendedCards.length > 0 ? ( -
+
{recommendedCards.map((c) => ( 0 && ( -
+
{recommendedCards.map((c) => ( { + if (!userTouchedRef.current) setExpanded(isStreaming); + }, [isStreaming]); + if (state.phase === "loading_agents") { return (
@@ -498,52 +585,178 @@ function RationaleBlock({ state }: { state: InternalState }) {
); } - if (state.phase === "recommending") { - const label = - state.streamPhase === "connecting" + + if (state.phase === "error") { + return ( +
+ 推荐失败,请使用下方的"重新推荐"重试,或取消后再次进入。 +
+ ); + } + + const showRationaleStage = + state.phase === "recommending" || + state.phase === "ready" || + state.phase === "fallback"; + if (!showRationaleStage) { + return null; + } + + const streamingLabel = + state.phase === "recommending" + ? state.streamPhase === "connecting" ? "调度推荐智能体..." : state.streamPhase === "connected" ? "已连接,等待响应..." - : "正在生成推荐..."; - return ( -
+ : "正在生成推荐..." + : null; + + const hasRationale = !!state.rationale; + + return ( +
+ {streamingLabel && (
- {label} + {streamingLabel}
- {state.rationale ? ( -

- {state.rationale} -

- ) : ( -

正在分析任务与候选智能体...

- )} -
- ); - } - if (state.phase === "ready" || state.phase === "fallback") { - if (!state.rationale) { - return
推荐完成。
; - } - return ( -

- {state.rationale} -

- ); - } - if (state.phase === "error") { - return
推荐失败,请使用下方的"重新推荐"重试,或取消后再次进入。
; - } - return null; + )} + + {/* 推荐流程阶段时间线(视觉与 chats/new 消息列表的 ChainOfThought 对齐): + **始终默认收起**,收起 header 上显示最新一步作为预览。 + streaming 用 LightbulbIcon,其它阶段用 DotIcon —— 推荐流程没有 tool_calls, + 所以不需要 chats 那套完整的工具图标分支。 */} + {state.steps.length > 0 && (() => { + const lastStep = state.steps[state.steps.length - 1]; + const LastIcon = lastStep?.key === "streaming" ? LightbulbIcon : DotIcon; + return ( + + + ) : undefined + } + > + {stepsOpen + ? "步骤" + : isStreaming + ? (lastStep?.label ?? `查看 ${state.steps.length} 个步骤`) + : "步骤列表"} + + + {state.steps.map((s, i) => ( + + {s.label} + {s.ts} + + } + status={ + isStreaming && i === state.steps.length - 1 ? "active" : "complete" + } + /> + ))} + + + ); + })()} + + {!hasRationale && isStreaming && ( +

正在分析任务与候选智能体...

+ )} + + {!hasRationale && !isStreaming && ( +
推荐完成。
+ )} + + {hasRationale && ( + <> + {/* 按钮在上、内容在下 —— 展开/收起按钮始终留在原位,避免点开时 + 内容把按钮挤到屏幕外,用户需要滚回去才能再点收起。 + 收起时完全不渲染内容区,只保留按钮(图标 + "思考过程")。 + streamingLabel 已经在上方独立显示阶段进度,这里不重复 spinner。 */} + + {expanded && ( +
+ +
+ )} + + )} +
+ ); } -function Section({ title, children }: { title: string; children: React.ReactNode }) { +function Section({ + title, + children, + collapsible = false, + defaultOpen = true, +}: { + title: string; + children: React.ReactNode; + /** True 时 header 可点击折叠/展开;false 保持原行为(始终展开,不可点击)。 */ + collapsible?: boolean; + /** 仅当 collapsible=true 时生效。 */ + defaultOpen?: boolean; +}) { + const [open, setOpen] = useState(defaultOpen); return (
-
- {title} -
-
{children}
+ {collapsible ? ( + + ) : ( +
+ {title} +
+ )} + {/* flex flex-col + w-full + items-stretch:替代原 `grid gap-2`, + 强制所有 AgentCard 满父宽。grid 在某些浏览器对
); } @@ -571,7 +784,9 @@ function AgentCard({ type="button" onClick={onToggle} className={cn( - "text-left rounded-md border p-3 transition-colors flex gap-3 items-start", + // w-full:`