Skip to content

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 无任何展示;逐条派生逻辑可复用其截断/回退思路
消息 DOMuser bubble 与回复组根元素无锚点标识无法按消息定位滚动,需补 data-mid 锚点
onChatScroll / scrolledUp已有滚动事件与 80px 上翻守卫可承载 scroll-spy 高亮与定位后的守卫协同,无需新增滚动机制

结论:本设计补齐四块——逐组大纲条目派生、消息 DOM 锚点、左侧 margin 刻度导轨定位、导轨交互(门槛显示 + 定位 + 高亮 + 悬浮标签)。

4. 目标与非目标 ​

目标 ​

  1. 每条 user 消息(回复组锚点)派生一个大纲条目:标题 + 组执行状态;
  2. 消息区/输入区布局保持原样(零侵入),刻度导轨置于消息区左侧 margin 空隙(右缘距消息区左缘 12px);
  3. 对话轮数 > 3(user 消息 ≥ 4 条)且面板宽度 ≥ 828px 时导轨自动显示,否则恒隐藏;
  4. 刻度三级状态(默认 / 已读 / 当前)以宽度与颜色递进区分,hover 刻度显示悬浮标题标签;
  5. 点击刻度平滑滚动定位到对应消息,目标消息闪烁反馈;
  6. 滚动消息区时 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 消息:

  1. 剔除 content 尾部内联 <file name="…">…</file> 附件块(正则 /<file name="[^"]*">[\s\S]*?<\/file>/g,与多模态发送装配格式对应);
  2. 全部连续空白字符(含换行)折叠为单个空格并 trim;
  3. 空串回退:有 images → 「图片」;有 files → 首个文件名;否则 「(空消息)」;
  4. 长度兜底:截断至 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-shell position: 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--border
    hover / focus-visible20×2px--muted
    已读(past)16×2px--muted-2
    当前(active)22×3pxrgb(var(--accent-rgb))(纯主题色,不用渐变)
    当前 + hover22×3px同 active
  • 悬浮标签(rail-tip):hover / focus 刻度时显示,fixed 定位(左 = 导轨右缘 + 14px,垂直与刻度对中 translateY(-50%),滑入 5px / 0.16s);内容 = 组状态点 + 标题文本,不含序号;样式:var(--panel-2) 背景、1px var(--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 定位滚动与闪烁反馈 ​

  1. user bubble 渲染 :data-mid="m.id" 作为锚点;
  2. 锚点元素样式加 scroll-margin-top: 40px(参考稿取值:定位后消息不贴消息区顶,保留一行上下文呼吸位);
  3. 点击刻度:scrollEl.querySelector('[data-mid="…"]')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
  4. 定位同时给目标 user bubble 加 flash 类:1.1s box-shadow 扩散淡出(0 0 0 4px rgba(var(--accent-rgb), .20) → 全透明),强制重启动画后定时移除——给用户明确的跳转落点反馈(参考稿交互,色值换为主题强调色);
  5. 锚点元素缺失(极端时序)→ 静默不滚动,不报错。

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. 未决问题 ​

  • 无