# M20：AI 简历优化从“按钮 loading”到“可演示闭环”教程大纲

## 一、为什么这次不急着上 SSE

- 当前问题不是“技术上能不能流式”，而是“用户是否感知到系统还在工作”
- 在同步接口阶段，先补齐 Rich Loading、取消、长等待提示，性价比更高
- 把等待体验做对后，再升级协议到 SSE，迁移路径更清楚

## 二、先拆页面职责，而不是堆更多组件

- `/dashboard/ai` 只做输入、触发与缓存报告回看
- `/dashboard/ai/resume-optimization/results/[resultId]` 专注结果承接
- 为什么独立结果页更利于：
  - 页面信息分层
  - 刷新后回看
  - 后续加历史结果
  - 后续加编辑器联动

## 三、服务端结果缓存为什么值得先做

- 纯前端状态的问题：
  - 刷新丢失
  - 路由切换后难回看
  - apply patch 暴露过重
- `resultId + cache` 的收益：
  - 展示 DTO 可以做轻量 locale 化
  - 真正 apply patch 留在服务端，前端不需要背全量双语数据
  - 后续能自然扩展到历史结果列表和 TTL 管理

## 四、DTO 分层：展示给用户的，与真正 apply 的，不应该是一回事

- 展示 DTO：
  - `resultId`
  - `summary`
  - `focusAreas`
  - `changedModules`
  - `moduleDiffs`
  - `providerSummary`
  - `createdAt`
- 服务端缓存内部：
  - `patch`
  - `draftUpdatedAt`
  - locale detail
- apply 接口为什么切到 `resultId + modules`

## 五、Rich Loading 设计思路

- 最小可感知元素：
  - 主标题
  - 已等待时长
  - 当前阶段说明
  - 长等待提示
  - 超长等待提示
  - 可取消
- 为什么用“固定阶段模拟”也足够有效：
  - 读取当前草稿
  - 调用 AI 生成建议
  - 整理结构化 diff
  - 准备结果页
- 模拟进度的经验规则：
  - 不要在前几秒快速涨到接近完成
  - `0s → 30s` 更适合用 `5s` 锚点逐步加速到 `70%`
  - 30 秒后再按固定步长缓慢增加
  - 未完成前最多只到 `95%`
  - 真正完成时再把进度补到 `100%`，并停留约 `1s`

## 六、模块级操作设计：先做最小可信版本

- 默认全选受影响模块
- `只应用当前建议`
- `重置回当前草稿` 第一版只代表：
  - 从待应用集合移除
  - 不回滚数据库
- 为什么这一语义在当前阶段最稳妥

## 七、测试怎么覆盖这种 AI 交互页面

- 不测模型质量本身，测交互闭环：
  - pending state
  - cancel
  - result page navigation
  - refresh-safe fetch by `resultId`
  - module reset / apply
- fake timers 在 Rich Loading 测试里的注意点

## 八、后续如何平滑演进到 M21 / SSE

- M21：编辑器 section 联动、逐条 accept / reject
- SSE：把固定阶段文案替换为真实服务端阶段事件
- 历史结果：把内存缓存升级为可持久化结果表
- undo：引入草稿版本或操作日志

## 九、默认 JD 模板与最近记录为什么值得单独做

- 不是每个 AI 页面都要从空白 textarea 开始
- 对于“重复粘贴成本高”的输入，默认模板可以显著降低启动成本
- 本地历史的职责是：
  - 快速回看最近结果
  - 快速回填历史 JD / 指令
  - 不替代服务端 `resultId` 结果缓存
- 一个可解释的三层状态结构：
  - 当前输入：`localStorage`
  - 最近记录摘要：`localStorage`
  - 真正结果详情与 apply：服务端 `resultId + cache`

## 十、为什么后来又拆出“优化记录”页

