fix(rectification): keep collect chips, timeline, and adopt cards aligned
Independent Staging Quality Gate / validate (push) Failing after 9m58s
Independent Staging Quality Gate / publish (push) Has been skipped

Move 没有/记不清 under the spoken question, inset the timeline to the assistant column, and keep the three-column card after 更像这个 with the closing line under it (BUG-618–620).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-09 18:10:02 +08:00
co-authored by Cursor
parent 584a56bd81
commit ca921c3f2a
16 changed files with 329 additions and 71 deletions
@@ -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}
@@ -84,7 +84,7 @@ export function RectificationRangeDelivery({
type="button"
className="rectification-range-delivery__accept"
aria-pressed={adopted}
disabled={busy}
disabled={busy || adopted}
onClick={() => onAccept(column.candidate_id)}
>
{adopting