diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 71069d28..e108cada 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -707,9 +707,25 @@ - 用户现象:生时校正右侧直接显示浏览器或操作系统默认滚动条,与站内温和、低对比的编辑式界面不一致。 - 触发条件:生时校正历史内容超过消息区高度,在桌面浏览器出现纵向滚动条。 - 根因:`.rectification-message-list` 只声明了 `overflow-y: auto`,没有提供跨浏览器的产品内滚动条颜色、宽度和交互状态。 -- 修复:为生时校正消息区增加细宽、透明轨道、低对比圆角拇指,并在 hover 时适度加深;Firefox 使用标准 scrollbar 属性,Chromium / Safari 使用 WebKit 伪元素,颜色继续取现有设计 token。 -- 验证:CSS 契约锁定标准与 WebKit 两套样式,聚焦组件测试、目标 ESLint、production build 与生产浏览器 smoke。 -- 防复发:独立滚动容器必须复用产品 token,并同时覆盖标准 scrollbar 属性和 WebKit 伪元素;不得引入渐变、重阴影或高饱和装饰。 +- 修复:为生时校正消息区增加细宽、透明轨道、低对比圆角拇指;默认隐藏拇指,仅在指针移动、列表滚动或键盘焦点进入消息区时短暂显示,停止操作后自动隐藏。Firefox 使用标准 scrollbar 属性,Chromium / Safari 使用 WebKit 伪元素,颜色继续取现有设计 token。 +- 验证:CSS 契约锁定默认透明、交互显现及标准与 WebKit 两套样式,聚焦组件测试、目标 ESLint、production build 与生产浏览器 smoke。 +- 防复发:独立滚动容器必须复用产品 token,并同时覆盖标准 scrollbar 属性和 WebKit 伪元素;默认态不得持续抢占视觉注意力,也不得引入渐变、重阴影或高饱和装饰。 - 相关记录:BUG-041 - 复发自:无 - 修复版本:本次简约滚动条修复提交 + +## BUG-043 | 生时校正把后台证据状态重复渲染成可展开管理面板 + +- 状态:resolved +- 首次发现:2026-07-23 +- 最近更新:2026-07-23 +- 影响面:生时校正消息流、候选进度、历史经历与更正入口 +- 用户现象:Agent 已经在自然对话中确认和追问经历,消息区底部仍额外显示“当前候选 · 待验证”、候选范围、历史经历列表和“更正”按钮;用户需要理解并操作第二套记录界面,破坏一问一答的连续性。 +- 触发条件:任何已有候选或至少一条 evidence recap 的生时校正案例。 +- 根因:语言交互改版后仍保留旧产品流程的 `
` 进度与证据管理面板,把本应仅供后台评分和恢复使用的结构化状态再次暴露给用户。 +- 修复:从生时校正消息流移除整块候选进度与历史经历管理面板,不再显示候选状态、范围、经历列表或逐条更正按钮;后台 evidence、评分与持久化保持不变,最终可确认状态仍通过明确确认动作呈现。 +- 验证:组件与真实 Chromium 回归锁定页面不包含候选进度、历史经历面板或更正按钮,同时保留自然对话、流式回答、撤回窗口、自动贴底和最终确认能力。 +- 防复发:结构化 evidence 是 Agent 的后台推理与持久化输入,不得在语言优先界面重复渲染成需要用户管理的卡片或表单;需要纠正时继续通过自然语言表达。 +- 相关记录:BUG-020、BUG-034、BUG-042 +- 复发自:BUG-020 +- 修复版本:待提交 diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index b57f7a05..86c99bc9 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -788,16 +788,26 @@ input:disabled, select:disabled { color: var(--color-ink-tertiary); background: overflow-y: auto; overscroll-behavior: contain; padding-bottom: var(--space-6); - scrollbar-color: color-mix(in srgb, var(--color-ink-tertiary) 46%, transparent) transparent; + scrollbar-color: transparent transparent; scrollbar-gutter: stable; scrollbar-width: thin; } +.rectification-message-list.is-scrollbar-visible, +.rectification-message-list:focus-within { + scrollbar-color: color-mix(in srgb, var(--color-ink-tertiary) 46%, transparent) transparent; +} .rectification-message-list::-webkit-scrollbar { width: 10px; } .rectification-message-list::-webkit-scrollbar-track { background: transparent; } .rectification-message-list::-webkit-scrollbar-thumb { min-height: 40px; border: 3px solid transparent; border-radius: 999px; + background: transparent; + background-clip: padding-box; + transition: background-color 160ms ease; +} +.rectification-message-list.is-scrollbar-visible::-webkit-scrollbar-thumb, +.rectification-message-list:focus-within::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--color-ink-tertiary) 42%, transparent); background-clip: padding-box; } @@ -805,19 +815,13 @@ input:disabled, select:disabled { color: var(--color-ink-tertiary); background: background: color-mix(in srgb, var(--color-ink-secondary) 62%, transparent); background-clip: padding-box; } -.rectification-message-details { margin-top: var(--space-3); padding-top: var(--space-2); border-top: 1px solid var(--color-border); color: var(--color-ink-secondary); font-size: var(--type-caption); } -.rectification-message-details summary { min-height: 44px; display: flex; align-items: center; cursor: pointer; font-size: var(--type-body-sm); font-weight: 600; } -.rectification-message-details p { margin: 0 0 var(--space-2); font-size: var(--type-caption); line-height: 1.6; } -.rectification-message-details ul { display: grid; gap: var(--space-2); margin: var(--space-3) 0 0; padding: 0; list-style: none; } -.rectification-message-details li { min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: var(--space-3); padding-top: var(--space-2); border-top: 1px solid var(--color-border); } -.rectification-message-details button, .rectification-correction-target button { min-height: 44px; padding-inline: var(--space-3); border: 0; background: transparent; color: var(--color-action); cursor: pointer; } +.rectification-correction-target button { min-height: 44px; padding-inline: var(--space-3); border: 0; background: transparent; color: var(--color-action); cursor: pointer; } .rectification-composer-wrap { position: static; flex: 0 0 auto; } .rectification-correction-target { width: min(760px, 100%); display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); margin: 0 auto var(--space-2); color: var(--color-ink-secondary); font-size: var(--type-caption); } .rectification-correction-target p { margin: 0; } .conversational-empty-state { width: min(760px, 100%); margin: var(--space-8) auto; color: var(--color-ink-secondary); } @media (max-width: 430px) { - .rectification-message-details li { align-items: stretch; grid-template-columns: minmax(0, 1fr); } .rectification-correction-target { align-items: stretch; display: grid; } } diff --git a/frontend/src/components/conversational-birth-time-rectification.tsx b/frontend/src/components/conversational-birth-time-rectification.tsx index 95c505b6..e14dacec 100644 --- a/frontend/src/components/conversational-birth-time-rectification.tsx +++ b/frontend/src/components/conversational-birth-time-rectification.tsx @@ -11,7 +11,6 @@ import { } from "../hooks/use-conversational-rectification.ts"; import type { ConversationalRectificationResponse, - ConversationalRectificationTurn, } from "../lib/conversational-rectification/contracts.ts"; type SurfaceProps = Readonly<{ @@ -28,13 +27,6 @@ function safely(request: Promise) { void request.catch(() => undefined); } -function candidateStatus(turn: ConversationalRectificationTurn): string { - if (turn.status === "completed" && turn.candidate.status === "confirmed") return "已确认"; - if (turn.status === "completed") return "范围已保存,分钟未确认"; - if (turn.candidate.status === "ready_for_confirmation") return "待确认,尚未验证"; - return "待验证"; -} - export function ConversationalRectificationSurface({ controller, openingAssistantText = "", @@ -44,6 +36,7 @@ export function ConversationalRectificationSurface({ }: SurfaceProps) { const composer = useRef(null); const messageList = useRef(null); + const scrollbarHideTimer = useRef | null>(null); const undoTimer = useRef | null>(null); const [submission, setSubmission] = useState () => { if (undoTimer.current) clearTimeout(undoTimer.current); + if (scrollbarHideTimer.current) clearTimeout(scrollbarHideTimer.current); }, []); const pendingQuestion = turn?.status === "completed" ? turn.pendingConsultationQuestion @@ -132,10 +126,31 @@ export function ConversationalRectificationSurface({ setSubmission(null); requestAnimationFrame(() => composer.current?.focus()); }; + const revealScrollbar = () => { + const list = messageList.current; + if (!list) return; + list.classList.add("is-scrollbar-visible"); + if (scrollbarHideTimer.current) clearTimeout(scrollbarHideTimer.current); + scrollbarHideTimer.current = setTimeout(() => { + list.classList.remove("is-scrollbar-visible"); + scrollbarHideTimer.current = null; + }, 900); + }; + const hideScrollbar = () => { + if (scrollbarHideTimer.current) clearTimeout(scrollbarHideTimer.current); + scrollbarHideTimer.current = null; + messageList.current?.classList.remove("is-scrollbar-visible"); + }; return (
-
+
{submission?.phase === "undo" ? "消息已发送,可以撤回修改" : controller.pending ? "Jyotisha 正在核对经历" : ""} {(controller.messages ?? [{ role: "assistant" as const, @@ -157,41 +172,6 @@ export function ConversationalRectificationSurface({ message={{ role: "user", text: submission.text, renderKey: "pending-evidence", state: "settled" }} /> )} -
- - {turn.candidate.representativeTime - ? `当前候选 ${turn.candidate.representativeTime} · ${candidateStatus(turn)}` - : `校正进度 · ${candidateStatus(turn)}`} - -

- {turn.candidate.rangeStart && turn.candidate.rangeEnd - ? `候选范围 ${turn.candidate.rangeStart}—${turn.candidate.rangeEnd};已记录 ${turn.evidenceRecap.length} 条经历。` - : `已记录 ${turn.evidenceRecap.length} 条经历,尚未缩小候选范围。`} -

-

候选只用于继续验证;这一步不会自动采用候选,未经发布门禁与明确确认不会成为当前排盘时间。

- {turn.evidenceRecap.length > 0 && ( -
    - {turn.evidenceRecap.map((entry) => ( -
  • - {entry.dateLabel} · {entry.summary}{entry.isCorrection ? "(已修订)" : ""} - {canAnswer && ( - - )} -
  • - ))} -
- )} -
{controller.pending && canAnswer && ( controller.streamingAssistantText ? ]+id="conversational-rectification-answer"/); assert.match(markup, /像聊天一样回答即可/); @@ -104,7 +104,7 @@ test("rectification is a language-first exchange with one free-text answer path" assert.doesNotMatch(markup, /2006[^<]*2011|BirthTimeChoiceQuestion|birth-time-choice-question/); }); -test("a resumed legacy turn preserves its Agent narrative and keeps evidence in progress details", () => { +test("a resumed legacy turn preserves its Agent narrative without a separate evidence panel", () => { const legacyTurn = { ...turn, status: "active", @@ -119,7 +119,7 @@ test("a resumed legacy turn preserves its Agent narrative and keeps evidence in assert.match(markup, /05:30 是范围内的待验证候选/); assert.match(markup, /D1 保持稳定/); - assert.match(markup, /2021 年 7 月 · 开始第一份长期工作/); + assert.doesNotMatch(markup, /2021 年 7 月 · 开始第一份长期工作|已记录的经历/); assert.doesNotMatch(markup, /范围暂未变化不代表提交失败/); }); @@ -194,28 +194,23 @@ test("the first Agent guidance streams into the empty rectification surface", () assert.doesNotMatch(markup, /正在建立校正记录/); }); -test("evidence is correctable, secondary controls stay hidden, and confirmation is explicit", () => { +test("history management stays hidden while final confirmation remains explicit", () => { const markup = renderToStaticMarkup(React.createElement( ConversationalRectificationSurface, { controller: controller() }, )); - assert.match(markup, /已记录的真实经历/); - assert.match(markup, /2021 年 7 月/); - assert.match(markup, /更正这条经历:开始第一份长期工作/); + assert.doesNotMatch(markup, /已记录的真实经历|2021 年 7 月|更正这条经历/); assert.doesNotMatch(markup, /本轮分析|等待经历验证/); assert.doesNotMatch(markup, /本轮技术回执|rectification-technical-v1|consult-d9/); - assert.match(markup, /当前候选 05:18/); - assert.match(markup, /待确认,尚未验证/); + assert.doesNotMatch(markup, /当前候选 05:18/); assert.match(markup, /确认采用 05:18(尚未验证)/); assert.match(markup, /aria-label="确认将 05:18 设为当前排盘时间;当前分钟尚未验证"/); - assert.match(markup, /已记录 1 条经历/); - assert.match(markup, /候选只用于继续验证/); - assert.match(markup, /这一步不会自动采用候选/); + assert.doesNotMatch(markup, /已记录 1 条经历|候选只用于继续验证|这一步不会自动采用候选/); assert.doesNotMatch(markup, /暂停,稍后继续|继续校正|放弃本次校正/); }); -test("correction mode identifies its durable target, can be cancelled, and marks revised recaps", () => { +test("an existing correction target can still be cancelled without rendering history management", () => { const revisedTurn = { ...turn, evidenceRecap: [{ ...turn.evidenceRecap[0]!, isCorrection: true }], @@ -241,7 +236,7 @@ test("correction mode identifies its durable target, can be cancelled, and marks assert.match(markup, /正在更正/); assert.match(markup, /开始第一份长期工作/); assert.match(markup, /取消更正/); - assert.match(markup, /(已修订)/); + assert.doesNotMatch(markup, /(已修订)|已记录的经历|更正这条经历/); }); test("pending markup and responsive CSS expose accessibility contracts", () => { @@ -287,10 +282,12 @@ test("pending markup and responsive CSS expose accessibility contracts", () => { assert.match(css, /\.conversation\.is-rectification[^}]*padding-bottom:\s*0/); assert.match(css, /\.rectification-chat[^}]*height:\s*100%[^}]*display:\s*flex/); assert.match(css, /\.rectification-message-list[^}]*flex:\s*1[^}]*overflow-y:\s*auto/); + assert.match(css, /\.rectification-message-list[^}]*scrollbar-color:\s*transparent transparent/); assert.match(css, /\.rectification-message-list[^}]*scrollbar-width:\s*thin/); - assert.match(css, /\.rectification-message-list::\-webkit-scrollbar-thumb[^}]*background-clip:\s*padding-box/); + assert.match(css, /\.rectification-message-list::\-webkit-scrollbar-thumb[^}]*background:\s*transparent[^}]*background-clip:\s*padding-box/); + assert.match(css, /\.rectification-message-list\.is-scrollbar-visible::\-webkit-scrollbar-thumb[^}]*color-mix/); assert.match(css, /\.rectification-message-list::\-webkit-scrollbar-thumb:hover[^}]*color-mix/); - assert.match(css, /\.rectification-message-details button[^}]*min-height:\s*44px/); + assert.match(css, /\.rectification-correction-target button[^}]*min-height:\s*44px/); assert.match(css, /\.composer:focus-within[^}]*border-color:/); assert.match(css, /\.composer textarea[^}]*border:\s*0/); assert.match(css, /\.rectification-composer-wrap[^}]*position:\s*static/); @@ -298,8 +295,11 @@ test("pending markup and responsive CSS expose accessibility contracts", () => { assert.doesNotMatch(css, /\.conversational-domain-picker|\.conversational-event-date/); assert.doesNotMatch(css, /button\[aria-label\$="下一步建议"\]/); assert.match(css, /@media\s*\(prefers-reduced-motion:\s*reduce\)/); - assert.match(css, /@media\s*\(max-width:\s*430px\)[\s\S]*\.rectification-message-details/); + assert.doesNotMatch(css, /\.rectification-message-details/); assert.doesNotMatch(component, /确认放弃且不应用候选|本轮技术回执/); + assert.match(component, /onPointerMove=\{revealScrollbar\}/); + assert.match(component, /onScroll=\{revealScrollbar\}/); + assert.match(component, /classList\.remove\("is-scrollbar-visible"\)/); assert.match(component, /controller\.answer\(undefined, text\)/); assert.match(component, /event\.key === "Enter" && !event\.shiftKey/); assert.match(component, /onPendingChange/); @@ -802,8 +802,9 @@ test("real Chromium at 390px verifies layout, keyboard focus, streamlined contro ); await cdp.evaluate("globalThis.__rectificationHarness.setTurn('activeA1')"); await waitFor( - () => cdp?.evaluate(`document.body.textContent.includes('当前候选 05:18') - && document.body.textContent.includes('2021-07 · 开始第一份长期工作')`) ?? Promise.resolve(false), + () => cdp?.evaluate(`document.body.textContent.includes('当前判断') + && !document.body.textContent.includes('当前候选 05:18') + && !document.body.textContent.includes('已记录的经历')`) ?? Promise.resolve(false), "streamlined async initial turn", ); @@ -833,23 +834,6 @@ test("real Chromium at 390px verifies layout, keyboard focus, streamlined contro assert.equal(layout.selectCount, 0, "language-first flow should not render date selects"); assert.equal(layout.domainChoiceCount, 0, "language-first flow should not render domain buttons"); - await cdp.evaluate("document.querySelector('.rectification-message-details').open = true"); - await cdp.evaluate("document.querySelector('[aria-label^=\"更正这条经历\"]').click()"); - await waitFor( - () => cdp?.evaluate(`(() => { - const textarea = document.getElementById('conversational-rectification-answer'); - return document.body.textContent.includes('正在更正') - && document.body.textContent.includes('开始第一份长期工作') - && textarea?.value === ''; - })()`) ?? Promise.resolve(false), - "durable correction target selection without polluting the new answer", - ); - await cdp.evaluate("[...document.querySelectorAll('button')].find((button) => button.textContent.includes('取消更正')).click()"); - await waitFor( - () => cdp?.evaluate("!document.body.textContent.includes('正在更正') && document.getElementById('conversational-rectification-answer').value === ''") ?? Promise.resolve(false), - "correction cancellation", - ); - const mistakenAnswer = "2020年9月离职写错了"; await cdp.evaluate(`(() => { const textarea = document.getElementById('conversational-rectification-answer'); @@ -874,8 +858,9 @@ test("real Chromium at 390px verifies layout, keyboard focus, streamlined contro await waitFor( () => cdp?.evaluate(`(() => { const text = document.body.textContent; - return text.includes('当前候选') - && !text.includes('候选时间') + return text.includes('当前判断') + && !text.includes('当前候选 05:18') + && !text.includes('已记录的经历') && !text.includes('本轮技术回执') && !text.includes('暂停,稍后继续') && !text.includes('放弃本次校正') diff --git a/frontend/tests/conversational-visible-narrative.test.ts b/frontend/tests/conversational-visible-narrative.test.ts index ed705664..5b84c606 100644 --- a/frontend/tests/conversational-visible-narrative.test.ts +++ b/frontend/tests/conversational-visible-narrative.test.ts @@ -68,7 +68,7 @@ test("visible rectification copy does not replace a tailored Agent answer with a assert.match(narrative, /事业转折/); }); -test("the rectification chat renders the controller's alternating message history", () => { +test("the rectification chat renders alternating messages without a separate evidence panel", () => { const source = readFileSync( new URL("../src/components/conversational-birth-time-rectification.tsx", import.meta.url), "utf8", @@ -76,9 +76,5 @@ test("the rectification chat renders the controller's alternating message histor assert.match(source, /controller\.messages/); assert.match(source, /controller\.messages[\s\S]*?\.map\(\(message\)/); - const messageHistoryIndex = source.indexOf("controller.messages"); - const progressDetailsIndex = source.indexOf("rectification-progress-details"); - const evidenceRecapIndex = source.indexOf("turn.evidenceRecap.map"); - assert.ok(messageHistoryIndex >= 0 && messageHistoryIndex < progressDetailsIndex); - assert.ok(evidenceRecapIndex > progressDetailsIndex); + assert.doesNotMatch(source, /rectification-progress-details|turn\.evidenceRecap\.map|更正这条经历/); });