- 当工作台同时承载输入、缓存回看、最近记录时，主路径会失焦
- 把 `/dashboard/ai` 收口成输入与触发页，能让“分析当前草稿”成为更纯粹的教程主案例
- 把 `/dashboard/ai/optimization-history` 作为记录页，可以稳定承接：
  - 最近优化记录
  - 缓存报告
  - viewer 只读体验
- 最终形成更清晰的三段式：
  - 工作台：输入与分析
  - 结果页：阅读与应用
  - 记录页：回看与复用

## 十一、字段级建议说明与原因说明为什么重要

- 只有“改完后的文案”会让 AI 看起来像黑盒改写
- 增加 `suggestion` 与 `reason` 后，结果更接近：
  - AI 在建议什么
  - 为什么值得这样改
  - 用户为什么可以接受或拒绝
- 这一层解释能力后续还能复用到：
  - 编辑器内联建议
  - 字段级 accept / reject
  - 审核式 AI 交互

## 十二、为什么真实 AI 调用必须服务端留痕

- AI 功能不是普通本地交互，真实 Provider 调用通常意味着成本、耗时和失败风险
- 只做前端本地历史的问题：
  - 换设备或清缓存后丢失
  - 无法审计真实调用次数
  - 无法复盘失败请求和耗时
- 本轮新增 `ai_usage_records` 的教学重点：
  - `analysis-report` 存完整报告详情，支持 Drawer 刷新后回看
  - `resume-optimization` 只存轻量摘要和模块变化，不复制可写回草稿的 patch
  - 成功和失败都记录，方便解释“AI 调用链路不是只有 happy path”
- 一个稳定的记录字段模型应该覆盖：
  - 调用类型
  - 分析场景
  - 输入摘要
  - Provider / model
  - 状态与耗时
  - 关联结果 id
  - 可回看的详情 JSON

## 十三、Table + Drawer 为什么适合 AI 分析报告

- AI 分析报告天然是“多次调用、多种场景、多层详情”
- Table 负责解决列表层问题：
  - 本次是什么场景
  - 使用什么语言
  - 来源是 Provider 还是缓存
  - 评分或摘要是什么
  - 什么时候生成
- Drawer 负责解决阅读层问题：
  - 结论层
  - 依据层
  - 风险层
  - 建议动作
- 这种结构比“生成后直接堆在页面底部”更适合教学：
  - 列表能说明每次 AI 调用都被记录
  - Drawer 能说明 AI 输出如何被结构化阅读
  - 页面主路径不会被历史详情淹没

## 十四、局部 pending 浮层比全页 loading 更适合工作台

- 工作台不是单页提交表单，而是包含输入、文件提取、辅助分析和结构化优化的复合界面
- 如果只改按钮 loading，用户仍可能误点其他控件
- 如果做全页 loading，又会破坏“当前操作发生在哪个模块”的空间感
- 局部浮层的收益：
  - 明确告诉用户正在分析当前草稿
  - 覆盖当前分析面板，减少误操作
  - 保留页面上下文，让用户知道自己还在 AI 工作台
  - 后续可以平滑替换成 SSE 真实阶段事件

## 十五、结果页为什么要强化左右对比而不是重复标题

- 对比页的核心不是反复写“当前草稿 / 修改建议”，而是让视觉结构本身可读
- 左侧当前内容适合中性浅底和普通边框
- 右侧修改内容适合蓝色浅底和强调边框
- 建议说明应该和建议内容放在同一张卡里，让用户先理解“要改成什么”
- 原因说明用浅黄色和小字号弱化展示，让用户再理解“为什么这样改”
- 本轮保持模块级 apply，是为了让用户先学会按模块审阅 AI 建议；字段级 apply 留给 M21 或后续编辑器内联建议

## 十六、复合工作台里为什么要避免表单嵌套

