From fec000f79df22cb21c2313418ae890dc629536ed Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Wed, 2 Sep 2026 04:56:38 +0000 Subject: [PATCH] =?UTF-8?q?docs(rectification):=20record=20the=20surface?= =?UTF-8?q?=20round=20=E2=80=94=20BUG-479..483,=20DESIGN.md=20rectificatio?= =?UTF-8?q?n=20surface,=20walkthrough=20section=208?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45 --- BLOCKED.md | 7 ++ PROGRESS-rectification-ux-20260902.md | 95 +++++++++++++++++++ docs/BUG_HISTORY.md | 80 ++++++++++++++++ .../staging-manual-walkthrough-20260901.md | 19 ++++ frontend/DESIGN.md | 28 +++++- 5 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 PROGRESS-rectification-ux-20260902.md diff --git a/BLOCKED.md b/BLOCKED.md index 2c76efb2..e162e610 100644 --- a/BLOCKED.md +++ b/BLOCKED.md @@ -1,5 +1,12 @@ # BLOCKED +## 生时校正会话面:消除空白假死与交互摩擦(2026-09-02,分支 `codex/rectification-ux-20260902`) + +- **浏览器级手工验收未做:执行环境无登录态、无 Chrome。** 任务书任务 0 的六条手工项与任务 1 的整轮录屏(开场 → 3 道选择题 → 候选 → 采用)都做不了;已按任务书把它们写进 `docs/testing/staging-manual-walkthrough-20260901.md` 第 8 节,交给有真实会话的人。本轮的替代证据:`tests/rectification-surface-state.test.ts`(纯函数四态、hydration 超时/失败、文案)与 `tests/rectification-surface-contract.test.ts`(源码锁:一次揭幕、无重挂、续接同一行、空态、板首态)。 +- **1.3「若快照 API 提供填报时间宫位表则直接渲染」未做:API 不提供。** `frontend/src/app/api/rectification/cases/[caseId]/route.ts` 的 `dossierResponse` 只带 `accepted_time` / `confirmed_time`,grep `natal|declared` 无命中;按任务书只做文案与收窄,不造数据。 +- **书面偏差:hydration 不是 `Promise.allSettled([turns, snapshot])`,而是一次请求。** turns 与快照来自同一个 `GET /api/rectification/cases/:id` 读取,两个并行请求会重复;`hydrateRectificationCase` 用 `Promise.race` 对一次读取套 4 秒上限,语义(并行拉完再切换、超时仍揭幕)不变,契约锁改锁 `hydrateRectificationCase` + `RECTIFICATION_OPEN_HYDRATE_TIMEOUT_MS`。 +- **书面偏差:turns 后到的填充与重试计数复位不用 `useEffect`。** `npm run lint` 的 `react-hooks` 规则拦截 effect 内同步 setState;改为 React 文档的"渲染中按上一 prop 调整 state"模式与事件处理器内复位,行为等价。 + ## Agent 聊天流式体验与双会话面统一(2026-09-01,分支 `codex/streaming-ux-20260901`) - **~~任务 3 未做,等待第三批拆页合入~~ 已解除并完成(2026-09-02)。** `origin/staging` 合入 `bf6989ec`(第三批)、`124d3990`、`058e5db9` 后,本分支 rebase 到其上,任务 3 按任务书原文完成:`page.tsx` 的滚动 effect 与内联「跳到最新」按钮删除,跟随并入 `useConversationScrollAnchor`,校正面接入同一 hook 与 `JumpToLatestButton`,复用 `ChatComposer`(`value` 受控、500 字上限),`rectification-sticky-scroll.ts` 删除,720px 覆写删除。对应 DESIGN.md 四条与 BUG-477/476 一并落地。 diff --git a/PROGRESS-rectification-ux-20260902.md b/PROGRESS-rectification-ux-20260902.md new file mode 100644 index 00000000..5289be1f --- /dev/null +++ b/PROGRESS-rectification-ux-20260902.md @@ -0,0 +1,95 @@ +# PROGRESS · 生时校正会话面:消除空白假死与交互摩擦(2026-09-02) + +工作树:`.worktrees/rectification-ux-20260902` +分支:`codex/rectification-ux-20260902` +基线:`origin/staging` @ `4dc0c8c7`(`codex/streaming-ux-20260901` 的 merge commit;任务书前置条件满足)。 +执行时 `TASK-rectification-walkthrough-polish-20260902.md` 与 `TASK-unified-loading-20260902.md` 均未落地(远端无对应分支):B.2/D.2 按任务书由本轮承担,没有 `question.ready` 事件,0.4 走"完成后立即拉一次 + 定时重试"的分支。 + +未改服务端与 SQL。未改 `.gitea/workflows/**`。未合并 staging,未碰 main。未手写 `useCallback` / `useMemo`(`rectification-agentic-chat.tsx` 既有的原样保留,新增的 `refetchQuestion` / `reloadQuestion` / `startOpeningManually` 是普通函数)。BUG 编号开工时确认远端最大号为 478,本轮 479–483。 + +| 任务 | 状态 | commit | BUG | +| --- | --- | --- | --- | +| 0.1 入口有反馈不转圈 | 完成 | `91b8b33a` | BUG-479 | +| 0.2 一次揭幕、面板不重挂 | 完成 | `91b8b33a` | BUG-479 | +| 0.3 问题槽只有生成中态、在对话流内 | 完成 | `91b8b33a` | BUG-479 | +| 0.4 「等待服务端更新」→ live 行 + 重试 + 重新加载 | 完成 | `91b8b33a` | BUG-479 | +| 0.5 选择题/采用不留缝不闪卡 | 完成 | `91b8b33a` | BUG-480 | +| 0.6 空 turns 会话的起点 | 完成 | `91b8b33a` | BUG-481 | +| 0.7 停止后的文案 | 完成(现状不足,已补) | `91b8b33a` | — | +| 1.1 选择卡确认感 | 完成 | `ef11f6e3` | BUG-482 | +| 1.2 用户选择回显 | 完成(在 `91b8b33a`,与 `send` 重写同一处) | `91b8b33a` | BUG-482 | +| 1.3 盘面首态 | 完成(文案 + 收窄;宫位表 API 不提供,未造数据) | `ef11f6e3` | BUG-483 | +| 1.4 开场 live 文案 | 完成(在 `91b8b33a`) | `91b8b33a` | — | +| 1.5 402 跳转前提示 | 完成(在 `91b8b33a`) | `91b8b33a` | — | +| 2 DESIGN.md | 完成 | 本 docs commit | — | + +1.2 / 1.4 / 1.5 的改动都落在 `send` 与 `submitStructuredChoice` 的重写里,无法从任务 0 的 commit 里干净拆出,所以随 `91b8b33a` 提交。 + +## 六段空白与死角:各自的消除证据 + +| 空白 | 消除方式 | 证据(契约 / 纯函数用例) | +| --- | --- | --- | +| 1 首页卡片无反馈 | `rectificationCardLabel` 在 `rectificationLoading` 时为「正在打开…」;卡片 `aria-busy` + `data-opening` + `cursor: progress`;侧栏目标行「打开中」 | `rectification-surface-contract` → "entry feedback is static…" | +| 2 侧栏进入:普通对话 → 空白 → 重挂 | `selectSession` 对未打开的校正会话不再先 `setActiveSessionId`/写 URL;hook 在 `/cases/open` 后 `hydrateRectificationCase`(一次 Case 读取取 turns + 快照,4 秒上限)再一次性切换;面板 key 去掉 `-ready/-loading` | 同上 → "…revealed once…";`rectification-surface-state` → "hydration reads turns and snapshot from one Case read" / "…resolves incomplete…" | +| 3 第一轮结束整面重挂 | key 只剩 `${sessionId}-${caseId}`;后到的 turns 经 prop 更新只填空 transcript(渲染中按上一 prop 调整) | 同上(`doesNotMatch(page, /"ready" : "loading"/)`、`seededTurns` 锁) | +| 4 快照回来前问题槽空 | 快照随揭幕到位(`initialSnapshot` 初始化 state,`caseSnapshotLoaded` 初值 `initialSnapshot !== null`);面板不再自拉 | 同上(`useState(initialSnapshot !== null)`、无挂载 fetch) | +| 5 选择题两段缝 + 闪卡 | `send(action, text, continuation)` 续接同一条 live 行并跳过 busy 守卫;`willContinue` 在同一 async 链里 `await send("read_only", …)`;`choiceContinuationPending` 与 effect 删除;`busy` 全程 true | 同上 → "…keeps one live row through the follow-up turn" | +| 6 采用只有按钮变灰 | 点击即追加「正在采用 HH:MM…」行,POST → 快照 → 复用该行续接 read_only | 同上;`rectification-surface-state` → `rectificationAdoptingLabel` | +| 死角 空 turns 会话 | `rectificationConversationState` 判 `empty` → 「这段校正还没有开始。」+「开始提问」(`send("opening")`,`openingStarted` 守卫) | `rectification-surface-state` → "conversation state…";契约 → "an empty Case…" | +| 文案 「等待服务端更新」 | `rectificationQuestionSlotState` 四态:`preparing`(时间线 live 行「正在准备下一个问题…」+ 2s 定时重拉 ≤2 次)→ `unavailable`(「没有拿到下一个问题。」+ 44px「重新加载」) | `rectification-surface-state` → "question slot: a gap is preparing…";契约 → "…never tells the reader to wait for the server" | + +## 改动文件 + +- 新建:`frontend/src/lib/rectification-surface-state.ts`(常量、hydration、turn 解析、两个纯状态函数、板空态文案、填报时间派生)、`frontend/tests/rectification-surface-state.test.ts`(9 用例)、`frontend/tests/rectification-surface-contract.test.ts`(8 用例)。 +- 改动:`use-rectification-surface.ts`、`use-session-management.ts`、`page.tsx`(state 2 处、hook 参数、卡片文案、面板 key/props、侧栏 prop、import)、`app-sidebar.tsx`、`sidebar-session-row.tsx`、`starter-home.tsx`、`conversational-birth-time-rectification.tsx`(props)、`rectification-agentic-chat.tsx`、`consultation-run-timeline.tsx`(导出 `ConsultationTimelineLiveRow`,同一行形态)、`rectification-choice-card.tsx`、`rectification-board.tsx`、`rectification-board-model.ts`(`rectificationBoardPeekCopy` 第二参可选,旧调用不变)、`globals.css`、`DESIGN.md`、`docs/BUG_HISTORY.md`、`docs/testing/staging-manual-walkthrough-20260901.md`(第 8 节)、`BLOCKED.md`。 + +## 1.3 核实 + +`frontend/src/app/api/rectification/cases/[caseId]/route.ts` 的 `dossierResponse` 只带 `accepted_time` / `confirmed_time` / `candidate_range`,`grep -n 'natal\|declared\|birth_time\|profile'` 无命中 → 快照 API 不提供填报时间的宫位表。按任务书只做文案与收窄,`declaredTime` 由 `page.tsx` 从 profile(`reportedTime || time`,须为 `H:MM`)派生。 + +## 与任务书的书面偏差(均登记 BLOCKED.md) + +1. hydration 是一次 `GET /api/rectification/cases/:id`(turns 与快照同一端点)套 4 秒 `Promise.race`,不是 `Promise.allSettled([turns, snapshot])`;语义不变。契约锁改锁 `hydrateRectificationCase` + `RECTIFICATION_OPEN_HYDRATE_TIMEOUT_MS`。unified-loading 落地时应把它的 4 秒常量与 `RECTIFICATION_OPEN_HYDRATE_TIMEOUT_MS` 合一。 +2. turns 后到的填充与重试计数复位不用 `useEffect`(`npm run lint` 的 `react-hooks/set-state-in-effect` 拦):前者是渲染中按上一 prop 调整 state,后者在 `applyCaseSnapshot`(有问题到达)与 `setPending(true)` 里复位。 +3. 选择卡 pending 行用 `InlineSpinner` + shimmer 文案而不是嵌一个 `ConsultationTimelineLiveRow`:卡片内是 `

