diff --git a/frontend/src/components/birth-time-candidate-result.tsx b/frontend/src/components/birth-time-candidate-result.tsx index 036e3eb2..4665eb8b 100644 --- a/frontend/src/components/birth-time-candidate-result.tsx +++ b/frontend/src/components/birth-time-candidate-result.tsx @@ -23,7 +23,6 @@ export function BirthTimeCandidateResult({ journey, controller }: CandidateResul if (!result && action.kind === "present_low_result") { return (
-

目前没有足够的新信息继续稳定缩小范围,本次评估已结束并保存当前候选范围。

系统不会选择或应用未经证据支持的具体分钟,当前排盘使用时间保持不变。

{terminalPath && }
diff --git a/frontend/src/components/birth-time-choice-question.tsx b/frontend/src/components/birth-time-choice-question.tsx index 21d6ebcd..198f4370 100644 --- a/frontend/src/components/birth-time-choice-question.tsx +++ b/frontend/src/components/birth-time-choice-question.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { choiceQuestionGroups, choiceSelectionIntent, @@ -43,6 +43,12 @@ export function BirthTimeQuestionProgress({ progress }: { export function BirthTimeChoiceQuestion(props: ChoiceQuestionProps) { const groups = choiceQuestionGroups(props.question); const [selectedId, setSelectedId] = useState(""); + const firstChoiceRef = useRef(null); + useEffect(() => { + if (!props.pending && (props.progress.answeredCount > 0 || props.error)) { + firstChoiceRef.current?.focus(); + } + }, [props.error, props.pending, props.progress.answeredCount, props.question.questionId]); const select = (option: PublicDynamicChoiceQuestion["options"][number]) => { const intent = choiceSelectionIntent(option); setSelectedId(intent.optionId); @@ -55,12 +61,13 @@ export function BirthTimeChoiceQuestion(props: ChoiceQuestionProps) {
{props.question.prompt}
- {groups.primary.map((option) => ( + {groups.primary.map((option, index) => ( +
diff --git a/frontend/src/components/birth-time-rectification.tsx b/frontend/src/components/birth-time-rectification.tsx index d251ab5a..a59e8206 100644 --- a/frontend/src/components/birth-time-rectification.tsx +++ b/frontend/src/components/birth-time-rectification.tsx @@ -1,5 +1,6 @@ "use client"; +import { useEffect, useRef } from "react"; import { BirthTimeCandidateResult } from "@/components/birth-time-candidate-result"; import { BirthTimeChoiceQuestion, @@ -50,6 +51,17 @@ function statusCopy(action: DynamicNextAction): string { } export function BirthTimeRectification(props: BirthTimeRectificationProps) { + const assessmentHeadingRef = useRef(null); + const previousTurnVersion = useRef(props.journey.turnVersion); + useEffect(() => { + const changed = previousTurnVersion.current !== props.journey.turnVersion; + previousTurnVersion.current = props.journey.turnVersion; + if (!changed || props.journey.journeyProtocol !== "dynamic-choice-v2") return; + const action = props.journey.nextAction; + if (action.kind !== "ask_dynamic_choice" && action.kind !== "clarify_unmatched_answer") { + assessmentHeadingRef.current?.focus(); + } + }, [props.journey]); if (props.journey.journeyProtocol !== "dynamic-choice-v2") { return ; } @@ -65,7 +77,7 @@ export function BirthTimeRectification(props: BirthTimeRectificationProps) { return (
-
出生时间评估

{heading.title}

+
出生时间评估

{heading.title}

{heading.badge}
{showsProgress && } diff --git a/frontend/tests/birth-time-guide-flow.test.ts b/frontend/tests/birth-time-guide-flow.test.ts index ffa841a3..fd9e2efe 100644 --- a/frontend/tests/birth-time-guide-flow.test.ts +++ b/frontend/tests/birth-time-guide-flow.test.ts @@ -1,78 +1,11 @@ import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; import test from "node:test"; import { guidedTurnIdentity } from "../src/lib/birth-time-guided-turn-identity.ts"; -const read = (path: string) => readFileSync(new URL(path, import.meta.url), "utf8"); -const rectificationSource = read("../src/components/birth-time-rectification.tsx"); -const legacyRectificationSource = read("../src/components/birth-time-legacy-rectification.tsx"); -const choiceSource = read("../src/components/birth-time-choice-question.tsx"); -const turnSource = read("../src/components/birth-time-guide-turn.tsx"); -const draftSource = read("../src/components/birth-time-evidence-draft-card.tsx"); -const candidateSource = read("../src/components/birth-time-candidate-result.tsx"); -const hookSource = read("../src/hooks/use-birth-time-guided-journey.ts"); -const automaticEffectsSource = read("../src/hooks/use-birth-time-automatic-journey-effects.ts"); - -test("guided rectification renders exactly the persisted action, never a questionnaire slice", () => { - assert.match(rectificationSource, /journey\.nextAction/); - assert.doesNotMatch(rectificationSource, /questions\.slice\(0, 3\)/); - assert.doesNotMatch(rectificationSource, /nextRoundQuestions/); - assert.match(turnSource, /说出大概年份也可以/); -}); - -test("each persisted question has a stable remount identity so skipped input cannot leak", () => { +test("persisted questions receive stable identities without leaking prior input", () => { assert.equal(guidedTurnIdentity(3, "education_entry"), "3:education_entry"); assert.notEqual( guidedTurnIdentity(3, "education_entry"), guidedTurnIdentity(4, "relationship_entry"), ); - assert.match(legacyRectificationSource, /key=\{guidedTurnIdentity\(props\.journey\.turnVersion, action\.question\.questionId\)\}/); - assert.match(rectificationSource, /key=\{`\$\{props\.journey\.turnVersion\}:\$\{action\.question\.questionId\}`\}/); -}); - -test("draft review is explicit, domain locked, and incomplete confirmation stays disabled", () => { - assert.match(draftSource, /确认并用于校正/); - assert.match(draftSource, /disabled=\{props\.pending \|\| !isValid\}/); - assert.match(draftSource, /domainLabels\[props\.draft\.domain\]/); - assert.doesNotMatch(draftSource, /name=["']domain/); - assert.match(hookSource, /reviseBirthTimeEvidenceDraft/); - assert.match(hookSource, /confirmBirthTimeEvidenceDraft/); -}); - -test("guided orchestration owns fallback copy, polling, and retry", () => { - assert.match(automaticEffectsSource, /fallbackQuestionCopy/); - assert.match(automaticEffectsSource, /requestBirthTimeGuidePrompt/); - assert.match(automaticEffectsSource, /runBirthTimeScoringPoll/); - assert.match(hookSource, /retry_scoring/); - assert.match(automaticEffectsSource, /AbortController/); -}); - -test("candidate UI is nextAction-gated and keeps application boundary explicit", () => { - assert.match(candidateSource, /present_medium_result/); - assert.match(candidateSource, /request_candidate_confirmation/); - assert.match(candidateSource, /ready/); - assert.match(candidateSource, /候选时间/); - assert.match(candidateSource, /当前排盘使用时间/); - assert.doesNotMatch(candidateSource, /真实出生分钟/); -}); - -test("candidate confirmation and ready copy keep semantic time phrases intact", () => { - assert.match(candidateSource, /当前排盘使用时间<\/span>已更新为/); - assert.match(candidateSource, /原始填报时间<\/span>仍已保留/); - assert.match(candidateSource, /原始填报时间<\/span>仍会保留/); -}); - -test("guided controls expose live status and minimum target classes", () => { - assert.match(turnSource, /aria-live=["']polite/); - assert.match(draftSource, /role=["']alert/); - assert.match(turnSource, /birth-time-guided-action/); - assert.match(candidateSource, /birth-time-guided-action/); - assert.match(choiceSource, /aria-busy=\{props\.pending\}/); - assert.match(choiceSource, /birth-time-guided-action/); -}); - -test("the active v2 surface cannot render the legacy prose and date draft flow", () => { - assert.doesNotMatch(rectificationSource, /BirthTimeGuideTurn|BirthTimeEvidenceDraftCard|