- AI 工作台不是单一表单，而是“文件提取 + 当前草稿优化 + 辅助分析”组合界面
- 如果把文件提取面板作为 accessory 直接塞进主分析表单，HTML 会出现 `<form>` 嵌套 `<form>`
- 这类结构在 React / Next.js 下会直接触发 hydration warning，严重时还会让 submit 行为混乱
- 更稳妥的做法是：
  - 每个独立提交行为使用自己的 `Form`
  - 文件提取面板与分析表单保持同级
  - 通过视觉容器表达“它们相关”，而不是通过 DOM 嵌套表达“它们从属”

## 十七、新增持久表后为什么必须显式执行 db:push

- 新增 `ai_usage_records` 只修改 `schema.ts` 并不会自动让本地数据库拥有新表
- 在当前项目约束下，我们选择显式 `db:push`，而不是启动自动迁移
- 这样做的收益：
  - schema 变更时机可控
  - 本地、测试、部署环境的行为一致
  - 不会在服务启动时偷偷改库
- 教学上也更清楚：出现 `history` 500 时，要优先排查“代码问题”还是“数据库未同步”

## 十八、为什么优化记录中心要从“资料卡”升级成“Table + Drawer”

- 资料卡适合少量结果摘要，但不适合承接“同一条记录下的多种 AI 调用详情”
- 记录中心真正要解决的是两个问题：
  - 列表层：这次优化是什么时候生成的、影响了哪些模块、关联了哪些分析
  - 详情层：这次优化背后还有哪些 JD 匹配 / Offer 对比 / 简历评审结果
- `Table + Drawer` 的好处：
  - Table 负责横向比较多条记录
  - Drawer 负责纵向展开单条记录的多分组阅读
  - 页面主区域不会被长内容冲散

## 十八点五、为什么 instruction 原文要放进 Modal，而不是直接塞进表格

- 优化记录表格的目标是“快速定位一条记录”，不是“当场读完整份 JD”
- 如果把 `summary + instruction` 全塞进“优化摘要”列，会立刻出现三个问题：
  - 行高失控，整张表很难扫描
  - `影响模块 / 关联状态 / 操作` 被挤压到很难读
  - 用户无法快速判断这条记录的主题
- 更合适的做法是：
  - 表格里只展示标题化摘要
  - 标题优先取 Markdown H1，没有 H1 就取第一条非空行
  - 原始 instruction 放进 `Modal`，需要时再展开完整阅读
- 这样用户在记录中心的阅读顺序就会变成：
  - 先看“这条记录是什么”
  - 再看“影响了哪些模块、关联了哪些分析”
  - 最后按需打开 Modal 看完整原文

## 十八点六、为什么优化结果 `Not Found` 时不能把整个 Drawer 打成报错墙

- `optimization-history` 的数据来源不是单一接口，而是：
  - 本地 recent history
  - 本地 relation index
  - 服务端 usage record detail
  - 服务端 result cache
- 其中 `resultId` 对应的 result cache 有 TTL，过期后返回 `Not Found` 是合理现象
- 如果前端把这种情况等同于“整个 Drawer 加载失败”，用户会误以为整条记录都坏了
- 更稳妥的交互应该是：
  - 只让“简历优化建议”分组降级
  - 告诉用户“该条优化结果已失效或不可读取”
  - 继续保留本地摘要、影响模块和回填动作
  - 已关联的 `JD 匹配 / Offer 对比 / 简历评审` 仍然继续可读
- 这也是 AI 产品里常见的一种分组级容错：不要因为一个缓存 miss，就把整条历史记录判死刑

## 十九、前端 relation index 为什么是这一轮最合适的关联方案

- 当前我们已经有：
  - `resultId`
  - `usageRecordId`
  - 优化输入文本
- 因此可以先用 `instructionHash` 做一个可解释的前端本地关联层：
  - 同一份输入下的 `resume-optimize`
  - 同一份输入下的 `jd-match / offer-compare / resume-review`
- 第一版只保留每个场景最新一次关联，原因是：
  - 简单
  - 易讲解
  - 不会过早引入“历史版本树”的复杂度
- 后续如果需要跨设备同步或审计升级，再把这层关系迁到服务端也更自然
