fix(rectification): offer times under the agent bubble
Independent Staging Quality Gate / validate (push) Successful in 11m52s
Independent Staging Quality Gate / publish (push) Successful in 10m10s

Keep the natal house table as a live snapshot outside chat, and show candidate times only after selection is allowed, attached to the latest settled reply.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-19 22:13:48 +08:00
co-authored by Cursor
parent 053e63effa
commit 4d872c68b2
5 changed files with 184 additions and 63 deletions
+18 -12
View File
@@ -1847,17 +1847,25 @@ 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-snapshot {
display: grid;
gap: var(--space-3);
margin: 8px 0 16px;
}
.rectification-candidates {
display: grid;
gap: 14px;
width: calc(100% - var(--assistant-content-inset));
margin: 8px 0 16px;
margin-inline-start: var(--assistant-content-inset);
margin: 0;
padding: 18px;
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
background: var(--color-canvas-soft);
}
.rectification-message-wrap .rectification-candidates {
width: calc(100% - var(--assistant-content-inset));
margin: var(--space-3) 0 var(--space-4);
margin-inline-start: var(--assistant-content-inset);
}
.rectification-candidates-heading { display: grid; gap: 5px; }
.rectification-candidates-heading strong { font-size: var(--type-title-sm); font-family: var(--font-display); font-weight: 600; letter-spacing: -.2px; }
.rectification-candidates-heading span,
@@ -1905,18 +1913,15 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
font-weight: 650;
}
.rectification-candidate-action { align-self: end; color: var(--color-action); font-size: 13px; font-weight: 650; }
.rectification-saved {
width: calc(100% - var(--assistant-content-inset));
margin: 8px 0 16px;
margin-inline-start: var(--assistant-content-inset);
color: var(--color-ink);
font-size: var(--type-body-sm);
}
.rectification-saved { margin: 8px 0 16px; color: var(--color-ink); font-size: var(--type-body-sm); }
.rectification-house-table {
display: grid;
gap: var(--space-3);
margin: 8px 0 16px;
padding-inline-start: var(--assistant-content-inset);
margin: 0;
padding: 18px;
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
background: var(--color-canvas-soft);
}
.rectification-house-table__heading { display: grid; gap: 5px; }
.rectification-house-table__heading strong {
@@ -1932,6 +1937,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
}
.rectification-house-table .markdown-table { margin: 0; }
@media (max-width: 640px) {
.rectification-house-table,
.rectification-candidates { padding: 14px; }
.rectification-candidate-list {
grid-template-columns: none;
@@ -54,6 +54,53 @@ type PersistedTurn = Readonly<{
type CandidateResult = RectificationCandidateResult | null;
function RectificationCandidateCards({
result,
acceptingCandidateId,
readonly,
onAccept,
}: Readonly<{
result: RectificationCandidateResult;
acceptingCandidateId: string | null;
readonly: boolean;
onAccept: (candidateId: string) => void;
}>) {
return (
<section className="rectification-candidates" aria-label="生时校正候选时间">
<div className="rectification-candidates-heading">
<strong>当前可能的出生时间</strong>
<span>这次校正的结果是采用一个代表性时间作当前排盘。相邻分钟目前分不开,还不能确认唯一分钟。可以先采用,也可以继续补充事件或改选;新增证据后,候选和相对支持度会重新计算。</span>
<span>相对支持度不是统计概率;采用不等于确认出生时间,也不会覆盖原始填报时间。</span>
</div>
<div className="rectification-candidate-list">
{result.candidates.map((candidate) => {
const selected = result.selectedTime === candidate.time;
const recommended = isRecommendedRectificationCandidate(result, candidate);
return (
<button
type="button"
className={`rectification-candidate${selected ? " is-selected" : ""}`}
key={candidate.candidateId}
disabled={selected || Boolean(acceptingCandidateId) || readonly}
onClick={() => onAccept(candidate.candidateId)}
>
<span className="rectification-candidate-time">
<strong>{candidate.time}</strong>
{selected && <span className="rectification-candidate-badge">已采用</span>}
{recommended && <span className="rectification-candidate-badge">当前推荐</span>}
</span>
<span className="rectification-candidate-support">相对支持度 {candidate.relativeSupport}</span>
<span className="rectification-candidate-action">
{selected ? "已采用" : acceptingCandidateId === candidate.candidateId ? "正在采用…" : result.selectedTime ? "改选为此时间" : "采用此时间"}
</span>
</button>
);
})}
</div>
</section>
);
}
type RectificationAgenticChatProps = Readonly<{
caseId: string;
sessionId: string;
@@ -194,7 +241,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
top: container.scrollHeight,
behavior: busy || reduceMotion ? "auto" : "smooth",
});
}, [busy, error, messages, savedTime]);
}, [busy, candidateResult, error, messages, savedTime]);
// Candidate snapshot comes from the persisted Candidate Snapshot API, never
// from parsing agent text or hidden sentinels.
@@ -498,6 +545,20 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
&& Boolean(message.turnId)
&& Boolean(message.text)
))?.renderKey;
const latestOfferMessageKey = [...messages]
.reverse()
.find((message) => (
message.role === "assistant"
&& message.state === "settled"
&& !message.failed
&& Boolean(message.text)
))?.renderKey;
const showSelectionCards = Boolean(
candidateResult?.selectionAllowed
&& latestOfferMessageKey
&& !busy
&& regeneratingMessageKey === null,
);
const canSend = !busy && !readonly && !regeneratingMessageKey;
return (
@@ -552,45 +613,21 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
onRegenerate={() => void regenerateMessage(message)}
/>
)}
{showSelectionCards && message.renderKey === latestOfferMessageKey && candidateResult && (
<RectificationCandidateCards
result={candidateResult}
acceptingCandidateId={acceptingCandidateId}
readonly={readonly}
onAccept={(candidateId) => void acceptCandidate(candidateId)}
/>
)}
</div>
);
})}
{candidateResult?.houseTable && (
<RectificationHouseTableView table={candidateResult.houseTable} />
)}
{candidateResult?.selectionAllowed && (
<section className="rectification-candidates" aria-label="生时校正候选时间">
<div className="rectification-candidates-heading">
<strong>当前可能的出生时间</strong>
<span>这次校正的结果是采用一个代表性时间作当前排盘。相邻分钟目前分不开,还不能确认唯一分钟。可以先采用,也可以继续补充事件或改选;新增证据后,候选和相对支持度会重新计算。</span>
<span>相对支持度不是统计概率;采用不等于确认出生时间,也不会覆盖原始填报时间。</span>
</div>
<div className="rectification-candidate-list">
{candidateResult.candidates.map((candidate) => {
const selected = candidateResult.selectedTime === candidate.time;
const recommended = isRecommendedRectificationCandidate(candidateResult, candidate);
return (
<button
type="button"
className={`rectification-candidate${selected ? " is-selected" : ""}`}
key={candidate.candidateId}
disabled={selected || Boolean(acceptingCandidateId) || readonly}
onClick={() => void acceptCandidate(candidate.candidateId)}
>
<span className="rectification-candidate-time">
<strong>{candidate.time}</strong>
{selected && <span className="rectification-candidate-badge">已采用</span>}
{recommended && <span className="rectification-candidate-badge">当前推荐</span>}
</span>
<span className="rectification-candidate-support">相对支持度 {candidate.relativeSupport}</span>
<span className="rectification-candidate-action">
{selected ? "已采用" : acceptingCandidateId === candidate.candidateId ? "正在采用…" : candidateResult.selectedTime ? "改选为此时间" : "采用此时间"}
</span>
</button>
);
})}
</div>
</section>
<div className="rectification-snapshot">
<RectificationHouseTableView table={candidateResult.houseTable} />
</div>
)}
{savedTime && (
<p className="rectification-saved" role="status">
@@ -6,12 +6,13 @@ export function RectificationHouseTableView({
table,
}: Readonly<{ table: RectificationHouseTable }>) {
return (
<section className="rectification-house-table" aria-label="本命宫位表">
<section className="rectification-house-table" aria-label="当前本命宫位" aria-live="polite">
<div className="rectification-house-table__heading">
<strong>本命宫位</strong>
<strong>当前本命宫位</strong>
<span>
按 {table.time} 排出,上升{table.lagna}。这是当前代表性时间下的盘面,不是已确认出生盘。
按 {table.time} 排出,上升{table.lagna}。补充经历后会按新线索重算。
</span>
<span>这是当前代表性时间下的盘面,不是已确认出生盘。</span>
</div>
<div className="markdown-table">
<table>