From eeb82dfdc4843d20b95d6fc6f38b0390ddcf438e Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Tue, 1 Sep 2026 23:14:42 +0000 Subject: [PATCH] docs(chat): record streaming-ux progress, block task 3 on batch three, align DESIGN.md DESIGN.md gains the streaming-states table, the thinking-collapse and label-swap motion rows, the 17px body correction and the single live marker rule; task 3 is registered in BLOCKED.md until the home split batch three lands; PROGRESS lists every touched assertion with its former value. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45 --- BLOCKED.md | 6 ++ PROGRESS-chat-streaming-ux-20260901.md | 95 +++++++++++++++++++++++++ frontend/DESIGN.md | 31 ++++++-- frontend/src/lib/stream-frame-buffer.ts | 5 -- 4 files changed, 128 insertions(+), 9 deletions(-) create mode 100644 PROGRESS-chat-streaming-ux-20260901.md diff --git a/BLOCKED.md b/BLOCKED.md index 56aa6f7c..1c19f209 100644 --- a/BLOCKED.md +++ b/BLOCKED.md @@ -1,5 +1,11 @@ # BLOCKED +## Agent 聊天流式体验与双会话面统一(2026-09-01,分支 `codex/streaming-ux-20260901`) + +- **任务 3(校正面复用 `ChatComposer` + 统一滚动跟随 + `JumpToLatestButton` + 720→900 宽度)未做,等待 `TASK-home-split-batch3-20260901.md` 合入。** 任务书红线 7:任务 3 要动 `page.tsx` 两处(按 `activeStreamingText` 触发的滚动 effect、内联 Tailwind 的「跳到最新」按钮),与第三批拆页冲突。开工时 `git log origin/staging -- frontend/src/app/page.tsx` 最新为 `551d6317`(第二批),第三批尚未合入,故按红线登记。随之延后的还有任务 4 里与任务 3 对应的 DESIGN.md 条目(§4 阅读宽度、§5 composer 共用一句、§5 新增 Jump to latest、§7 `--shadow-soft`)与 BUG-476/475。任务 0/1/2 未碰 `page.tsx`。 +- **浏览器级手工验收未做:执行环境无登录态、无 Chrome。** 任务书里的 Performance 录制(3k 字回答无 >50ms 长任务)、结算不闪录屏、流式期间折叠时间线、校正/普通会话并排截图,全部留给有真实会话的人按 `docs/testing/staging-manual-walkthrough-20260901.md` 的方式补。本轮的替代证据是 `tests/home-streaming-render-split.test.ts` 的按帧驱动渲染计数与 `tests/stream-frame-buffer.test.ts` 的释放节奏断言。 +- **`LatestAssistantEntry` 的「一次会话只 mount 一次」探针无法在测试里驱动。** 仓库没有 jsdom / happy-dom,`renderToString` 不跑 effect,所以 `latestEntryMounts` 只是留给浏览器里手动读的探针;测试改用纯函数 `latestAssistantView` 的 key 一致性 + 源码锁(只有一处 `50ms 长任务)**未做**:无登录态、无 Chrome,见 `BLOCKED.md`。 + +## 任务 1 · 结算与时间线 + +改动:`src/components/chat-transcript.tsx`(重写)、`src/lib/chat-message-view.ts`(`latestAssistantView`)、`src/lib/home-streaming-render-probe.ts`(`latestEntryMounts`)、`src/components/consultation-run-timeline.tsx`(重写)、`src/components/chat-message-row.tsx`(GSAP 0.16)、`src/app/globals.css`、`scripts/home-streaming-render-benchmark.mts`(改用 `LatestAssistantEntry`)。 + +- `ChatTranscript` = `SettledMessageList excludeLatestAssistant` + 一个 `LatestAssistantEntry key={renderKey}`。同一个 `MessageEntry` 渲染历史行、流式行、刚结算行,动作条与追问只在 settled 出现。`renderKey` 在流式(`message-N`,N=历史条数)与结算(末条下标 N)下相同,纯函数 `latestAssistantView` 有测试锁。 +- `.message` 的 CSS `message-enter` 关键帧删除,只剩 GSAP 一份,时长 0.18 → 0.16(DESIGN.md §6 = 160ms)。 +- 时间线:去掉 `key={live…}`;外层 `
` + `button[aria-expanded][aria-controls]`;`open = userOpen ?? live`;折叠是 `grid-template-rows 0fr↔1fr` 180ms,折叠后内容 `inert`;summary 文案换行 120ms 淡入;live 行与 summary 文案共用既有 `agent-activity-status__text` shimmer;`rows` 为空且 live 时渲染 `QUEUED_TIMELINE_ROW`「正在处理…」。reduced-motion 下三处过渡为 0(全局 `*` 规则之外再显式写了一次)。 +- `latestEntryMounts` 探针只能在浏览器里手动读(无 DOM 测试库,`renderToString` 不跑 effect),见 `BLOCKED.md`。 + +## 任务 2 · 双会话面统一 + +改动:`src/lib/rectification-timeline-adapter.ts`(新)、`src/components/rectification-agentic-chat.tsx`、`src/components/agent-activity-status.tsx`(重写为兜底)、`src/components/chat-message-row.tsx`、`src/app/globals.css`、`package.json` / `package-lock.json`(`npm uninstall thinking-orbs`);删除 `consultation-thinking-report.tsx`、`thinking-step-tree.tsx`、`completed-activity-receipt.tsx`。 + +- 适配:tool trace → `calculate` 行(started=live / completed=done,标签沿用 `RECTIFICATION_TOOL_*_LABELS`);`failedTool` 的行标签加「未完成」且不挂 chips;回执 `methods` 经 `PUBLIC_RECTIFICATION_METHOD_LABELS` 去重 ≤8 作为最后一条完成 calculate 行的 `sources`;没有 live 行且 `activity.label` 以「正在」开头时补一条 live 行(`answer-composition` → `write`,其余 → `calculate`);think 类 trace 项映射为 think 行(现网不会出现,服务端丢弃)。 +- 校正结算后与普通会话一样收成「已完成 N 步」一行,方法 chips 在行内展开;第二个回执折叠块、消息上方红字失败行、重新生成时的假「正在组织回答…」全部删除;失败走既有 `error` 通知;重新生成显示 queued 行。 +- `chat-message-row.tsx` 只剩 timeline 一条主路径 + `AgentActivityStatus` 兜底(无 trace 分支、无 orb);live 标记全部 `InlineSpinner`;`.conversation.is-rectification .agent-thinking-step/.agent-thinking-marker` 14px 覆写删除。 +- 校正的「已用时 N 秒」计时(`ActivityElapsed`)随 trace 面板一起退出——时间线本来就没有这个元素,两面一致优先。 + +## 任务 3 · 未做 + +`git log origin/staging -- frontend/src/app/page.tsx` 最新 `551d6317`(第二批),第三批未合入。按红线 7 登记 `BLOCKED.md`,未占 BUG-476/475。 + +## 任务 4 · DESIGN.md + +已改:§3 思考正文 13px 写成显式例外;§5 Message 正文 16px → 17px(≤767px 时 16px,与 `globals.css` 一致)、Motion 改为"只有 GSAP 一份 160ms、结算不重放"、新增「Streaming states」七态表(含 failed 不用消息内红字、pacing 公式、reduced-motion 行为、校正只差不显示思考正文);§6 表新增 Thinking collapse 180ms / Timeline label swap 120ms / Text reveal;§9 等待表把时间线 live 步写成两面共用,注明 live 标记只有 `InlineSpinner`、orb 已移除。 + +随任务 3 延后:§4 阅读宽度(720 vs 900)、§5 composer 共用一句、§5 Jump to latest 条目、§7 `--shadow-soft`。 + +## 被触碰的既有断言(红线 3,每处源码上方均有原值与错因注释) + +| 文件 | 原断言 | 现在 | 理由 | +| --- | --- | --- | --- | +| `tests/home-streaming-render-split.test.ts` | `split.streamingRowRenders === tokens.length` | `≤ tokens.length && ≥ 1`,另加按帧驱动的新用例 | 原值是"每 token 一次提交"的现状快照(BUG-473) | +| `tests/chat-bundle-splitting-contract.test.ts` | `duration: 0\.18` | `0\.16` | DESIGN.md §6 160ms,0.18 是 GSAP 副本漂移(BUG-474) | +| `tests/session-conversation-layout.test.ts` | `duration:\s*0\.18` | `0\.16` | 同上 | +| `tests/class-name-definition-contract.test.ts` | `knownUnstyled` 含 `consultation-run-timeline__summary`、`consultation-step-tree` | 两条移除 | 前者现在有样式(按钮),后者类名已随组件删除;该测试本身要求列表无过期项 | +| `tests/chat-bundle-splitting-contract.test.ts` | "the thinking orb loads on demand…"(`OrbState` 导入、`dynamic(...)`、20px 占位、`prefetchOnIdle(importThinkingOrb)`) | 改为"live 标记是共享 InlineSpinner,无 orb chunk" | orb 是第二种 live 标记且被 14px 裁切(BUG-476) | +| `tests/chat-stream-layout.test.ts` | `reportSource`/`treeSource` 读取;``/不 open/「本轮完成 · N 个步骤」;`activityStatus /activityTrace/` | 改为锁 `chat /rectificationTimelineRows/`、`chat /completedReceipt/`、`activityStatus` 不含 `activityTrace` | 锁的是校正专属第二折叠块与 trace 面板(BUG-476) | +| `tests/rectification-activity-receipt.test.ts` | `deltaBranch` 内 `state: raw.trim() ? "streaming" : "thinking"`、`text: raw,`;`message.state === "settled" && message.failed` | 状态派生改锁 frame flush 段(`state: text.trim() ? …`、`text,`、`frames.setAnswer(raw)`);失败行改锁 `&& !message.failed` 且无 `rectification-activity-failure` | 前者锁的是每 chunk 一次 setMessages(BUG-473),后者锁的是校正专属红字失败行(BUG-476)。**注**:这个文件在任务 0 的聚焦跑里漏跑了,任务 2 跑全量前才发现,改在 `cc17d971` | + +新增测试:`tests/stream-frame-buffer.test.ts`(7)、`tests/chat-markdown-split.test.ts`(4)、`tests/chat-stream-settle-contract.test.ts`(4)、`tests/rectification-timeline-adapter.test.ts`(6)、`home-streaming-render-split` +1。 + +## 命令输出 + +`./node_modules/.bin/tsc --noEmit` → **0**(任务 0/1/2 各跑一次,均无诊断)。 + +`./node_modules/.bin/eslint` 触及文件 → **0 error**。`use-consultation-run.ts` 有 9 条既有 `no-unused-vars` warning(未使用的导入/`startNewChat`),本轮只加了一行 import,与 `origin/staging` 一致;`stream-frame-buffer.ts` 一条 `dirty` 未用已在本 commit 清掉。 + +`./node_modules/.bin/tsx --test tests/*.test.ts`: + +| | 基线(开工时 `fbb80fa3`) | 现在 | +| --- | ---: | ---: | +| tests | 2460 | 2482 | +| pass | 2426 | 2448 | +| fail | 24 | 24 | +| skipped | 10 | 10 | + +24 条失败逐条比对**完全同一集合**(`comm` 差集为空):全部是本机无 Docker / PostgreSQL / 部署夹具的数据库、Better Auth、staging workflow YAML、live sync 类(admin code functions、Owner recovery、local PostgreSQL、redeem security、personal report job/sections RLS、migration runner、v9 migration 等),与上一轮 PROGRESS 记录的环境缺口同类。10 条 skipped 同基线。无产品逻辑新增失败。 + +`./node_modules/.bin/next build` → **0**。`✓ Compiled successfully in 21.7s`、`Finished TypeScript in 58s`、`Generating static pages (62/62)`,`/` 仍是 `○ (Static)`。5 条既有 `skill-package-registry.ts` filesystem-tracing warning,未失败。 + +## 未做与原因 + +见 `BLOCKED.md` 顶部三条:任务 3 等第三批拆页;浏览器级手工验收(Performance 录制、结算录屏、两面并排截图、流式期间折叠)无登录态无 Chrome;`latestEntryMounts` 探针无 DOM 测试库不可驱动。 diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index 5e8e4005..ada6df41 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -156,6 +156,8 @@ is read through an external store so a change in one tab reaches the others. Display headings use the serif stack at weight 400. Body copy never drops below 14px; 12–13px is reserved for short labels and metadata. CJK text uses `text-wrap: pretty`; display text uses `text-wrap: balance`. +One documented exception: the thinking text inside a timeline step (`.consultation-run-timeline__thinking`) and the fallback thinking trace (`.message-thinking-body`) render at 13px. They are working notes shown on request inside a collapsed row, not reading copy; the answer itself never inherits that size. + ## 4. Spacing & Layout The base unit is 4px. Tokens are `--space-1: 4px`, `--space-2: 8px`, `--space-3: 12px`, `--space-4: 16px`, `--space-5: 20px`, `--space-6: 24px`, `--space-8: 32px`, `--space-10: 40px`, `--space-12: 48px`, `--space-16: 64px`, and `--space-24: 96px`. @@ -261,10 +263,26 @@ The base unit is 4px. Tokens are `--space-1: 4px`, `--space-2: 8px`, `--space-3: - **Variants:** assistant editorial text on canvas; user text on warm card surface; streaming; error. Streaming uses a timeline of completed steps plus the current step; the thinking body expands while streaming, collapses when answer text appears, and is stored with the assistant message. - **Identity:** every assistant message carries the 32px Jyotisha logo avatar; user messages stay visually lighter and avatar-free. -- **Typography:** assistant body 16px with serif subheadings; user body 14px. +- **Typography:** assistant body 17px (16px below 768px) with serif subheadings; user body 14px. - **Tables:** three-column technique audit tables keep 状态 on one line. Below 768px they stack each row as title + status, then the note, instead of squeezing 已执行 into a vertical glyph column. - **Follow-up:** the latest settled consultation answer may offer two or three grounded next questions under that answer. Clicking one sends it in the current session. The composer never hosts suggestion chips. If the answer does not support a grounded continuation, nothing is shown. -- **Motion:** new messages enter with a short opacity/translate transition only. +- **Motion:** a new row enters once, through the GSAP tween in `chat-message-row.tsx` at the 160ms Message duration; there is no CSS entrance keyframe beside it. The trailing assistant reply is one component (`LatestAssistantEntry`) from its first streamed token through settlement, so settling never remounts it and never replays the entrance. + +#### Streaming states + +Every assistant reply moves through the same states on both chat surfaces, and each state has exactly one visual. The step timeline (`ConsultationRunTimeline`) is the only activity surface; the rectification session projects its tool trace onto the same rows and differs only in that it never shows thinking text, because the server drops rectification `thinking.delta` at the public boundary. + +| State | When | Visible | Transition in | +|---|---|---|---| +| `queued` | request sent, no server event yet | timeline open with one live row “正在处理…” (spinner + shimmer label), summary “正在分析” | row enters with the message | +| `loading-method` | `skill.started` | live method row | label swap, 120ms fade | +| `calculating` | `tool.started` / chart activity | live calculate row; completed rows above it show the check marker | label swap, 120ms fade | +| `thinking` | `thinking.section` / `thinking.delta` | live think row; thinking text inside the row, paced per frame | label swap, 120ms fade | +| `composing` | first `answer.delta` | live write row; answer text below the timeline, released per frame | answer paragraphs appear as text, no per-token animation | +| `settled` | `run.completed` | summary becomes “已完成 N 步”, timeline collapses in place unless the reader opened or closed it themselves; actions and follow-ups appear | 180ms height transition, 120ms label fade | +| `failed` | `run.failed`, abort, network loss | whatever was received stays in place; the message carries no inline banner — the notice goes to the composer notice / error line | none | + +Text release is paced, not animated: the frame buffer commits at most once per animation frame and reveals `max(2, ⌈backlog ÷ 12⌉)` characters per frame, so a burst catches up in about twelve frames and a slow model never reads as stalled. The reader's own toggle on the timeline always wins over the state default. Under `prefers-reduced-motion: reduce` the collapse and the label fade are instant; the pacing stays, because it is content arrival rather than decoration. ### Suggestion card @@ -340,6 +358,9 @@ system. | Micro | 120ms | ease-out / `--ease-out` | Button, row, popover and tooltip feedback | | Standard | 180ms | `--ease-out` | Dialogs, onboarding cards, auth steps | | Message | 160ms | `--ease-out` | A message row entering the transcript | +| Thinking collapse | 180ms | `--ease-out` | The step timeline opening or closing in place (`grid-template-rows` 0fr ↔ 1fr) | +| Timeline label swap | 120ms | ease-out | “正在分析” → “已完成 N 步”, and a live row's label changing | +| Text reveal | per-frame release, not a duration | none | Streamed answer and thinking text; see Streaming states | | Sheet | 280ms | `--ease-out` | Rectification sheet rising from the bottom | | Spatial | Instant | None | Sidebar state changes and mobile drawer | @@ -372,8 +393,10 @@ or user IDs. | 类别 | 语义 | 组件 / 样式 | 时长 | `prefers-reduced-motion: reduce` | | --- | --- | --- | ---: | --- | | 整页 / 整块阻塞 | 载入账户、正在准备问题、生时评估浮层 | `AppLoadingIndicator`(轨道环 `app-loading-orbit`) | 1.4s linear | 全局 `*` 规则把循环收成一帧;不要改它的 DOM | -| 行内 / 局部等待 | 出生地解析、咨询时间线 live 步、个人报告列表与详情 | `InlineSpinner`(`inline-spin`) | 0.8s linear | `animation: none`,收成静止圆点,不要半圈圆弧 | -| 流式生成中 | 引导语打字、Agent 活动字 | `onboarding-caret` / `agent-activity-shimmer` | 700ms steps / 1.6s linear | 保持现有全局降级 | +| 行内 / 局部等待 | 出生地解析、两个会话面时间线的 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 标记。 今日星语首次拉取是行内等待,但不用 spinner、也不用透明度呼吸:卡片用静态占位文案(`aria-busy` 仍保留)。轨道环消失后不得再换一套动效继续等。 diff --git a/frontend/src/lib/stream-frame-buffer.ts b/frontend/src/lib/stream-frame-buffer.ts index c96b2c0b..92410aef 100644 --- a/frontend/src/lib/stream-frame-buffer.ts +++ b/frontend/src/lib/stream-frame-buffer.ts @@ -104,7 +104,6 @@ export function createStreamFrameBuffer( let answerBacklog = 0; let thinkingBacklog = 0; let meta = options.initialMeta; - let dirty = false; let disposed = false; let frameHandle: number | null = null; let timeoutHandle: number | null = null; @@ -121,7 +120,6 @@ export function createStreamFrameBuffer( }; const emit = (settled: boolean) => { - dirty = false; options.flush({ answer: releasedAnswer, thinking: releasedThinking, @@ -173,12 +171,10 @@ export function createStreamFrameBuffer( setMeta(next) { if (disposed) return; meta = typeof next === "function" ? (next as (current: Meta) => Meta)(meta) : next; - dirty = true; schedule(); }, touch() { if (disposed) return; - dirty = true; schedule(); }, settle() { @@ -198,7 +194,6 @@ export function createStreamFrameBuffer( releasedThinking = ""; answerBacklog = 0; thinkingBacklog = 0; - dirty = false; if (nextMeta !== undefined) meta = nextMeta; }, dispose() {