From e07d1bada4d313dd8aa188b616d9b529bb72d5ef Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Thu, 20 Aug 2026 14:24:05 +0800 Subject: [PATCH] fix(rectification): show natal houses and lagna changes in a live right rail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The board was live data sitting under chat, so each new event looked like nothing changed. Keep candidate cards in the transcript and group 换升 by minute for the side panel. Co-authored-by: Cursor --- docs/BUG_HISTORY.md | 24 +- frontend/src/app/globals.css | 225 +++++++++++++- frontend/src/app/page.tsx | 2 +- .../components/rectification-agentic-chat.tsx | 154 ++++------ .../src/components/rectification-board.tsx | 282 ++++++++++++++++++ .../components/rectification-house-table.tsx | 34 ++- .../v9/refinement-packet.ts | 30 ++ frontend/src/lib/rectification-board-model.ts | 177 +++++++++++ .../tests/rectification-agentic-entry.test.ts | 55 ++-- .../tests/rectification-board-model.test.ts | 132 ++++++++ 10 files changed, 985 insertions(+), 130 deletions(-) create mode 100644 frontend/src/components/rectification-board.tsx create mode 100644 frontend/src/lib/rectification-board-model.ts create mode 100644 frontend/tests/rectification-board-model.test.ts diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 20ada462..46d26772 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -4678,8 +4678,8 @@ - 根因:表来自 Case 快照、挂在消息列表末尾,却按聊天气泡的起始线排版,标题也只说“本命宫位”。 - 修复:宫位表单独放在消息列表后的 `rectification-snapshot`,表面与结果卡同类。标题改为“当前本命宫位”,并写明补充经历后会按新线索重算。时间选择卡不再进这个快照,见 BUG-312。 - 验证:`frontend/tests/rectification-agentic-entry.test.ts` 锁定快照容器在 `ChatMessageRow` 之外、不含候选卡、`aria-live`、重算文案,以及成功回合后 `loadCandidate`。 -- 防复发:宫位表必须在消息气泡外;文案必须说明随新线索重算。列对齐用 snapshot 容器的 `--assistant-content-inset`,不得再用表上的 `padding-inline-start` 把它扮成 Agent 正文。 -- 相关记录:BUG-307、BUG-310、BUG-312 +- 防复发:宫位表必须在消息气泡外,并作为聊天右侧工作台原地刷新,不得再挂在消息列表底部。文案必须说明随新线索重算。候选卡仍用 `--assistant-content-inset` 对齐 Agent 正文;宫位表不再用 inset 冒充气泡。 +- 相关记录:BUG-307、BUG-310、BUG-312、BUG-322 - 复发自:BUG-310(对齐 Agent 正文后更像一条聊天) - 修复版本:`4d872c68` @@ -4726,8 +4726,8 @@ - 根因:宫位表改成消息外的 snapshot 卡后,容器不再吃 `--assistant-content-inset`,仍按整列 `message-list` 铺宽;Agent 正文在头像加 gap 之后。 - 修复:snapshot 与候选卡同一列:`width: calc(100% - var(--assistant-content-inset))`,`margin-inline-start` 用同一 inset。表本身保持卡片描边和内边距,不用 `padding-inline-start` 冒充气泡。 - 验证:`frontend/tests/rectification-agentic-entry.test.ts` 锁定 snapshot 与候选卡共用 inset 宽度和起始线,且 `.rectification-house-table` 不写 inset。 -- 防复发:宫位表左缘和宽度必须跟 Agent 正文列一致;inset 只加在 snapshot 容器上。 -- 相关记录:BUG-310、BUG-311 +- 防复发:宫位表在右侧工作台内,不再与 Agent 正文同列对齐。候选时间卡仍用 `--assistant-content-inset` 对齐气泡。不得把工作台再缩进成消息列。 +- 相关记录:BUG-310、BUG-311、BUG-322 - 复发自:BUG-311(去掉消息缩进后未收回正文列) - 修复版本:`a732ff4b` @@ -4842,3 +4842,19 @@ - 相关记录:BUG-309、BUG-315、BUG-320 - 复发自:BUG-309(validate 跳过 `next build`;日志末尾 Import traces 掩盖类型检查失败) - 修复版本:0eebf971 + +## BUG-322 | 生时纠正宫位表和换升观察仍埋在消息列表底部 + +- 状态:resolved +- 首次发现:2026-08-20 +- 最近更新:2026-08-20 +- 影响面:生时纠正对话布局、`rectification-snapshot`、换升时刻 +- 用户现象:本命宫位和换升时刻跟在消息列表下面。每次补充经历后表会重算,但要滚过整段对话才能看见变化。 +- 触发条件:校正已有 Candidate Snapshot,继续在对话里补充经历。 +- 根因:BUG-311 把宫位表移出气泡,但仍放在 `.message-list` 末尾。换升时刻按图层一行一条平铺,不适合侧栏。 +- 修复:生时纠正单独做成聊天 | 工作台分栏。宫位表、按分钟合并的换升、确认门和大运对照放在右侧并原地刷新。候选时间卡和“用这个时间看盘”仍留在对话里。窄屏用“当前盘面”打开对话框,不挤第二列。普通咨询 session 不改。 +- 验证:`frontend/tests/rectification-agentic-entry.test.ts`;`frontend/tests/rectification-board-model.test.ts`。 +- 防复发:`rectification-snapshot` 不得回到消息循环。换升不得再按 `user_meaning` 一层一行平铺。`.conversation:not(.is-rectification)` 的 session 列宽不得套到校正对话。 +- 相关记录:BUG-311、BUG-312、BUG-314 +- 复发自:BUG-311(移出气泡后仍挂在消息列表底部) +- 修复版本:待提交 diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index f3e2e7bd..1f027751 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -474,6 +474,7 @@ button:disabled { cursor: default; opacity: .45; } } .chat-panel { height: 100%; min-width: 0; min-height: 0; overflow: hidden; display: grid; background: var(--color-canvas); grid-template-rows: 68px minmax(0, 1fr) auto; } +.chat-panel.is-rectification { grid-template-rows: 68px minmax(0, 1fr); } .chat-header { z-index: 2; min-width: 0; display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 20px; border-bottom: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent); padding: 0 var(--space-8); background: var(--color-frosted); backdrop-filter: saturate(130%) blur(20px); text-align: left; } .chat-header > div { min-width: 0; } .chat-header strong { max-width: min(560px, 62vw); overflow: hidden; line-height: 1.35; text-overflow: ellipsis; white-space: nowrap; font-family: var(--font-display); font-size: var(--type-title-md); font-weight: 400; letter-spacing: -.3px; } @@ -1069,6 +1070,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class .sidebar-scrim { position: fixed; z-index: 11; inset: 0; display: block; border: 0; background: var(--color-scrim); opacity: 1; visibility: visible; } [data-sidebar="rail"] { display: none; } .chat-panel { grid-template-rows: calc(64px + env(safe-area-inset-top)) minmax(0, 1fr) auto; } + .chat-panel.is-rectification { grid-template-rows: calc(64px + env(safe-area-inset-top)) minmax(0, 1fr); } .chat-header { gap: 10px; padding: max(0px, env(safe-area-inset-top)) var(--space-3) 0; } .conversation.is-empty { display: block; @@ -1122,6 +1124,17 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class .paywall-footer > a, .paywall-footer > button { width: 100%; } .dialog-actions { flex-direction: column-reverse; } .dialog-actions > button { width: 100%; } + dialog.rectification-board { + width: 100%; + height: min(86dvh, 100%); + margin-block-start: auto; + margin-inline: 0; + border-inline-start: 0; + border-block-start: 1px solid var(--color-border); + border-start-start-radius: var(--radius-lg); + border-start-end-radius: var(--radius-lg); + padding-bottom: env(safe-area-inset-bottom); + } .auth-story { min-height: 0; padding: var(--space-5); } .auth-story h2 { font-size: var(--type-title-lg); } .auth-panel { padding: var(--space-6) var(--space-5) max(var(--space-6), env(safe-area-inset-bottom)); } @@ -1847,12 +1860,218 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class .birth-time-clock-menu.select-content { width: 108px; min-width: 108px; } .birth-time-clock-menu .select-item { justify-content: flex-start; } +.rectification-workspace { + min-width: 0; + min-height: 0; + height: 100%; + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(18rem, 22.5rem); + background: var(--color-canvas); +} +.rectification-workspace.is-compact { + grid-template-columns: minmax(0, 1fr); +} +.rectification-workspace__chat { + min-width: 0; + min-height: 0; + display: grid; + grid-template-rows: minmax(0, 1fr) auto; +} +.rectification-workspace__chat .conversation { + padding-bottom: var(--space-4); +} +.rectification-workspace__chat .message-list { + width: 100%; + max-width: 720px; +} +.rectification-workspace__chat .composer-wrap { + position: relative; + z-index: 2; +} +.rectification-board { + min-width: 0; + min-height: 0; + display: grid; + grid-template-rows: auto minmax(0, 1fr); + border-inline-start: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent); + background: var(--color-canvas-soft); + color: var(--color-ink); +} +dialog.rectification-board { + width: min(22.5rem, 100%); + height: 100dvh; + max-width: none; + max-height: none; + margin: 0; + margin-inline-start: auto; + padding: 0; + border: 0; + border-inline-start: 1px solid var(--color-border); + background: var(--color-canvas-soft); + color: var(--color-ink); +} +dialog.rectification-board::backdrop { + background: var(--color-scrim); +} +.rectification-board__header { + position: sticky; + top: 0; + z-index: 1; + display: grid; + gap: var(--space-2); + padding: var(--space-4) var(--space-4) var(--space-3); + background: var(--color-canvas-soft); +} +.rectification-board__title-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--space-2) var(--space-3); +} +.rectification-board__header h2 { + margin: 0; + font-family: var(--font-display); + font-size: var(--type-title-sm); + font-weight: 600; + letter-spacing: -.2px; +} +.rectification-board__clock { + color: var(--color-ink); + font-variant-numeric: tabular-nums; + font-size: var(--type-body-sm); + font-weight: 600; +} +.rectification-board__close { + min-height: 44px; + margin-inline-start: auto; + padding: 0 var(--space-3); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + background: var(--color-canvas); + color: var(--color-ink); + cursor: pointer; +} +.rectification-board__close:focus-visible, +.rectification-board-peek:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 2px; +} +.rectification-board__stage, +.rectification-board__empty, +.rectification-board__disclosure { + margin: 0; + color: var(--color-ink-secondary); + font-size: var(--type-body-sm); + line-height: 1.55; +} +.rectification-board__body { + min-height: 0; + display: grid; + align-content: start; + gap: var(--space-6); + overflow-y: auto; + overscroll-behavior: contain; + padding: 0 var(--space-4) var(--space-6); +} +.rectification-board .rectification-snapshot { + display: grid; + gap: var(--space-4); + width: auto; + margin: 0; +} +.rectification-board .rectification-house-table { + padding: 0; + border: 0; + border-radius: 0; + background: transparent; +} +.rectification-board .technique-audit { + margin-top: 0; + padding-top: 0; + border-top: 0; +} +.rectification-board__section { + display: grid; + gap: var(--space-3); +} +.rectification-board__section h3 { + margin: 0; + font-family: var(--font-display); + font-size: var(--type-title-sm); + font-weight: 600; + letter-spacing: -.2px; +} +.rectification-board__minutes { + display: grid; + gap: var(--space-3); + margin: 0; + padding: 0; + list-style: none; +} +.rectification-board__minute { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 6px 10px; +} +.rectification-board__minute time { + font-variant-numeric: tabular-nums; + font-weight: 600; +} +.rectification-board__minute-note { + color: var(--color-ink-secondary); + font-size: var(--type-caption); +} +.rectification-board__chips { + display: flex; + flex-wrap: wrap; + gap: 6px; + flex: 1 1 100%; + margin: 0; + padding: 0; + list-style: none; +} +.rectification-board__chip { + display: inline-flex; + align-items: center; + min-height: 24px; + padding: 0 8px; + border-radius: 999px; + background: var(--color-canvas); + color: var(--color-ink); + font-size: var(--type-caption); +} +.rectification-board-peek { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-3); + width: 100%; + min-height: 44px; + margin: 0 0 var(--space-2); + padding: var(--space-2) var(--space-4); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + background: var(--color-canvas-soft); + color: var(--color-ink); + text-align: start; + cursor: pointer; +} +.rectification-board-peek strong { font-size: var(--type-body-sm); } +.rectification-board-peek span { + color: var(--color-ink-secondary); + font-size: var(--type-caption); +} +.rectification-house-table__changed { + display: inline; + margin-inline-start: .4em; + color: var(--color-ink-secondary); + font-size: var(--type-caption); +} + .rectification-snapshot { display: grid; gap: var(--space-3); - width: calc(100% - var(--assistant-content-inset)); - margin: 8px 0 16px; - margin-inline-start: var(--assistant-content-inset); } .rectification-candidates { display: grid; diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 8c6cd2f5..ae7792f4 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -3552,7 +3552,7 @@ export default function Home() { ) : null} - +
diff --git a/frontend/src/components/rectification-agentic-chat.tsx b/frontend/src/components/rectification-agentic-chat.tsx index cb4df9bb..10717cf0 100644 --- a/frontend/src/components/rectification-agentic-chat.tsx +++ b/frontend/src/components/rectification-agentic-chat.tsx @@ -1,7 +1,7 @@ "use client"; import { ArrowUp } from "lucide-react"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from "react"; import { parseAgentReply } from "@/lib/agent-reply"; import type { ChatMessage, ChatMessageView } from "@/lib/chat-message-view"; import { @@ -17,6 +17,10 @@ import { workingRectificationHouseTable, type RectificationCandidateResult, } from "@/lib/rectification-candidate-result"; +import { + diffRectificationBoard, + RECTIFICATION_BOARD_SPLIT_MIN_PX, +} from "@/lib/rectification-board-model"; import { membershipHref } from "@/lib/membership"; import { vargaSentenceFromMethods } from "@/lib/rectification-varga-sentence"; import { @@ -30,8 +34,7 @@ import { toggleChatMessageFeedback, } from "./chat-message-actions"; import { ModelSelector } from "./model-selector"; -import { RectificationHouseTableView } from "./rectification-house-table"; -import { TechniqueAuditDisclosure, ConfirmationGateDisclosure } from "./technique-audit-disclosure"; +import { RectificationBoard, RectificationBoardPeek } from "./rectification-board"; import { Button } from "./ui/button"; import { Textarea } from "./ui/textarea"; @@ -104,75 +107,6 @@ function RectificationCandidateCards({ ); } -function RectificationRefinementPanel({ - result, -}: Readonly<{ result: RectificationCandidateResult }>) { - const transitions = result.windowTransitions; - const ledger = result.eventDashaLedger; - const agreement = result.dashaAgreement; - const contrast = result.lagnaContrast; - const nakshatra = result.nakshatraBoundary; - const stage = result.precisionStage; - if ( - transitions.length === 0 - && ledger.length === 0 - && !agreement - && !contrast - && !nakshatra?.near_boundary - && !stage - ) return null; - return ( -
- {stage && ( -

{stage.user_meaning}

- )} - {transitions.length > 0 && ( -
- 换升时刻 -
    - {transitions.map((item) => ( -
  • {item.user_meaning}
  • - ))} -
-
- )} - {contrast && ( -
- 两段本命上升 -

{contrast.user_meaning}

-
    - {contrast.intervals.map((interval) => ( -
  • - {interval.start}-{interval.end}:{interval.lagna} - {interval.lords.l1 ? `;一/四/七/十宫主 ${[interval.lords.l1, interval.lords.l4, interval.lords.l7, interval.lords.l10].filter(Boolean).join("、")}` : ""} -
  • - ))} -
-
- )} - {ledger.length > 0 && ( -
- 经历与大运对照 -
    - {ledger.map((item) => ( -
  • {item.user_meaning}
  • - ))} -
-
- )} - {agreement && ( -

{agreement.user_meaning}

- )} - {nakshatra?.near_boundary && nakshatra.user_meaning && ( -
- 交界节奏 -

{nakshatra.user_meaning}

-
- )} -
- ); -} - function RectificationOosPanel({ prompts, }: Readonly<{ prompts: RectificationCandidateResult["oosBlindPrompts"] }>) { @@ -319,9 +253,37 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { const [copiedMessageKey, setCopiedMessageKey] = useState(null); const [regeneratingMessageKey, setRegeneratingMessageKey] = useState(null); const conversation = useRef(null); + const workspace = useRef(null); const composer = useRef(null); const keyCounter = useRef(0); const openingStarted = useRef(false); + const previousBoardResult = useRef(null); + const [compactBoard, setCompactBoard] = useState(false); + const [boardOpen, setBoardOpen] = useState(false); + const [boardDiff, setBoardDiff] = useState(() => diffRectificationBoard(null, null)); + const boardId = useId(); + const boardTitleId = useId(); + + useLayoutEffect(() => { + const node = workspace.current; + if (!node) return; + const update = () => { + const nextCompact = node.clientWidth < RECTIFICATION_BOARD_SPLIT_MIN_PX; + setCompactBoard(nextCompact); + if (!nextCompact) setBoardOpen(false); + }; + update(); + const observer = new ResizeObserver(update); + observer.observe(node); + return () => observer.disconnect(); + }, []); + + useEffect(() => { + setBoardDiff(diffRectificationBoard(previousBoardResult.current, candidateResult)); + previousBoardResult.current = candidateResult; + }, [candidateResult]); + + const closeBoard = useCallback(() => setBoardOpen(false), []); const setPending = useCallback((value: boolean) => { setBusy(value); @@ -684,9 +646,13 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { const canSend = !busy && !readonly && !regeneratingMessageKey; return ( - <> -
-
+
+
+
+
{pendingConsultationQuestion?.trim() && (

先陪你核对出生时间范围,之后会回到你原来的问题:“{pendingConsultationQuestion.trim()}” @@ -746,19 +712,6 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {

); })} - {candidateResult?.houseTable && ( -
- {candidateResult.natalRecast && ( -

{candidateResult.natalRecast.user_meaning}

- )} - - - {savedStatus === "accepted" && candidateResult.techniqueAudit.length > 0 && ( - - )} -
- )} - {candidateResult && } {savedTime && (

@@ -786,8 +739,16 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {

-
-
+
+ {compactBoard && ( + setBoardOpen(true)} + /> + )} +