外观
AI 会话大纲设计方案
description: 会话大纲的条目派生模型、左侧刻度导轨 UI、滚动定位与高亮联动设计
created: 2026-10-13 19:57:29
updated: 2026-10-13 21:28:24
author:
1. 设计目标与硬约束
范围:当前 scope 对话的大纲导航功能——大纲条目派生、左侧刻度导轨 UI、点击定位滚动与目标闪烁反馈、当前条目高亮(scroll-spy)、导轨 margin 定位与显示门槛。 边界:不覆盖多会话列表管理(一个 scope 一条会话的现状不变);不覆盖 AI 智能标题生成(决策见 §6.2)与大纲导出;不改动消息持久化结构与 SSE 协议。 关联代码:
frontend/src/components/AiChatPanel.vue、frontend/src/components/RightPanel.vue、frontend/src/lib/ai.ts、frontend/src/style.css
| 目标 | 量化口径 |
|---|---|
| 零模型开销 | 大纲派生不发起任何模型调用,单条派生为纯字符串处理,100 条会话重派生 < 1ms |
| 零持久化变更 | AiChatRecord 不新增字段;大纲完全由消息列表 computed 推导,重载后内容一致 |
| 布局零侵入 | 消息区/输入区布局与大纲引入前完全一致(760px 限宽居中、padding: 10px 12px);导轨置于消息区左侧 margin 空隙,不占用消息区任何空间 |
| 按需展示 | 导轨仅在对话轮数 > 3(user 消息 ≥ 4 条)且面板宽度 ≥ 828px(左侧 margin 放得下导轨)时显示,否则恒隐藏,无手动开关 |
| 定位准确 | 点击刻度后目标锚点距消息区顶部 40px(scroll-margin),偏差 < 1 行,并伴随 1.1s 闪烁反馈 |
| 不劫持视口 | 刻度定位后与既有流式/晋升滚动守卫(STICK_THRESHOLD_PX = 80)天然兼容,无需新增守卫 |
| 长会话可用 | 导轨最多 10 个刻度可见(max-height 215px),超出内部滚动并以上下淡出遮罩提示 |
| 滚动低开销 | scroll-spy 帧回调零布局读取:锚点 offsetTop 变化时采样缓存,激活判定为有序数组二分查找 O(log n),100 条锚点单次查找 < 0.01ms |
2. 必须遵守的既有项目约束
- 渲染层必须可由消息列表纯推导,重载后结果一致(ai-message-render-v2-design.md §2);大纲作为消息列表的派生视图,同样不得持有独立存储;
- 滚动行为遵守「用户上翻则暂停自动滚动」守卫,用户主动动作(发送、切会话)不受守卫限制;点击刻度定位属用户主动动作;
- 删除用户消息时整组删除(既有行为),大纲派生与显示门槛必须自动跟随,无残留刻度;
- 颜色一律使用全局 CSS 变量(
--border/--muted/--muted-2/--panel-2/--panel/--text),当前刻度使用纯主题色rgb(var(--accent-rgb))(不用渐变),跟随applyAccent主题动态注入,不写死色值; - AI 面板为暗色主题,参考稿(浅色)的所有色值只取角色不取数值。
3. 现状盘点与差距
| 位置 | 现状 | 差距 |
|---|---|---|
AiChatPanel.vue 消息区 | 长对话(多轮 ReAct、多步 Plan)只能在消息流中线性翻找,无结构视图;.chat-scroll 现状 padding: 10px 12px、内容限宽 760px,宽面板下两侧 margin 空闲未利用 | 用户无法一览"本次对话问了什么、读到哪里",也无法快速跳回历史问题;刻度导轨可置于左侧 margin |
chatTitle()(L400) | 已从首条 user 消息截断生成标题并持久化到 AiChatRecord.title | 仅取首条、仅用于落盘记录,UI 无任何展示;逐条派生逻辑可复用其截断/回退思路 |
| 消息 DOM | user bubble 与回复组根元素无锚点标识 | 无法按消息定位滚动,需补 data-mid 锚点 |
onChatScroll / scrolledUp | 已有滚动事件与 80px 上翻守卫 | 可承载 scroll-spy 高亮与定位后的守卫协同,无需新增滚动机制 |
结论:本设计补齐四块——逐组大纲条目派生、消息 DOM 锚点、左侧 margin 刻度导轨定位、导轨交互(门槛显示 + 定位 + 高亮 + 悬浮标签)。
4. 目标与非目标
目标
- 每条 user 消息(回复组锚点)派生一个大纲条目:标题 + 组执行状态;
- 消息区/输入区布局保持原样(零侵入),刻度导轨置于消息区左侧 margin 空隙(右缘距消息区左缘 12px);
- 对话轮数 > 3(user 消息 ≥ 4 条)且面板宽度 ≥ 828px 时导轨自动显示,否则恒隐藏;
- 刻度三级状态(默认 / 已读 / 当前)以宽度与颜色递进区分,hover 刻度显示悬浮标题标签;
- 点击刻度平滑滚动定位到对应消息,目标消息闪烁反馈;
- 滚动消息区时 scroll-spy 实时更新当前刻度,当前刻度在导轨可滚区域内保持可见。
非目标
- 不做手动开关按钮(显示完全由轮数门槛驱动);
- 不做多会话列表 / 会话切换(一 scope 一会话现状不变);
- 不做 AI 智能标题(策略决策见 §6.2);
- 不做大纲导出为 Markdown/文档;
- 不承载步骤级导航(刻度扁平,计划进度由计划面板承担);
- 不持久化大纲任何状态(导轨显示为纯派生结果)。
5. 总体架构
text
┌─ RightPanel panel-head ───────────────────────┐
│ AI 助手 [scope] [清空] [⛶] │
├─ .ai-panel ───────────────────────────────────┤
│ ┌─ .chat-wrap(position: relative,全宽)───┐ │
│ │ =│◄─ 刻度导轨:消息区左侧 margin 空隙内 │ │
│ │ -│ (右缘距消息区左缘 12px) │ │
│ │ ┌─ .chat-scroll(760px 限宽居中)─────┐ │ │
│ │ │ (消息列表,布局与大纲引入前一致, │ │ │
│ │ -│ padding 10px 12px) -│ │ │
│ │ │ - 默认 16×2px / = 当前 22×3px 主题色│ │ │
│ │ └───────────────────────────────────┘ │ │
│ │ (轮数 > 3 且面板 ≥ 828px 自动出现) │ │
│ └───────────────────────────────────────────┘ │
│ (输入区 / 计划面板 / 授权弹窗,不变化) │
└───────────────────────────────────────────────┘
数据流:messages(持久化源)─computed→ outlineEntries ─→ 刻度渲染(length > 3 时)
└─→ scroll-spy activeId(显示期间随滚动事件 rAF 节流更新)- 大纲是消息列表的只读派生视图:无独立数据源,无持久化字段,无手动开关;
- 导轨锚定消息滚动区:
.chat-scroll外包一层全宽.chat-wrap(position: relative),导轨为其 absolute 子元素,不进入滚动内容; - 布局零侵入:消息区布局(760px 限宽居中、
padding: 10px 12px)与大纲引入前完全一致,导轨置于消息区左侧 margin 空隙(left: calc(50% - 414px)),显示/隐藏均不改变消息区任何布局属性; - 锚点:user bubble 渲染
data-mid,定位与 scroll-spy 共用。
6. 详细设计
6.1 条目派生模型
ts
interface OutlineEntry {
/** 锚点消息 id = user 消息 id */
id: string
/** 派生标题(已扁平化、长度兜底) */
title: string
/** 组执行状态(悬浮标签内状态点用色) */
state: 'waiting' | 'running' | 'done' | 'error' | 'stopped'
}outlineEntries = computed(...),遍历消息列表按 [groupStart, groupEnd] 分组推导,与渲染层 v2 使用同一分组口径。
6.2 标题派生规则
对每条 user 消息:
- 剔除 content 尾部内联
<file name="…">…</file>附件块(正则/<file name="[^"]*">[\s\S]*?<\/file>/g,与多模态发送装配格式对应); - 全部连续空白字符(含换行)折叠为单个空格并
trim; - 空串回退:有
images→「图片」;有files→ 首个文件名;否则「(空消息)」; - 长度兜底:截断至 60 字符(悬浮标签内再行视觉截断;60 取值为悬浮标签 max-width 240px 约 18 字的 3 倍,避免超长字符串进入 DOM)。
策略决策:标题采用本地截断,不引入 AI 摘要。刻度只需"定位锚点"而非阅读级标题,运维场景 user 消息多为短问句,截断辨识度已充足;AI 摘要需扩展
AiChatRecord缓存字段、定义失效规则、承担 token 成本且生成前仍需截断兜底,性价比不足。
6.3 组执行状态推导
按 [groupStart, groupEnd] 区间内 assistant 消息推导:
| 条件(按优先级) | state | 状态点色 |
|---|---|---|
任一 status === 'streaming' | running | --rhost-status-processing(脉冲动画,复用 plan-badge) |
末条 status === 'error' | error | --rhost-status-error |
末条 status === 'stopped' | stopped | --rhost-status-warning |
| 存在 assistant 消息 | done | --rhost-status-success |
| 无 assistant 消息(刚发送/异常残留) | waiting | --rhost-status-idle |
状态点仅出现在悬浮标签内(见 §6.5),刻度本身不承载执行状态,保持刻度视觉极简。
6.4 布局回退与显示门槛
- 布局零侵入:
.chat-scroll保持大纲引入前的原始布局——padding: 10px 12px、max-width: 760px居中(.chat-wrap为全宽相对定位容器,仅作导轨定位基准)。取舍记录:初版曾将消息区左右 padding 扩为 38px(后收窄 22px)为导轨腾位,因挤压对话区且与输入框宽度错位被回退;现导轨完全不占消息区空间,置于消息区左侧 margin 空隙(left: calc(50% - 414px),414px = 760/2 + 22 导轨宽 + 12 间距); - 显示门槛:
outlineEntries.length > 3(即第 4 条 user 消息出现)且面板宽度 ≥ 828px(RAIL_MIN_WIDTH_PX= 760 + 2×34,ResizeObserver 监听.chat-wrap维护)才渲染导轨,否则恒不渲染。轮数阈值 3 的取值理由:≤3 轮对话的消息量通常一屏内可见,线性翻找成本极低,导轨导航价值小,常驻反而增加视觉噪音;≥4 轮开始出现明显回翻需求。宽度阈值 828px 的取值理由:左侧 margin 不足 34px 时导轨会遮挡消息正文,整体隐藏更优——普通窄面板下面板宽度不足,导轨仅在专注模式/宽面板下出现; - 门槛联动:删除消息导致轮数回落至 ≤3、或面板拖窄越过宽度阈值时导轨自动消失;条件恢复时自动重现。纯 computed 推导,无中间态、无动画(刻度细线淡入 160ms 可选,不允许位移动画)。
6.5 刻度导轨 UI 与交互
布局参考上传附件(deepseek_html_20261013_fea5ed.html)的左侧刻度目录,色值与令牌按本项目暗色主题重映射。
导轨容器:
.rail-shellposition: absolute; left: calc(50% - 414px); top: 50%; transform: translateY(-50%),宽 22px,置于消息区左侧 margin 空隙(右缘距消息区左缘 12px),不遮挡消息正文;容量:刻度行高 14px、间距 7px,
max-height: 215px(= 10×14 + 9×7 + 12,最多 10 个刻度可见;10 为参考稿取值,兼顾竖向占用与一屏浏览);超出内部滚动,隐藏滚动条。行高 14px 大于横线自身 2~3px,为的是扩大 hover/focus 热区——细线难以命中,热区不足会让导航功能形同虚设;嵌套滚动:导轨(
overflow-y: auto)与消息区按浏览器默认嵌套滚动链处理,不做干预——导轨可滚且未到端时 wheel 滚导轨,到端或不可滚时冒泡到消息区;上下淡出遮罩:导轨可滚且未贴顶/贴底时,对应端显示 16px 高渐变遮罩(
var(--panel)→ 透明,与右面板背景同色避免色差接缝),提示还能继续滚;状态类can-scroll / at-top / at-bottom随导轨滚动同步;刻度(tick):按钮元素,
::before为横线,过渡width/height/background .26s cubic-bezier(.4,0,.2,1)(参考稿取值,标准 ease 曲线,宽度过渡不突兀)。阅读进度类名取past/active,不与组执行状态'done'复用命名(一个是视口阅读进度,一个是执行结果,语义不同):状态 尺寸 颜色 默认 16×2px --borderhover / focus-visible 20×2px --muted已读(past) 16×2px --muted-2当前(active) 22×3px rgb(var(--accent-rgb))(纯主题色,不用渐变)当前 + hover 22×3px 同 active 悬浮标签(rail-tip):hover / focus 刻度时显示,fixed 定位(左 = 导轨右缘 + 14px,垂直与刻度对中
translateY(-50%),滑入 5px / 0.16s);内容 = 组状态点 + 标题文本,不含序号;样式:var(--panel-2)背景、1pxvar(--border)、10px 圆角、var(--text)12.5px、max-width: 240px单行 ellipsis、阴影0 10px 28px rgba(0,0,0,.35)(与 perm-menu 同级);pointer-events: none。刻度本身无文字,悬浮标签是标题的唯一载体,属必要信息展示——与已移除的"文字列表行冗余 tooltip"性质不同;导轨内部滚动时立即收起标签,避免错位。
6.6 定位滚动与闪烁反馈
- user bubble 渲染
:data-mid="m.id"作为锚点; - 锚点元素样式加
scroll-margin-top: 40px(参考稿取值:定位后消息不贴消息区顶,保留一行上下文呼吸位); - 点击刻度:
scrollEl.querySelector('[data-mid="…"]')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); - 定位同时给目标 user bubble 加
flash类:1.1sbox-shadow扩散淡出(0 0 0 4px rgba(var(--accent-rgb), .20)→ 全透明),强制重启动画后定时移除——给用户明确的跳转落点反馈(参考稿交互,色值换为主题强调色); - 锚点元素缺失(极端时序)→ 静默不滚动,不报错。
6.7 scroll-spy 高亮
- 仅导轨显示期间启用:
onChatScroll中追加分支,rAF 节流(参考稿做法:滚动事件只置位,帧回调内统一计算); - 锚点位置缓存(性能关键):锚点
offsetTop只在布局变化时改变,滚动本身不改变——因此在采样时机一次性读取并缓存为有序数组(Array<{ id, top }>,按 top 升序);帧回调不做任何布局读取,仅对缓存数组二分查找(O(log n)),避免高频滚动下逐条目getBoundingClientRect()批量读取引发的布局开销; - 采样/重采样时机:导轨挂载(含第 4 轮出现、切换 scope 后重载恢复)即完成首次采样与激活计算,不等首次滚动事件;此后仅在四种情况重采样——
outlineEntries结构变化(锚点增删、编辑重发)、chat-scroll宽度变化(换行导致高度重排,ResizeObserver 监听,仅宽度变化时触发)、窗口 resize、折叠区/思考条目展开收起(含任务落定折叠区自动收起、用户手动切换,均改变组总高度)。流式 token 追加不触发重采样:token 只追加到当前流式轮(最后一组),其前所有锚点位置不受影响; - 激活线 =
chat-scroll可视高度 × 0.34(参考稿取值:激活点偏视口上 1/3,符合阅读视线——当前话题通常在视口上部);取缓存数组中最后一个top <= scrollTop + 激活线的条目为active,其之前全部条目标记past(已读); - 贴底兜底:
scrollTop + clientHeight >= scrollHeight - 40时强制末条 active(参考稿取值 40px;末条锚点后还有长回复时,不兜底会出现末条永远点不亮); - 当前刻度保持可见:active 变化后校正导轨内部滚动,使当前刻度落在可滚区域内(上下各留 6px 余量,参考稿
ensureVisible)。
6.8 与滚动守卫的协同
刻度定位是用户主动动作,但定位方向是向上(回到历史),与既有守卫的关系:
| 定位后视口状态 | 守卫行为 |
|---|---|
| 目标为历史条目(距底 > 80px) | onChatScroll 自然将 scrolledUp 置真,后续流式 token / 结果晋升不自动回底,用户可停留阅读 |
| 目标为最新条目(距底 ≤ 80px) | scrolledUp 保持假,流式跟随与晋升补偿正常 |
无需新增守卫逻辑,与「消息晋升滚动守卫」约束完全兼容。
7. 安全与降级
- 纯前端渲染层改动,无新增网络、文件、命令执行路径,无安全面变化;大纲内容仅来自本机已持久化的消息列表;
- 降级路径:锚点元素缺失 → 不滚动;
outlineEntries.length ≤ 3(含异常旧数据无 user 消息)→ 导轨不渲染;锚点采样异常(元素未挂载/读取失败)→ 跳过该次采样并清空缓存,scroll-spy 退化为不高亮,不影响定位与派生主流程; - 派生为纯函数,任何消息异常(空 content、纯附件)均有回退标题,不抛错。
8. 测试与验证
| 项 | 验证方式 |
|---|---|
| 派生正确性 | 多轮对话(含 Plan、纯图片消息、含 <file> 附件消息)下刻度数 = user 消息数,悬浮标签标题/回退/状态点正确且无序号 |
| 显示门槛 | 1~3 轮导轨恒不渲染;发出第 4 条 user 消息后导轨出现;删除消息回落至 3 轮导轨消失;面板宽度拖过 828px 阈值时导轨随之隐藏/重现;全程消息区布局零位移 |
| 布局零侵入 | 默认窄面板(280px)与专注模式下消息区/输入区布局与大纲引入前完全一致(760px 限宽居中、padding 10px 12px);宽面板下导轨位于消息区左侧 margin(间距 12px),不遮挡消息正文 |
| 删除联动 | 删除中间 user 消息(整组删除)后导轨立即重派生,无残留刻度 |
| 刻度状态 | 默认/hover/past/active 四态宽度与颜色符合 §6.5 表;active 为 22×3px 纯主题色 |
| 悬浮标签 | hover 各刻度标签位置对中不抖动;导轨内滚动标签立即收起;窄面板下不越界 |
| 定位与闪烁 | 点击刻度后锚点距消息区顶 40px,目标 bubble 闪烁一次 |
| scroll-spy | 滚动消息区,激活刻度按 0.34 激活线推进,已读刻度同步迁移;贴底时末条强制激活;第 4 轮发出、导轨首次挂载瞬间即有正确高亮(不等首次滚动);流式输出期间锚点缓存不重采样、高亮保持正确 |
| 容量 | 构造 12+ 条会话:导轨仅 10 个刻度可见,内部滚动正常,两端淡出遮罩按需显示,active 刻度自动保持可见 |
| 守卫协同 | 定位到历史条目后发起流式回复,视口不被劫持;定位到最新条目后流式跟随正常 |
| 回归 | npm run build(frontend/)通过;清空对话、重新生成、编辑重发、授权弹窗、计划面板交互不回归 |
9. 未决问题
- 无