merge origin/staging into report chart layout fix

Keep BUG-616/617 after 614/615 and before 618–620.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-09 18:46:18 +08:00
co-authored by Cursor
36 changed files with 1005 additions and 171 deletions
@@ -29,6 +29,7 @@ export type VedicChartModel = NorthIndianChartModel;
interface VedicChartSvgProps {
chart: NorthIndianChartModel;
ariaLabel: string;
highlightHouseNumbers?: ReadonlySet<number>;
}
function buildRetrogradeSet(chart: NorthIndianChartModel): Set<string> {
@@ -82,7 +83,7 @@ function OccupantStack({
);
}
export function VedicChartSvg({ chart, ariaLabel }: VedicChartSvgProps) {
export function VedicChartSvg({ chart, ariaLabel, highlightHouseNumbers }: VedicChartSvgProps) {
const chartId = useId().replace(/:/g, "");
const linesByHouse = new Map<number, string[]>();
for (const house of chart.houses) {
@@ -117,7 +118,7 @@ export function VedicChartSvg({ chart, ariaLabel }: VedicChartSvgProps) {
<g key={houseNumber}>
<polygon
points={polygonPoints(polygon)}
className="personal-report-chart-cell"
className={`personal-report-chart-cell${highlightHouseNumbers?.has(houseNumber) ? " is-changed" : ""}`}
/>
{ordinal !== null && (
<text
@@ -107,9 +107,12 @@ import {
applyLiveCandidateOffer,
copyTextForMessage,
interviewQuestionBlocksAdoptOffer,
nextSelectionCardLock,
parseTurnQuestion,
persistedOfferFromTurn,
questionIsAnswered,
resolveSelectionCardMessageKey,
type SelectionCardLock,
type TurnQuestion,
} from "@/lib/rectification-agentic/v9/turn-question";
import { Button } from "@/components/ui/button";
@@ -184,6 +187,33 @@ function RectificationReadonlyRange({
);
}
function CollectSpokenReplies({
onChoose,
}: Readonly<{
onChoose: (text: "没有" | "记不清") => void;
}>) {
return (
<div className="rectification-collect-replies" role="group" aria-label="采集题快捷回答">
<Button
type="button"
variant="outline"
className="rectification-collect-reply"
onClick={() => onChoose("没有")}
>
</Button>
<Button
type="button"
variant="outline"
className="rectification-collect-reply"
onClick={() => onChoose("记不清")}
>
</Button>
</div>
);
}
type RectificationAgenticChatProps = Readonly<{
caseId: string;
@@ -489,6 +519,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
const stepStartedAtRef = useRef(0);
const liveToolRef = useRef<string | null>(null);
const liveBaseLabelRef = useRef("正在处理…");
const selectionOfferLockRef = useRef<SelectionCardLock | null>(null);
const [compactBoard, setCompactBoard] = useState(false);
const [boardOpen, setBoardOpen] = useState(false);
const [boardDiff, setBoardDiff] = useState(() => diffRectificationBoard(null, null));
@@ -1425,12 +1456,29 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
stage: caseStage,
});
const persistedOfferKey = [...messages].reverse().find((message) => message.candidateOffer)?.renderKey;
const selectionCardMessageKey = persistedOfferKey
const liveSelectionCardKey = persistedOfferKey
?? (canOfferCards && !candidateResult?.selectedTime ? latestSettledAssistant?.renderKey : undefined);
selectionOfferLockRef.current = nextSelectionCardLock(selectionOfferLockRef.current, {
resultId: candidateResult?.resultId,
key: liveSelectionCardKey,
});
const selectionCardMessageKey = resolveSelectionCardMessageKey({
persistedOfferKey,
fallbackKey: latestSettledAssistant?.renderKey,
locked: selectionOfferLockRef.current,
resultId: candidateResult?.resultId,
selectedTime: candidateResult?.selectedTime,
canOffer: canOfferCards,
});
// Adopt already has in-card “正在采用…” / “已采用”; hiding the whole card
// for `busy` is what made the three columns vanish on 「更像这个」.
const keepSelectionCardsWhileBusy = Boolean(
acceptingCandidateId || candidateResult?.selectedTime,
);
const showSelectionCards = Boolean(
candidateResult
&& caseSnapshotLoaded
&& !busy
&& (!busy || keepSelectionCardsWhileBusy)
&& selectionCardMessageKey
&& (canOfferCards || Boolean(candidateResult.selectedTime))
&& !messages.some((message) => (
@@ -1472,6 +1520,12 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
retryAttempts: questionRetryAttempts,
retryLimit: RECTIFICATION_QUESTION_RETRY_LIMIT,
});
const verifiedIdleCopy = questionGap === "verified_idle"
? postAdoptVerifyDoneCopy(
savedTime,
messages.some((message) => message.question?.kind === "reverse_verify"),
)
: null;
const conversationState = rectificationConversationState({
messageCount: messages.length,
busy,
@@ -1642,6 +1696,9 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
onStop={submitStop}
/>
)}
{liveQuestion && question.kind === "collect_spoken" && (
<CollectSpokenReplies onChoose={(text) => void send("message", text)} />
)}
{question.status === "skipped" && (
<p className="rectification-question-skipped" role="status">
{savedTime ? `已跳过(已采用 ${savedTime}` : "已跳过"}
@@ -1678,12 +1735,19 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
/>
)}
{showSelectionCards && candidateResult && message.renderKey === selectionCardMessageKey && (
<RectificationRangeDelivery
result={candidateResult}
acceptingCandidateId={acceptingCandidateId}
readonly={readonly || busy || regeneratingMessageKey !== null}
onAccept={(candidateId) => void acceptCandidate(candidateId)}
/>
<>
<RectificationRangeDelivery
result={candidateResult}
acceptingCandidateId={acceptingCandidateId}
readonly={readonly || regeneratingMessageKey !== null || (busy && !acceptingCandidateId)}
onAccept={(candidateId) => void acceptCandidate(candidateId)}
/>
{verifiedIdleCopy && (
<p className="rectification-pending-note" role="status">
{verifiedIdleCopy}
</p>
)}
</>
)}
{showReadonlyRange && message.renderKey === latestSettledAssistant?.renderKey && candidateResult?.credibleRange && (
<RectificationReadonlyRange
@@ -1706,12 +1770,9 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
</Button>
</div>
)}
{questionGap === "verified_idle" && (
{questionGap === "verified_idle" && verifiedIdleCopy && !showSelectionCards && (
<p className="rectification-pending-note" role="status">
{postAdoptVerifyDoneCopy(
savedTime,
messages.some((message) => message.question?.kind === "reverse_verify"),
)}
{verifiedIdleCopy}
</p>
)}
{savedTime && savedStatus === "confirmed" && (
@@ -1733,26 +1794,6 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
{!conversationAnchor.anchored && (
<JumpToLatestButton onClick={conversationAnchor.anchorToLatest} />
)}
{collectSpokenPrompt && !busy && !readonly && regeneratingMessageKey === null ? (
<div className="rectification-collect-replies" role="group" aria-label="采集题快捷回答">
<Button
type="button"
variant="outline"
className="rectification-collect-reply"
onClick={() => void send("message", "没有")}
>
</Button>
<Button
type="button"
variant="outline"
className="rectification-collect-reply"
onClick={() => void send("message", "记不清")}
>
</Button>
</div>
) : null}
<ChatComposer
inputRef={composer}
value={draft}
@@ -4,6 +4,7 @@ import { X } from "lucide-react";
import { useEffect, useRef } from "react";
import {
groupWindowTransitions,
houseTableToNorthIndianChart,
rectificationBoardPeekCopy,
workingRectificationTime,
type RectificationBoardDiff,
@@ -18,6 +19,7 @@ import {
type WindowScanLayer,
} from "@/lib/rectification-agentic/v9/refinement-packet";
import { rectificationBoardEmptyCopy } from "@/lib/rectification-surface-state";
import { VedicChartSvg } from "@/components/personal-report/vedic-chart-svg";
import { RectificationHouseTableView } from "./rectification-house-table";
import { ConfirmationGateDisclosure, TechniqueAuditDisclosure } from "./technique-audit-disclosure";
@@ -100,11 +102,24 @@ function RectificationBoardBody({
)}
{table && (
<div className="rectification-snapshot">
<RectificationHouseTableView
table={table}
changedHouses={changedHouses}
lagnaChanged={diff.lagnaChanged}
/>
<figure className="rectification-board__chart">
<VedicChartSvg
chart={houseTableToNorthIndianChart(table)}
ariaLabel={`当前本命盘,上升${table.lagna}`}
highlightHouseNumbers={new Set([
...changedHouses,
...(diff.lagnaChanged ? [1] : []),
])}
/>
</figure>
<details className="technique-audit">
<summary></summary>
<RectificationHouseTableView
table={table}
changedHouses={changedHouses}
lagnaChanged={diff.lagnaChanged}
/>
</details>
{result && <ConfirmationGateDisclosure gate={result.confirmationGate} />}
{savedStatus === "accepted" && result && result.techniqueAudit.length > 0 && (
<TechniqueAuditDisclosure rows={result.techniqueAudit} />
@@ -112,8 +127,10 @@ function RectificationBoardBody({
</div>
)}
{(scoringMinutes.length > 0 || displayMinutes.length > 0) && (
<section className="rectification-board__section" aria-label="换升时刻">
<h3></h3>
<details className="technique-audit" aria-label="换升时刻">
<summary>
· {grouped.length}
</summary>
{scoringMinutes.length > 0 && (
<ol className="rectification-board__minutes">
{scoringMinutes.map((row) => {
@@ -157,7 +174,7 @@ function RectificationBoardBody({
</ol>
</details>
)}
</section>
</details>
)}
{contrast && (
<details className="technique-audit">
@@ -5,13 +5,14 @@ import {
RECTIFICATION_USER_COPY,
REPRESENTATIVE_MINUTE_DISCLAIMER,
rangeDeliveryEventCopy,
rangeDeliveryFitCopy,
rangeDeliveryWindowCopy,
rangeDeliveryWorstCopy,
} from "@/lib/rectification-agentic/user-copy";
import type { RangeDeliveryProjection } from "@/lib/rectification-agentic/v9/divergence-panel";
import { RectificationVerificationReport } from "@/components/rectification-verification-report";
function columnHasTraits(column: RangeDeliveryProjection["columns"][number]): boolean {
return Boolean(column.traits.d9 || column.traits.d10 || column.traits.nakshatra.length > 0);
}
export function RectificationRangeDelivery({
result,
acceptingCandidateId,
@@ -36,18 +37,25 @@ export function RectificationRangeDelivery({
: RECTIFICATION_USER_COPY.rangeDeliveryTitle;
const markdown = delivery?.verification_markdown
?? result.verificationReportMarkdown;
const sharedTraits = delivery?.shared_traits ?? [];
return (
<section className="rectification-candidates rectification-range-delivery" aria-label="生时校正区间">
<div className="rectification-candidates-heading">
<strong>{title}</strong>
</div>
{sharedTraits.length > 0 ? (
<ul className="rectification-range-delivery__shared">
{sharedTraits.map((line) => (
<li key={line}>{line}</li>
))}
</ul>
) : null}
<ul className="rectification-range-delivery__columns">
{columns.map((column) => {
const adopted = selected === column.time;
const adopting = acceptingCandidateId === column.candidate_id;
const worst = rangeDeliveryWorstCopy(column.fit.worst);
const windows = column.windows.slice(0, 2);
const missing = column.fit == null || column.windows == null;
return (
<li
key={column.candidate_id}
@@ -59,32 +67,30 @@ export function RectificationRangeDelivery({
{" "}
{column.probability_percent}%
</p>
<div className="rectification-range-delivery__block">
<h3>{RECTIFICATION_USER_COPY.rangeDeliveryTraitsHeading}</h3>
{column.traits.d9 ? <p>{column.traits.d9}</p> : null}
{column.traits.d10 ? <p>{column.traits.d10}</p> : null}
{column.traits.nakshatra.map((line) => (
<p key={line}>{line}</p>
))}
</div>
<div className="rectification-range-delivery__block">
<h3>{RECTIFICATION_USER_COPY.rangeDeliveryFitHeading}</h3>
<p>{rangeDeliveryFitCopy(column.fit)}</p>
{worst ? <p>{worst}</p> : null}
</div>
<div className="rectification-range-delivery__block">
<h3>{RECTIFICATION_USER_COPY.rangeDeliveryWindowsHeading}</h3>
{windows.length > 0
? windows.map((window) => (
<p key={`${window.domain}-${window.from}`}>{rangeDeliveryWindowCopy(window)}</p>
))
: <p>{RECTIFICATION_USER_COPY.rangeDeliveryNoWindow}</p>}
</div>
{columnHasTraits(column) ? (
<div className="rectification-range-delivery__block">
{column.traits.d9 ? <p>{column.traits.d9}</p> : null}
{column.traits.d10 ? <p>{column.traits.d10}</p> : null}
{column.traits.nakshatra.map((line) => (
<p key={line}>{line}</p>
))}
</div>
) : null}
{missing ? (
<p className="rectification-range-delivery__muted">
{RECTIFICATION_USER_COPY.rangeDeliveryNotCompared}
</p>
) : (
<>
<p>{column.fit_line}</p>
<p>{column.window_line}</p>
</>
)}
<button
type="button"
className="rectification-range-delivery__accept"
aria-pressed={adopted}
disabled={busy}
disabled={busy || adopted}
onClick={() => onAccept(column.candidate_id)}
>
{adopting