fix(rectification): ask tie-break questions before the range card (BUG-685/686/687)
Close leads hold delivery until unused D9/D10 style questions are asked. Tie-break POST merges the new turn into the transcript. Range copy no longer lists declined lines.
This commit is contained in:
@@ -120,13 +120,17 @@ import {
|
||||
interviewQuestionBlocksAdoptOffer,
|
||||
nextSelectionCardLock,
|
||||
parseTurnQuestion,
|
||||
persistedOfferFromTurn,
|
||||
questionIsAnswered,
|
||||
resolveSelectionCardMessageKey,
|
||||
type SelectionCardLock,
|
||||
type TurnQuestion,
|
||||
} from "@/lib/rectification-agentic/v9/turn-question";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
appendUnseenAssistantTurns,
|
||||
applySnapshotTurnsToMessages,
|
||||
mergeTurnQuestions,
|
||||
} from "@/lib/rectification-snapshot-messages";
|
||||
|
||||
type PersistedTurn = Readonly<{
|
||||
id: string;
|
||||
@@ -245,33 +249,6 @@ type RenderMessage = ChatMessageView & {
|
||||
candidateOffer?: Readonly<{ resultId: string }>;
|
||||
};
|
||||
|
||||
function mergeTurnQuestions(current: RenderMessage[], turns: readonly unknown[]): RenderMessage[] {
|
||||
const byId = new Map<string, { question: TurnQuestion | null; offerResultId: string | null }>();
|
||||
for (const item of turns) {
|
||||
if (!item || typeof item !== "object") continue;
|
||||
const turn = item as { id?: unknown; question?: unknown; offer_result_id?: unknown };
|
||||
if (typeof turn.id !== "string") continue;
|
||||
byId.set(turn.id, {
|
||||
question: parseTurnQuestion(turn.question),
|
||||
offerResultId: typeof turn.offer_result_id === "string" ? turn.offer_result_id : null,
|
||||
});
|
||||
}
|
||||
return current.map((message) => {
|
||||
if (!message.turnId || !byId.has(message.turnId)) return message;
|
||||
const next = byId.get(message.turnId);
|
||||
const question = next?.question ?? undefined;
|
||||
return {
|
||||
...message,
|
||||
question: question ?? undefined,
|
||||
candidateOffer: persistedOfferFromTurn(
|
||||
next?.offerResultId,
|
||||
message.candidateOffer,
|
||||
true,
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function markQuestionAnswered(
|
||||
current: RenderMessage[],
|
||||
focusId: string,
|
||||
@@ -295,16 +272,6 @@ function snapshotTurns(payload: { turns?: unknown } | null | undefined): readonl
|
||||
return Array.isArray(payload?.turns) ? payload.turns : [];
|
||||
}
|
||||
|
||||
function appendUnseenAssistantTurns(current: RenderMessage[], turns: readonly unknown[]): RenderMessage[] {
|
||||
const known = new Set(current.flatMap((message) => message.turnId ? [message.turnId] : []));
|
||||
const extras = messagesFromTurns(turns as readonly PersistedTurn[]).filter((message) => (
|
||||
message.role === "assistant"
|
||||
&& message.turnId
|
||||
&& !known.has(message.turnId)
|
||||
));
|
||||
return extras.length ? [...current, ...extras] : current;
|
||||
}
|
||||
|
||||
function choiceCardFromQuestion(
|
||||
question: TurnQuestion,
|
||||
live: ChoiceCardModel | null,
|
||||
@@ -1194,7 +1161,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
const withoutPlaceholder = current.filter((message) => message.renderKey !== assistantRenderKey);
|
||||
const withHistory = appendUnseenAssistantTurns(
|
||||
mergeTurnQuestions(withoutPlaceholder, turns),
|
||||
turns,
|
||||
messagesFromTurns(turns as readonly PersistedTurn[]),
|
||||
);
|
||||
if (withHistory.length > withoutPlaceholder.length) return withHistory;
|
||||
return [
|
||||
@@ -1396,7 +1363,13 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
if (!response.ok || payload?.ok !== true) {
|
||||
throw new Error(payload?.error || payload?.message || "暂时无法开始参考题");
|
||||
}
|
||||
await loadCaseSnapshot();
|
||||
await loadCaseSnapshot((turns) => {
|
||||
setMessages((current) => applySnapshotTurnsToMessages(
|
||||
current,
|
||||
turns,
|
||||
(incoming) => messagesFromTurns(incoming as readonly PersistedTurn[]),
|
||||
));
|
||||
});
|
||||
} catch (caught) {
|
||||
setError(caught instanceof Error ? caught.message : "暂时无法开始参考题");
|
||||
} finally {
|
||||
@@ -1579,7 +1552,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
questionMissing: currentQuestion === null,
|
||||
questionLoadFailed: questionSource === "unavailable" || deadChoice,
|
||||
questionPersisted: Boolean(currentQuestion?.prompt && questionSource === "focus") && !deadChoice,
|
||||
offerAwaitingReader: showSelectionCards && !candidateResult?.selectedTime,
|
||||
offerAwaitingReader: showSelectionCards && !candidateResult?.selectedTime && currentQuestion === null,
|
||||
nextUserActionId,
|
||||
collectWaiting: collectWaiting && !deadChoice,
|
||||
sessionOutcome: interviewSessionOutcome ?? candidateResult?.sessionOutcome ?? null,
|
||||
@@ -1622,7 +1595,13 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
},
|
||||
});
|
||||
async function refetchQuestion() {
|
||||
await loadCaseSnapshot();
|
||||
await loadCaseSnapshot((turns) => {
|
||||
setMessages((current) => applySnapshotTurnsToMessages(
|
||||
current,
|
||||
turns,
|
||||
(incoming) => messagesFromTurns(incoming as readonly PersistedTurn[]),
|
||||
));
|
||||
});
|
||||
}
|
||||
async function repairQuestion() {
|
||||
if (questionRepairing || questionRepairAttempts >= RECTIFICATION_QUESTION_REPAIR_LIMIT) return;
|
||||
@@ -1650,7 +1629,13 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
);
|
||||
if (!recovered) setQuestionRepairAttempts((current) => current + 1);
|
||||
} else {
|
||||
await loadCaseSnapshot();
|
||||
await loadCaseSnapshot((turns) => {
|
||||
setMessages((current) => applySnapshotTurnsToMessages(
|
||||
current,
|
||||
turns,
|
||||
(incoming) => messagesFromTurns(incoming as readonly PersistedTurn[]),
|
||||
));
|
||||
});
|
||||
setQuestionRepairAttempts((current) => current + 1);
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -59,6 +59,9 @@ export function RectificationRangeDelivery({
|
||||
{RECTIFICATION_USER_COPY.rangeDeliveryTieBreakEntry}
|
||||
</button>
|
||||
) : null}
|
||||
{delivery?.tie_break_note ? (
|
||||
<p className="rectification-range-delivery__narrow">{delivery.tie_break_note}</p>
|
||||
) : null}
|
||||
{sharedTraits.length > 0 ? (
|
||||
<ul className="rectification-range-delivery__shared">
|
||||
{sharedTraits.map((line) => (
|
||||
|
||||
Reference in New Issue
Block a user