fix(rectification): show question stems and unblock adopt after occupation
Choice legends and spoken collect prompts were hidden after BUG-461, and occupation still blocked canAdopt once dated coverage was done. Restore visible stems and stop polling extra collects when adopt is allowed. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2956,6 +2956,13 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
margin-block: var(--space-3);
|
||||
margin-inline-start: var(--assistant-content-inset);
|
||||
}
|
||||
.rectification-question-slot__prompt {
|
||||
margin: 0;
|
||||
color: var(--color-ink);
|
||||
font-size: var(--type-body-sm);
|
||||
font-weight: 600;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.rectification-question-slot__status {
|
||||
margin: 0;
|
||||
color: var(--color-ink-secondary);
|
||||
@@ -2988,16 +2995,14 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-canvas-soft);
|
||||
}
|
||||
.rectification-choice-card .birth-time-choice-question legend.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
.rectification-choice-card .birth-time-choice-question legend {
|
||||
display: block;
|
||||
margin: 0 0 var(--space-3);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
color: var(--color-ink);
|
||||
font-size: var(--type-body-sm);
|
||||
font-weight: 600;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.rectification-choice-card .birth-time-choice-question:disabled .birth-time-choice-option {
|
||||
cursor: default;
|
||||
|
||||
@@ -325,6 +325,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
const [boardDiff, setBoardDiff] = useState(() => diffRectificationBoard(null, null));
|
||||
const boardId = useId();
|
||||
const boardTitleId = useId();
|
||||
const collectSpokenPromptId = useId();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const query = window.matchMedia(`(max-width: ${RECTIFICATION_BOARD_SPLIT_MIN_PX - 1}px)`);
|
||||
@@ -1229,6 +1230,11 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
onStop={submitStop}
|
||||
/>
|
||||
)}
|
||||
{collectSpokenPrompt && !showLiveChoiceCard && !readonly && (
|
||||
<p id={collectSpokenPromptId} className="rectification-question-slot__prompt">
|
||||
{collectSpokenPrompt}
|
||||
</p>
|
||||
)}
|
||||
{showMissingQuestion && (
|
||||
<p className="rectification-question-slot__status" role="status">
|
||||
当前没有可回答的问题,正在等待服务端更新。
|
||||
@@ -1278,6 +1284,9 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
<Textarea
|
||||
ref={composer}
|
||||
aria-label={readonly ? "该校正已结束,只能查看历史" : "继续描述你的经历或回答"}
|
||||
aria-describedby={collectSpokenPrompt && !showLiveChoiceCard && !readonly
|
||||
? collectSpokenPromptId
|
||||
: undefined}
|
||||
value={draft}
|
||||
disabled={!canSend}
|
||||
placeholder={readonly
|
||||
|
||||
@@ -48,7 +48,7 @@ export function RectificationChoiceCard(props: RectificationChoiceCardProps) {
|
||||
disabled={props.pending || props.disabled || answered}
|
||||
data-answered={answered ? "true" : "false"}
|
||||
>
|
||||
<legend className="sr-only">{props.card.prompt}</legend>
|
||||
<legend>{props.card.prompt}</legend>
|
||||
{props.card.why ? <p className="rectification-choice-why">{props.card.why}</p> : null}
|
||||
<div className="birth-time-primary-choices">
|
||||
{props.card.options.map((option, index) => (
|
||||
|
||||
@@ -78,6 +78,29 @@ function openingRangeFromDossier(dossier: {
|
||||
return openingRangeFromCandidateRange(dossier.case?.candidateRange ?? null);
|
||||
}
|
||||
|
||||
function shouldSkipFollowupPersist(input: {
|
||||
canAdopt: boolean;
|
||||
nextAction: string;
|
||||
}): boolean {
|
||||
return input.canAdopt
|
||||
&& input.nextAction !== "ask_fact_collection"
|
||||
&& input.nextAction !== "ask_candidate_discriminator"
|
||||
&& input.nextAction !== "ask_holdout_validation";
|
||||
}
|
||||
|
||||
function adoptHostNarration(input: {
|
||||
credibleRange?: readonly [string, string] | null;
|
||||
representativeTime?: string | null;
|
||||
openingRange: readonly [string, string] | null;
|
||||
receipt: Readonly<Record<string, unknown>> | null | undefined;
|
||||
}): string {
|
||||
return withProspectiveWindows(deliveryAdoptNarration({
|
||||
credibleRange: input.credibleRange,
|
||||
representativeTime: input.representativeTime,
|
||||
openingRange: input.openingRange,
|
||||
}), input.receipt);
|
||||
}
|
||||
|
||||
export type ApplyChoiceCommand = Readonly<{
|
||||
userId: string;
|
||||
caseId: string;
|
||||
@@ -316,6 +339,21 @@ export async function persistNextInterviewAfterChoice(input: {
|
||||
nextAction: ReturnType<typeof publicNextAction>;
|
||||
birthDate?: string | null;
|
||||
}): Promise<{ hostNarration: string; choiceReady: boolean; persisted?: boolean }> {
|
||||
if (shouldSkipFollowupPersist({
|
||||
canAdopt: input.nextAction.can_adopt,
|
||||
nextAction: input.nextAction.type,
|
||||
})) {
|
||||
return {
|
||||
hostNarration: adoptHostNarration({
|
||||
credibleRange: input.nextAction.credible_range,
|
||||
representativeTime: input.nextAction.representative_time,
|
||||
openingRange: openingRangeFromDossier(input.dossier),
|
||||
receipt: input.dossier.latestResult?.decisionReceipt,
|
||||
}),
|
||||
choiceReady: false,
|
||||
persisted: false,
|
||||
};
|
||||
}
|
||||
const latest = input.dossier.latestResult
|
||||
? {
|
||||
...input.dossier.latestResult,
|
||||
@@ -553,20 +591,16 @@ export async function persistNextInterviewIfIdle(input: {
|
||||
birthDate = null;
|
||||
}
|
||||
const decision = decideFromDossier(dossier, { birthDate });
|
||||
if (
|
||||
decision.canAdopt
|
||||
&& decision.nextAction !== "ask_fact_collection"
|
||||
&& decision.nextAction !== "ask_candidate_discriminator"
|
||||
&& decision.nextAction !== "ask_holdout_validation"
|
||||
) {
|
||||
if (shouldSkipFollowupPersist(decision)) {
|
||||
return {
|
||||
persisted: false,
|
||||
choiceReady: false,
|
||||
hostNarration: withProspectiveWindows(deliveryAdoptNarration({
|
||||
hostNarration: adoptHostNarration({
|
||||
credibleRange: decision.credibleRange,
|
||||
representativeTime: decision.representativeTime,
|
||||
openingRange: openingRangeFromDossier(dossier),
|
||||
}), dossier.latestResult?.decisionReceipt),
|
||||
receipt: dossier.latestResult?.decisionReceipt,
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (isNonConvergingRangeOffer(decision)) {
|
||||
|
||||
@@ -22,10 +22,13 @@
|
||||
* Finance/health score if volunteered; they are not method-layer rotation.
|
||||
* Method coverage finishes before repeating a precision-stage ask.
|
||||
* Appearance and marks are skipped_by_policy. Horary does not block offering
|
||||
* time cards. Occupation still blocks adopt and confirmation until a note
|
||||
* exists, but once the training gate is open it must not also block yearless
|
||||
* varga cards. Career evidence plus a closed occupation collect focus covers
|
||||
* occupation without depending on the model picking domain=occupation.
|
||||
* time cards. Occupation is a method-layer note, not an adopt gate: dated
|
||||
* dasha/D9/D10/relatives coverage plus the engine ceiling can offer
|
||||
* representative cards while occupation remains uncovered. Yearless-to-collect
|
||||
* conversion still waits until occupation is covered, so that note can be
|
||||
* asked later without blocking adopt. Career evidence
|
||||
* plus a closed occupation collect focus still covers the occupation method
|
||||
* without depending on the model picking domain=occupation.
|
||||
* Method coverage asks for dated events in natural language.
|
||||
* Known-event quality probes (exam went badly for a year already
|
||||
* in the ledger) are not reverse-inference cards. Dasha existence
|
||||
@@ -205,7 +208,6 @@ const BLOCKING_COVERAGE_IDS = new Set<MethodFollowupId>([
|
||||
"d9_relationship",
|
||||
"d10_career",
|
||||
"relatives",
|
||||
"occupation",
|
||||
]);
|
||||
|
||||
function isConfirmedDated(item: MethodFollowupEvidence): boolean {
|
||||
@@ -1685,7 +1687,7 @@ export function buildMethodFollowupPlan(input: {
|
||||
source: "method_coverage",
|
||||
});
|
||||
}
|
||||
if (!next && dashaCovered && coverageComplete) {
|
||||
if (!next && dashaCovered && coverageComplete && occupationCovered) {
|
||||
const allowLowGainDiscriminator = !coverageComplete || !candidatesSeparated;
|
||||
const yearless = yearlessDiscriminators[0];
|
||||
if (yearless && (allowLowGainDiscriminator || yearless.score >= 0.08)) {
|
||||
|
||||
@@ -33,9 +33,10 @@ export function dossierHasNonemptyAssistantBody(
|
||||
/**
|
||||
* Whether to append the server-owned focus prompt as its own assistant turn.
|
||||
*
|
||||
* Structured only: action, focus kind/intent, and whether this turn already
|
||||
* has a nonempty assistant body. Never inspect the body text to decide
|
||||
* “whether it already asked”.
|
||||
* Structured only: focus kind/intent, and whether this turn already has a
|
||||
* nonempty assistant body. Never inspect the body text to decide
|
||||
* “whether it already asked”. Collect prompts live in the question slot,
|
||||
* so a second assistant turn is only written when this turn has no body.
|
||||
*/
|
||||
export function shouldPersistFocusPromptTurn(input: {
|
||||
action: RectificationRouteAction;
|
||||
@@ -45,7 +46,7 @@ export function shouldPersistFocusPromptTurn(input: {
|
||||
}): boolean {
|
||||
const collect = input.questionKind === "collect_spoken"
|
||||
|| input.questionIntent === "collect_method_evidence";
|
||||
return !(input.action === "opening" && collect && input.assistantBodyPresent);
|
||||
return !(collect && input.assistantBodyPresent);
|
||||
}
|
||||
|
||||
export async function finalizeSuccessfulTurnExit(input: {
|
||||
|
||||
Reference in New Issue
Block a user