`,与时间线 live 行同一形态(同 spinner、同 `agent-activity-status__text`)。 + +## 被触碰的既有断言(红线 4,每处源码上方均注明原值与错因) + +| 文件 | 原断言 | 现在 | 理由 | +| --- | --- | --- | --- | +| `tests/rectification-agentic-entry.test.ts` :85、:194 | `key={…-${rectificationTurns.length > 0 ? "ready" : "loading"}}` | key 只含 session/Case;`doesNotMatch /"ready" : "loading"/`;`initialSnapshot={rectificationSnapshot}` | 锁的正是重挂缺陷(BUG-479) | +| 同上 :93 | `page` 含 turn 解析的 `methods: Array.isArray(…)` | 改读 `rectification-surface-state.ts`,`page` 锁 `parsePersistedRectificationTurns(payload?.turns)` | 解析器搬家,形状不变(BUG-479) | +| 同上 :221–:224 | `await loadCaseSnapshot(); … choiceContinuationPending.current = true;`、effect 里 `void send("read_only", "")`、`doesNotMatch` 同链 `await send` | 锁 `await loadCaseSnapshot(); … await send("read_only", "", { reuseAssistantRenderKey…, label: adoptingLabel })`,`doesNotMatch /choiceContinuationPending/` | 锁的是 effect 中转本身(BUG-480) | +| 同上 :504 | workspace className 以 ` is-board-open` 结尾 | 追加 `${candidateResult ? "" : " is-board-empty"}` | 空板收窄(BUG-483) | +| 同上 :557 | `match(chat, /isStructuredChoiceUserText/)` | `doesNotMatch` + `userVisibleChoiceLine(answeredCard, optionId)` | 过滤掉的正是用户回显(BUG-482) | +| `tests/rectification-answer-choice.test.ts` :1123 | `send\("read_only", ""\)` | `await send("read_only", "", { reuseAssistantRenderKey…, label: recordingLabel })` | 续接参数(BUG-480) | +| `tests/rectification-spoken-collect.test.ts` :134–:138 | `showMissingQuestion = Boolean(caseSnapshotLoaded…currentQuestion === null` + 「当前没有可回答的问题,正在等待服务端更新」 | 锁 `rectificationQuestionSlotState({… snapshotLoaded: caseSnapshotLoaded, … resumableCase,`;`doesNotMatch /等待服务端更新/` | 锁的正是开发者文案(BUG-479) | + +未改任何其它既有断言;`rectification-answer-choice` :583 的 `isStructuredChoiceUserText` 纯函数用例原样保留(函数仍存在,只是不再用于过滤渲染)。 + +## 命令输出 + +`./node_modules/.bin/tsc --noEmit` → **0**(任务 0、任务 1 后各一次)。 + +`./node_modules/.bin/eslint` 触及 13 个文件 → **0 error**,42 warning 全在 `page.tsx`(与 `origin/staging` 同文件既有 `react-hooks` warning;本轮在 `page.tsx` 只加了 2 个 state、1 个派生值、4 行 props,未新增 warning)。开发中一度因 effect 内 setState 报 2 个 error,已改(见偏差 2)。 + +Python quick gate 4 个文件(`test_daily_and_rectification_entrypoints` / `test_session_management_entrypoints` / `test_birth_time_journey_contract` / `test_supabase_user_data_contract`)→ **24 passed**。 + +`./node_modules/.bin/tsx --test tests/*.test.ts`(基线在同一 worktree 的 `4dc0c8c7` 干净状态上跑): + +| | 基线 `4dc0c8c7` | 现在 | +| --- | ---: | ---: | +| tests | 2486 | 2503 | +| pass | 2452 | 2469 | +| fail | 24 | 24 | +| skipped | 10 | 10 | + +失败清单与基线逐条比对:`comm -3` 差集为空,同一集合。24 条失败全是本机无 Docker / PostgreSQL / 部署夹具的数据库、Better Auth、staging workflow、live sync 类既有缺口(与前两轮 PROGRESS 同一组)。 + +聚焦 26 套件(本轮 2 个新文件 + 校正/会话/composer/滚动/样式契约)→ 248 / 248。 + +`./node_modules/.bin/next build` → **0**。`✓ Compiled successfully in 49s`、`Finished TypeScript in 49s`、`Generating static pages (62/62)`,`/` 仍是 `○ (Static)`。5 条既有 `skill-package-registry.ts` filesystem-tracing warning,未失败。 + +## 未做与原因 + +见 `BLOCKED.md` 顶部:浏览器级手工验收(六条手工项 + 整轮录屏)无登录态无 Chrome,已写进 `docs/testing/staging-manual-walkthrough-20260901.md` 第 8 节;1.3 的宫位表 API 不提供;两条书面偏差。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 6559b21b..828e062a 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -7362,3 +7362,83 @@ - 相关记录:BUG-477 - 复发自:无 - 修复版本:待发布 + +## BUG-479 | 生时校正会话面在打开、恢复与第一轮结束时各有一段空白,且整块重挂 + +- 状态:resolved +- 首次发现:2026-09-02 +- 最近更新:2026-09-02 +- 影响面:`page.tsx`、`use-rectification-surface.ts`、`use-session-management.ts`、`rectification-agentic-chat.tsx`、`starter-home.tsx`、`sidebar-session-row.tsx`、`app-sidebar.tsx`、`rectification-surface-state.ts`(新) +- 用户现象:首页点「生时校正」卡片无任何反馈;从侧栏点开已有校正会话先闪一帧普通对话、再整块空白、再重挂;新建校正第一轮回答结束时整个面板闪一下、滚动归零;恢复会话后问题槽在快照回来前空着;问题槽出现「正在等待服务端更新」的开发者文案。 +- 触发条件:任意进入校正面;任意校正第一轮完成。 +- 根因:面板 key 含 `rectificationTurns.length > 0 ? "ready" : "loading"`,turns 到达即卸载重挂;`rectificationSurfaceOpen` 要求 `activeSession.id === rectificationSessionId`,而 `selectSession` 先切会话再异步 open;面板挂载后自己再拉一次快照;入口卡只 `disabled`。 +- 修复:`/cases/open` 返回后先 `hydrateRectificationCase`(一次 Case 读取同时取 turns 与快照,4 秒上限)再一次性切换会话;`selectSession` 对未打开的校正会话不再先 `setActiveSessionId`;面板 key 只保留 `${sessionId}-${caseId}`,turns/快照作为初始 state,后到的 turns 经 prop 更新只填空 transcript;卸载时中止流与快照读;入口卡与侧栏行给静态「正在打开…」/「打开中」;问题槽改为纯函数四态(`rectification-surface-state.ts`),缝隙期显示时间线 live 行并定时重拉,两次后给「重新加载」按钮,旧文案删除。 +- 验证:`tests/rectification-surface-contract.test.ts`、`tests/rectification-surface-state.test.ts`、`rectification-agentic-entry`(key 锁改写)、`rectification-spoken-collect`(文案锁改写)、`chat-session-url`。 +- 防复发:校正面一个会话只挂载一次;turns 与快照只能是 prop/state 更新;任何等待必须是时间线 live 行或问题槽 live 行,不得出现「等待服务端更新」类文案;揭幕后不得再出现非生成中的 spinner。 +- 相关记录:BUG-480、BUG-481、BUG-478 +- 复发自:无 +- 修复版本:待发布 + +## BUG-480 | 选择题点击与采用候选之间有空帧、下一题卡片闪现,采用时只有按钮文案在变 + +- 状态:resolved +- 首次发现:2026-09-02 +- 最近更新:2026-09-02 +- 影响面:`rectification-agentic-chat.tsx` +- 用户现象:点完选项后「正在记录本次选择」行消失一帧,下一题卡片闪一下又没了,再出现新的「正在处理」行;点「采用此时间」只看到按钮变灰变成「正在采用…」,对话区静止数秒。 +- 触发条件:任意会继续追问的结构化选择;任意采用候选。 +- 根因:`submitStructuredChoice` 的 `willContinue` 分支删掉 thinking 行、置 `choiceContinuationPending`,由下一次 effect 再 `send("read_only")` 并重新追加一行;中间 `busy` 掉回 false,`showLiveChoiceCard` 对刚装进的新卡片为真。`acceptCandidate` 不在 transcript 里放任何 live 行。 +- 修复:`send` 增加 `continuation: { reuseAssistantRenderKey, label }`,续接同一条 live 行并跳过 busy 守卫;选择与采用都在同一个 async 链里 `await loadCaseSnapshot()` 后直接 `await send("read_only", …)`,`busy` 全程为 true;`choiceContinuationPending` 与对应 effect 删除;采用时先追加「正在采用 HH:MM…」行。 +- 验证:`tests/rectification-surface-contract.test.ts`、`rectification-agentic-entry`(采用链锁改写)、`rectification-answer-choice`(`send("read_only", "")` 锁改写)。 +- 防复发:续接的回合复用已有 live 行,不得删行再由 effect 追加;`busy` 在整条链上不得中途回落。 +- 相关记录:BUG-479 +- 复发自:无 +- 修复版本:待发布 + +## BUG-481 | 开场失败或未持久化的校正会话再进来永远空白,没有任何起点 + +- 状态:resolved +- 首次发现:2026-09-02 +- 最近更新:2026-09-02 +- 影响面:`rectification-agentic-chat.tsx`、`rectification-surface-state.ts` +- 用户现象:从侧栏进入一个没有任何记录的旧校正会话,画面空白,输入框可用但用户不知道该先说什么。 +- 触发条件:服务端对 resumed case 一律返回 `should_start_opening=false`,且该 case 的 turns 为空。 +- 根因:面板只在 `shouldStartOpening` 为真时自动发开场,其它情况不显示任何 CTA。 +- 修复:`rectificationConversationState` 判定 `empty` 时显示「这段校正还没有开始。」与「开始提问」按钮,调用已有的 `send("opening", "")`;服务端已抑制重复开场,客户端 `openingStarted` 守卫。不改服务端与 SQL。 +- 验证:`tests/rectification-surface-state.test.ts`、`tests/rectification-surface-contract.test.ts`。 +- 防复发:面板任何"无内容"状态都必须给出下一步动作。 +- 相关记录:BUG-479 +- 复发自:无 +- 修复版本:待发布 + +## BUG-482 | 用户的选择不回显、选中无确认感 + +- 状态:resolved +- 首次发现:2026-09-02 +- 最近更新:2026-09-02 +- 影响面:`rectification-agentic-chat.tsx`、`rectification-choice-card.tsx`、`globals.css` +- 用户现象:点完选项,transcript 里没有一条"我选了 B",刷新后更看不到自己答过什么;卡片选中后没有对勾,等待时只是鼠标变沙漏。 +- 触发条件:任意结构化选择。 +- 根因:答过的卡贴在 assistant 消息下但不产生用户行;服务端返回的 `userMessage`(`applied.userDisplay`)客户端未用;持久化的结构化选择 turn 被 `isStructuredChoiceUserText` 过滤掉;卡片 pending 只改 `cursor: wait`。 +- 修复:点击即以 `userVisibleChoiceLine`(停止用 `stop_label`)追加用户气泡,成功后以服务端 `userMessage` 校正文本,失败则连同 live 行一起移除;`messagesFromTurns` 保留结构化选择 turn;卡片选中项显示 `Check` + 「已选择」,pending 时卡顶一行 `InlineSpinner` + 「正在记录…」。 +- 验证:`tests/rectification-surface-contract.test.ts`、`rectification-agentic-entry`(`isStructuredChoiceUserText` 锁改写)。 +- 防复发:用户的每个动作在 transcript 里都要有对应的用户行;等待态必须有可见进度。 +- 相关记录:BUG-480 +- 复发自:无 +- 修复版本:待发布 + +## BUG-483 | 盘面首态是一整块空面板 + +- 状态:resolved +- 首次发现:2026-09-02 +- 最近更新:2026-09-02 +- 影响面:`rectification-board.tsx`、`rectification-board-model.ts`、`rectification-surface-state.ts`、`page.tsx`、`globals.css` +- 用户现象:第一阶段右侧 18–22.5rem 的面板只有一句「补充经历后,这里会显示当前本命宫位和换升时刻。」;移动端 peek 是「补充经历后会在这里更新」。用户明明填过出生时间。 +- 触发条件:任何候选结果出现之前。 +- 根因:面板没有拿到填报时间;快照 API 不含填报时间对应的宫位表(已核实 `cases/[caseId]/route.ts` 只有 accepted/confirmed time)。 +- 修复:面板新增 `declaredTime`(`page.tsx` 从 profile 派生),无结果时时钟位显示填报时间,正文两行「填报出生时间 HH:MM」「回答几个问题后,这里会显示宫位随时间的变化。」;peek 为「当前盘面 · 填报 HH:MM」;`is-board-empty` 把板列收为 `minmax(16rem, 18rem)`。不造宫位数据。 +- 验证:`tests/rectification-surface-state.test.ts`、`tests/rectification-surface-contract.test.ts`、`rectification-board-model`。 +- 防复发:面板空态必须显示已知的用户数据,不得只放一句说明。 +- 相关记录:BUG-479 +- 复发自:无 +- 修复版本:待发布 diff --git a/docs/testing/staging-manual-walkthrough-20260901.md b/docs/testing/staging-manual-walkthrough-20260901.md index 7ab78c6a..8431d142 100644 --- a/docs/testing/staging-manual-walkthrough-20260901.md +++ b/docs/testing/staging-manual-walkthrough-20260901.md @@ -56,3 +56,22 @@ --- 四轮改造:BUG-464(消息权威化)、BUG-465(会话 URL)、BUG-466(云端唯一真相)、拆页两批。自动化侧每轮均已验收(tsc / 2400+ 条测试 / 构建 / SQL 幂等),此清单覆盖的是自动化够不着的真浏览器行为。 + +## 8. 生时校正面:一次揭幕与无缝续接(BUG-479–483) + +1. 首页点「生时校正」卡片。 +- ✅ 预期:卡片脚注立即变成「正在打开…」(不转圈),随后**一次性**出现完整面板:历史消息、问题槽、右侧盘面同时就位;不会先出现一个空面板再填内容。 +2. 在普通对话里,从侧栏点一个已有的校正会话。 +- ✅ 预期:旧画面保持不动,侧栏该行短暂显示「打开中」,然后直接切到校正面板;**不闪**一帧普通对话,**不出现**空白面板。 +3. 新建一段校正,等第一条引导说完。 +- ✅ 预期:第一轮结束的瞬间画面不闪、滚动不归零、时间线开合状态保留。 +4. 连续答两道选择题。 +- ✅ 预期:点完选项立刻在卡上看到对勾「已选择」、卡顶一行「正在记录…」,transcript 里出现你选的那句作为你的气泡,后面一条「正在记录本次选择…」的进行中行**一直在**,直到下一题出现;中间没有空帧,下一题卡片不会闪现又消失。 +5. 出现候选时间后点「采用此时间」。 +- ✅ 预期:transcript 末尾出现「正在采用 HH:MM…」进行中行,同一条行一直到后续回复出现。 +6. 找一个当初开场失败的旧校正会话(记录为空)进入。 +- ✅ 预期:看到「这段校正还没有开始。」和「开始提问」按钮,点后正常开场。 +7. 右侧盘面在没有候选前。 +- ✅ 预期:头部时钟位显示你填报的出生时间,正文是「填报出生时间 HH:MM」两行;面板比有结果时窄一些。 +8. 流式回答中点停止。 +- ✅ 预期:已生成内容保留,提示「已停止,已生成的内容保留;本次不会扣点。」,不是「暂时不可用」。 diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index 87cc7856..fe7c16fb 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -200,6 +200,32 @@ The base unit is 4px. Tokens are `--space-1: 4px`, `--space-2: 8px`, `--space-3: - **Surface:** canvas fill, hairline border, `--shadow-elevated`; hover uses the warm card surface. No utility-class shadows. - **Accessibility:** a real button in document order with a visible label matching its accessible name, 44px target, and the focus ring; the icon is decorative. +### Rectification surface + +The birth-time rectification session is the consultation transcript plus a question slot and a house board. It shares every waiting vocabulary with the consultation surface; nothing here spins or says "loading" after the reveal. + +- **Reveal:** the surface mounts once per session/Case binding. Opening a Case (homepage card or sidebar row) reads the turns and the snapshot in one Case request before the switch, under the same 4-second budget as the home reveal; the entry shows a static note meanwhile (card footer “正在打开…”, sidebar row “打开中”, `cursor: progress`) and the previous view stays put. Turns and snapshot are initial state; anything that arrives later is a prop or state update, never a remount. A late or failed read still reveals, with the composer notice “校正记录没有完全加载,可以继续”. +- **States** — what the question slot, the transcript's trailing live row and the composer show: + +| State | Question slot | Trailing live row | Composer | +|---|---|---|---| +| `opening` | empty | “正在读取你的出生资料,准备第一个问题…” then tool labels | disabled, stop visible | +| `resuming` | whatever the snapshot already holds | none | enabled | +| `empty` | empty | none; the transcript shows “这段校正还没有开始。” and one primary action “开始提问” | enabled | +| `waiting-question` | one timeline live row “正在准备下一个问题…”, refetching on a 2s timer up to two retries | none | enabled | +| `waiting-question`, retries spent | “没有拿到下一个问题。” and a secondary 44px “重新加载” | none | enabled | +| `choice-live` | the choice card | none | enabled, placeholder points at the card | +| `choice-pending` | the answered card, `Check` + “已选择” on the tap, a top row “正在记录…” | the same live row from “正在记录本次选择…” through the follow-up turn | disabled, stop visible | +| `candidates` | empty | none | enabled | +| `accepted` | empty until the next question | “正在采用 HH:MM…” through the follow-up turn, then “用这个时间看盘” | disabled during the chain | +| `confirmed` | empty | none; “已确认校正时间:HH:MM” | enabled | +| `readonly` | hidden | none; “该校正已结束,只能查看历史。” and “再次校正” | disabled | +| `failed` / stopped | unchanged | the row settles with what streamed; notice “已停止,已生成的内容保留;本次不会扣点。” | enabled | + +- **Rules:** the question slot is the last entry of the transcript, indented like an assistant entry, never a widget below the cards. A reader's tap is echoed as their own line (server wording when it differs), live and after a refresh alike. A follow-up turn continues on the live row already in place; `busy` never drops in the middle of a chain. No copy may ask the reader to wait for the server; a gap is a live row with retries, then a reload button. +- **Board:** before any candidate exists the header clock shows the declared birth minute and the body reads “填报出生时间 HH:MM” / “回答几个问题后,这里会显示宫位随时间的变化。”; the column narrows to `minmax(16rem, 18rem)` (`is-board-empty`) and widens once a result arrives. No house table is invented for the declared time; the snapshot API does not provide one. +- **Accessibility:** the opening notes set `aria-busy`; the live row is the timeline row (`role="status"` shimmer label); the reload and start actions are real 44px buttons. + ### Birth time intake - **Structure:** birth date, five radio choice rows for time knowledge, then only the time, uncertainty, period, or clue field required by the selected source. @@ -405,7 +431,7 @@ or user IDs. | 行内 / 局部等待 | 出生地解析、两个会话面时间线的 live 步、兜底活动面板的 live 行、个人报告列表与详情 | `InlineSpinner`(`inline-spin`) | 0.8s linear | `animation: none`,收成静止圆点,不要半圈圆弧 | | 流式生成中 | 引导语打字、时间线 summary 与 live 行的文案 | `onboarding-caret` / `agent-activity-shimmer` | 700ms steps / 1.6s linear | 保持现有全局降级 | -Agent 的 live 标记只有 `InlineSpinner` 一种。曾经并存的 canvas 小球(`thinking-orbs`)已移除,不得再引入第二种 live 标记。 +Agent 的 live 标记只有 `InlineSpinner` 一种。曾经并存的 canvas 小球(`thinking-orbs`)已移除,不得再引入第二种 live 标记。校正面所有等待复用行内等待:问题槽的「正在准备下一个问题…」与选择卡的「正在记录…」都是时间线 live 行的同一形态;进入校正面之前的等待只用静态文案(「正在打开…」/「打开中」),不转圈。 今日星语首次拉取是行内等待,但不用 spinner、也不用透明度呼吸:卡片用静态占位文案(`aria-busy` 仍保留)。轨道环消失后不得再换一套动效继续等。