diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index a61e6b05..4ae0bd5f 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -5547,6 +5547,22 @@ - 复发自:BUG-379(邻近年学业存在性探针已跳过,但仍用整窗观察和剩余 D24 题干继续问入学/高考) - 修复版本:待发布 +## BUG-381 | 生时纠正进度条与点选卡重叠、分盘名消失 + +- 状态:resolved +- 首次发现:2026-08-25 +- 最近更新:2026-08-25 +- 影响面:生时纠正聊天气泡、阶段进度、点选卡、「回到最新」 +- 用户现象:有工具进度的回复看起来比上一句更往里缩;阶段和正文之间空一大截;进度条只剩「读取校正记录 / 整理证据」,看不到 D 盘对照;点选卡最下面的 D 和「先这样」被「回到最新」挡住。 +- 触发条件:本轮有公开工具进度;证据写入触发了重算但比较工具没单独出场;用户略微离开底部看点选卡。 +- 根因:(1) 有进度又有正文时套了咨询页 `consultation-thinking-report`,格子间距 24px,进度条自己还有下边距。(2) 已落盘回合不回放工具进度,最新一轮有勾选列、上一轮没有,看起来像缩进。(3) 证据批处理把 `executed_methods` 放在 `rescore` 里,公开活动只读顶层字段,技法句又写在口语后面。(4) 「回到最新」贴在输入框上方居中,离开底部 96px 就出现,正好盖住点选卡下沿。 +- 修复:进度和正文改用 8px 紧凑叠放。已落盘回执回放工具步骤。证据重算的分盘名并进进度条,不写进口语。点选卡仍在视口下沿时不显示「回到最新」,按钮改到末行外侧。不改 Skill `10.0.11`。 +- 验证:`frontend/tests/chat-stream-layout.test.ts`、`frontend/tests/rectification-agentic-entry.test.ts`、`frontend/tests/rectification-answer-choice.test.ts`、`frontend/tests/rectification-v9-stream.test.ts`、`frontend/tests/agent-activity-progress.test.ts`。 +- 防复发:有进度的纠正气泡不得用咨询页 24px 报告间距。技法句属于进度条,不得再进 `answer.delta`。点选卡仍可见时不得用居中浮层挡住 D / 「先这样」。 +- 相关记录:BUG-373、BUG-376、BUG-377 +- 复发自:BUG-376(关掉思考正文后,进度条只剩工具名,技法句不再出现;回到最新仍按咨询页浮层) +- 修复版本:待发布 + ## BUG-379 | 生时纠正已记入学后仍编造高考年并再问入学 - 状态:resolved diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 33412db1..558c6072 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -333,6 +333,17 @@ button:disabled { cursor: default; opacity: .45; } } .agent-activity-status + .message-answer, .agent-thinking-panel + .message-answer { margin-top: var(--space-2); } +.message-stage-and-answer { + display: grid; + gap: var(--space-2); +} +.message-stage-and-answer .agent-thinking-panel, +.message-stage-and-answer .agent-activity-status { + margin-bottom: 0; +} +.message-stage-and-answer .consultation-report-analysis .message-answer { + margin-top: 0; +} .composer textarea::placeholder { color: var(--color-ink-tertiary); } .composer button svg { width: 19px; height: 19px; } .dialog-close svg { width: 19px; height: 19px; } @@ -2366,11 +2377,24 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class grid-template-rows: minmax(0, 1fr) auto; } .rectification-workspace__chat .conversation { + --rectification-jump-clearance: calc(44px + var(--space-3)); padding-bottom: var(--space-4); } .rectification-workspace__chat .message-list { width: 100%; max-width: 720px; + padding-bottom: var(--rectification-jump-clearance); +} +.conversation.is-rectification .message-assistant .agent-thinking-step { + grid-template-columns: 14px minmax(0, 1fr); + gap: 6px; +} +.conversation.is-rectification .message-assistant .agent-thinking-marker { + width: 14px; + height: 14px; +} +.conversation.is-rectification .message-actions { + margin-inline-start: var(--assistant-content-inset); } .rectification-workspace__chat .composer-wrap { position: relative; @@ -2382,8 +2406,9 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class bottom: 100%; z-index: 3; display: flex; - justify-content: center; + justify-content: flex-end; padding-bottom: var(--space-2); + padding-inline-end: var(--space-4); pointer-events: none; } .rectification-jump-latest button { diff --git a/frontend/src/components/agent-activity-status.tsx b/frontend/src/components/agent-activity-status.tsx index ef548185..eba9b608 100644 --- a/frontend/src/components/agent-activity-status.tsx +++ b/frontend/src/components/agent-activity-status.tsx @@ -66,6 +66,17 @@ function MessageThinkingTrace({ ); } +function VargaTraceStep({ sentence }: Readonly<{ sentence: string }>) { + return ( +
  • + + {sentence} +
  • + ); +} + export function AgentActivityStatus({ state, label = labels[state], @@ -73,6 +84,7 @@ export function AgentActivityStatus({ completedTrail, thinkingText, activityTrace, + vargaSentence, hasAnswer = false, showLive = true, }: Readonly<{ @@ -82,12 +94,15 @@ export function AgentActivityStatus({ completedTrail?: string; thinkingText?: string; activityTrace?: readonly AgentActivityTraceItem[]; + vargaSentence?: string | null; hasAnswer?: boolean; showLive?: boolean; }>) { const completedSteps = activityCompletedSteps(completedTrail); const live = showLive && !hasAnswer; const trace = activityTrace ?? []; + const varga = vargaSentence?.trim() ?? ""; + const showVarga = Boolean(varga) && !trace.some((item) => item.label === varga); if (trace.length > 0) { return (
    @@ -100,15 +115,16 @@ export function AgentActivityStatus({ hasAnswer={hasAnswer} /> ))} + {showVarga ? : null}
    ); } - if (!live && completedSteps.length === 0 && !thinkingText?.trim()) return null; + if (!live && completedSteps.length === 0 && !thinkingText?.trim() && !showVarga) return null; return (
    - {(live || completedSteps.length > 0) && ( + {(live || completedSteps.length > 0 || showVarga) && (
      {completedSteps.map((step) => (
    1. @@ -129,6 +145,7 @@ export function AgentActivityStatus({
    2. ) : null} + {showVarga ? : null}
    )} {thinkingText ? : null} diff --git a/frontend/src/components/chat-message-row.tsx b/frontend/src/components/chat-message-row.tsx index fec5ae32..03d0a1c2 100644 --- a/frontend/src/components/chat-message-row.tsx +++ b/frontend/src/components/chat-message-row.tsx @@ -104,6 +104,7 @@ export function ChatMessageRow({ completedTrail={message.activity?.completedTrail} thinkingText={hasTrace ? undefined : message.thinkingText} activityTrace={message.activityTrace} + vargaSentence={vargaSentence} hasAnswer={hasAnswer} showLive={showLiveActivity} /> @@ -114,7 +115,7 @@ export function ChatMessageRow({ ) : null; @@ -157,7 +158,7 @@ export function ChatMessageRow({ /> )} {stackedThinkingAndAnswer ? ( -
    +
    {thinkingPanel}
    {spokenAnswer} diff --git a/frontend/src/components/rectification-agentic-chat.tsx b/frontend/src/components/rectification-agentic-chat.tsx index 7b198f6d..3eb2699f 100644 --- a/frontend/src/components/rectification-agentic-chat.tsx +++ b/frontend/src/components/rectification-agentic-chat.tsx @@ -17,6 +17,7 @@ import { RECTIFICATION_ACTIVITY_PROGRESS_LABELS, RECTIFICATION_TOOL_DONE_LABELS, RECTIFICATION_TOOL_PROGRESS_LABELS, + activityTraceFromReceipt, rectificationCompletedTrail, rectificationToolActivityPhase, } from "@/lib/rectification-activity-labels"; @@ -45,7 +46,7 @@ import { isPublicRectificationTool, } from "@/lib/rectification-agentic/v9/public-receipt"; import { userFacingRunFailure, isIncompleteRunBanner } from "@/lib/rectification-agentic/v9/run-diagnostic"; -import { isNearBottom } from "@/lib/rectification-sticky-scroll"; +import { isNearBottom, shouldShowJumpToLatest } from "@/lib/rectification-sticky-scroll"; import { CHOICE_ACTION, STOP_ACTION, @@ -226,12 +227,14 @@ function messagesFromTurns(initialTurns: readonly PersistedTurn[]): RenderMessag if (failed && !raw) return []; if (isIncompleteRunBanner(raw)) return []; const split = raw ? finalizeRectificationSpokenAndThinking(raw) : { thinking: "", spoken: raw }; + const completedReceipt = completedReceiptFromPersisted(turn.receipt); return [{ role: "assistant", text: split.spoken, renderKey: key, state: turn.status === "completed" || failed ? "settled" : "thinking", - completedReceipt: completedReceiptFromPersisted(turn.receipt), + completedReceipt, + activityTrace: activityTraceFromReceipt(completedReceipt), failed, turnId: turn.id, }]; @@ -291,6 +294,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { const followTailRef = useRef(true); const scrollFrameRef = useRef(null); const choiceActionIds = useRef(new Map()); + const choiceCardsOpen = useRef(false); const [showJumpToLatest, setShowJumpToLatest] = useState(false); const [compactBoard, setCompactBoard] = useState(false); const [boardOpen, setBoardOpen] = useState(false); @@ -332,7 +336,12 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { if (!viewport) return; const nearBottom = isNearBottom(viewport.scrollHeight, viewport.scrollTop, viewport.clientHeight); followTailRef.current = nearBottom; - setShowJumpToLatest(!nearBottom); + setShowJumpToLatest(shouldShowJumpToLatest( + viewport.scrollHeight, + viewport.scrollTop, + viewport.clientHeight, + choiceCardsOpen.current, + )); }, []); const scrollToLatest = useCallback((behavior: ScrollBehavior = "smooth") => { @@ -974,6 +983,10 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { && !readonly && regeneratingMessageKey === null, ); + choiceCardsOpen.current = showChoiceCards; + useLayoutEffect(() => { + updateFollowState(); + }, [showChoiceCards, updateFollowState]); const showSelectionCards = Boolean( candidateResult?.selectionAllowed && offeredSelectionOnce @@ -1051,7 +1064,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { }), } : message; - const vargaSentence = message.state === "settled" && !message.failed + const vargaSentence = !message.failed ? vargaSentenceFromMethods(message.completedReceipt?.methods) : null; return ( diff --git a/frontend/src/lib/rectification-activity-labels.ts b/frontend/src/lib/rectification-activity-labels.ts index 2e89f12d..277fb2bb 100644 --- a/frontend/src/lib/rectification-activity-labels.ts +++ b/frontend/src/lib/rectification-activity-labels.ts @@ -1,5 +1,7 @@ +import type { AgentActivityTraceItem } from "./agent-activity-trace.ts"; import { activityCompletedTrail } from "./chat-message-view.ts"; import type { PublicActivityPhase } from "./consultation-agent-events.ts"; +import type { CompletedActivityReceiptView } from "./rectification-activity-receipt.ts"; import type { PublicRectificationActivity, PublicRectificationTool, @@ -62,6 +64,19 @@ export function rectificationCompletedTrail(steps: readonly PublicRectificationT return activityCompletedTrail(steps.map((tool) => RECTIFICATION_TOOL_DONE_LABELS[tool])); } +export function activityTraceFromReceipt( + receipt: CompletedActivityReceiptView | null | undefined, +): readonly AgentActivityTraceItem[] { + if (!receipt?.steps.length) return []; + return receipt.steps.map((tool) => ({ + id: `persisted-${tool}`, + kind: "activity" as const, + status: "done" as const, + label: RECTIFICATION_TOOL_DONE_LABELS[tool], + tool, + })); +} + export function rectificationToolActivityPhase(tool: PublicRectificationTool): PublicActivityPhase { if (LOAD_TOOLS.has(tool)) return "loading-method"; if (COMPARE_TOOLS.has(tool)) return "chart-calculation"; diff --git a/frontend/src/lib/rectification-agentic/v9/stream-mapping.ts b/frontend/src/lib/rectification-agentic/v9/stream-mapping.ts index 8b07d2a0..a3f8ac13 100644 --- a/frontend/src/lib/rectification-agentic/v9/stream-mapping.ts +++ b/frontend/src/lib/rectification-agentic/v9/stream-mapping.ts @@ -106,6 +106,23 @@ export function isPublicRectificationToolName(value: unknown): value is PublicRe return isPublicRectificationTool(value); } +function executedMethodsFromRecord(value: unknown): PublicRectificationMethod[] { + if (!value || typeof value !== "object") return []; + const record = value as Record; + const found: PublicRectificationMethod[] = []; + const add = (methods: unknown) => { + if (!Array.isArray(methods)) return; + for (const method of methods) { + if (isPublicRectificationMethod(method) && !found.includes(method)) found.push(method); + } + }; + add(record.executed_methods); + if (record.rescore && typeof record.rescore === "object") { + add((record.rescore as Record).executed_methods); + } + return found; +} + function resultMethods(chunk: AgentChunkType): PublicRectificationMethod[] { if (chunk.type !== "tool-result") return []; const payload = chunk.payload && typeof chunk.payload === "object" @@ -113,9 +130,8 @@ function resultMethods(chunk: AgentChunkType): PublicRectificationMethod[] { : {}; const candidates = [payload.result, payload.output, (chunk as unknown as { object?: unknown }).object]; for (const candidate of candidates) { - if (!candidate || typeof candidate !== "object") continue; - const methods = (candidate as Record).executed_methods; - if (Array.isArray(methods)) return [...new Set(methods.filter(isPublicRectificationMethod))]; + const methods = executedMethodsFromRecord(candidate); + if (methods.length > 0) return methods; } return []; } diff --git a/frontend/src/lib/rectification-sticky-scroll.ts b/frontend/src/lib/rectification-sticky-scroll.ts index 9bb63373..bd112109 100644 --- a/frontend/src/lib/rectification-sticky-scroll.ts +++ b/frontend/src/lib/rectification-sticky-scroll.ts @@ -6,6 +6,8 @@ */ export const RECTIFICATION_NEAR_BOTTOM_PX = 96; +/** Keep the jump chip hidden while a choice card still occupies the composer overlay band. */ +export const RECTIFICATION_CHOICE_NEAR_BOTTOM_PX = 360; export function distanceFromBottom( scrollHeight: number, @@ -24,6 +26,18 @@ export function isNearBottom( return distanceFromBottom(scrollHeight, scrollTop, clientHeight) <= thresholdPx; } +export function shouldShowJumpToLatest( + scrollHeight: number, + scrollTop: number, + clientHeight: number, + choiceCardOpen = false, +): boolean { + const distance = distanceFromBottom(scrollHeight, scrollTop, clientHeight); + if (distance <= RECTIFICATION_NEAR_BOTTOM_PX) return false; + if (choiceCardOpen && distance <= RECTIFICATION_CHOICE_NEAR_BOTTOM_PX) return false; + return true; +} + export function shouldFollowLatest(followTail: boolean): boolean { return followTail === true; } diff --git a/frontend/tests/agent-activity-progress.test.ts b/frontend/tests/agent-activity-progress.test.ts index 5fce86de..9523a161 100644 --- a/frontend/tests/agent-activity-progress.test.ts +++ b/frontend/tests/agent-activity-progress.test.ts @@ -14,6 +14,7 @@ import { } from "../src/lib/consultation-activity-labels.ts"; import { RECTIFICATION_TOOL_PROGRESS_LABELS, + activityTraceFromReceipt, rectificationCompletedTrail, rectificationToolActivityPhase, } from "../src/lib/rectification-activity-labels.ts"; @@ -96,6 +97,17 @@ test("live rectification labels name the actual public tool", () => { ); }); +test("persisted receipts rebuild the public activity steps without thinking text", () => { + assert.deepEqual( + activityTraceFromReceipt({ + steps: ["rectification-read-case", "rectification-record-evidence-batch"], + methods: ["d1-rashi", "d10-dashamsa"], + }).map((row) => `${row.kind}:${row.label}`), + ["activity:读取校正记录", "activity:整理多条事件证据"], + ); + assert.deepEqual(activityTraceFromReceipt({ steps: [], methods: ["d1-rashi"] }), []); +}); + test("multi-domain chart calculation names the current item without domain ids", () => { assert.equal(chartCalculationProgressLabel(1, 1), CONSULTATION_CHART_CALCULATION_LABEL); assert.equal(chartCalculationProgressLabel(2, 3), "正在计算本命盘(第 2/3 项)…"); diff --git a/frontend/tests/chat-stream-layout.test.ts b/frontend/tests/chat-stream-layout.test.ts index 547d5b3c..d1bae1ce 100644 --- a/frontend/tests/chat-stream-layout.test.ts +++ b/frontend/tests/chat-stream-layout.test.ts @@ -75,6 +75,10 @@ test("shows honest agent activity states before and during streamed text", () => assert.match(messageRowSource, /