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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user