From bfd49760ed0990e9453b148127dcd572ed4bee59 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Sun, 19 Jul 2026 10:51:49 +0800 Subject: [PATCH] fix: complete dynamic choice preview flow --- .../hooks/use-birth-time-guided-journey.ts | 5 ++ .../src/lib/birth-time-dynamic-preview.ts | 68 +++++++++++++++++-- .../tests/birth-time-choice-question.test.ts | 32 ++++----- 3 files changed, 83 insertions(+), 22 deletions(-) diff --git a/frontend/src/hooks/use-birth-time-guided-journey.ts b/frontend/src/hooks/use-birth-time-guided-journey.ts index 211ddcf3..a58b9d66 100644 --- a/frontend/src/hooks/use-birth-time-guided-journey.ts +++ b/frontend/src/hooks/use-birth-time-guided-journey.ts @@ -183,6 +183,11 @@ export function useBirthTimeGuidedJourney(input: GuidedJourneyInput): BirthTimeG }; const retryScoring = () => { const turn = journey; + if (preview && turn?.journeyProtocol === "dynamic-choice-v2" + && (turn.nextAction.kind === "score_pending" || turn.nextAction.kind === "retry_scoring")) { + operate((current) => Promise.resolve(previewAction(current, { kind: "retry_scoring" }))); + return; + } if (turn?.nextAction.kind === "score_pending") { setError(""); setPollRun((value) => value + 1); diff --git a/frontend/src/lib/birth-time-dynamic-preview.ts b/frontend/src/lib/birth-time-dynamic-preview.ts index 16787090..e7d5acfd 100644 --- a/frontend/src/lib/birth-time-dynamic-preview.ts +++ b/frontend/src/lib/birth-time-dynamic-preview.ts @@ -147,14 +147,72 @@ export function advanceDynamicBirthTimePreview( if (action.kind !== "ask_dynamic_choice") return turn; const option = action.question.options.find((item) => item.optionId === command.optionId); if (!option) return turn; + const answeredCount = turn.progress.answeredCount + 1; + const effectiveAnswerCount = turn.progress.effectiveAnswerCount + (option.kind === "primary" ? 1 : 0); switch (option.kind) { - case "unmatched": return dynamicBirthTimePreview("clarification"); - case "primary": return dynamicBirthTimePreview("scoring"); - case "unknown": return response({ nextAction: { kind: "ask_dynamic_choice", question: questions[1] }, phase: "question", answeredCount: 1, turnVersion: 2 }); + case "unmatched": return response({ + nextAction: { kind: "clarify_unmatched_answer", questionId: action.question.questionId }, + phase: "clarification", + answeredCount, + effectiveAnswerCount, + turnVersion: turn.turnVersion + 1, + }); + case "primary": { + if (action.question.questionId === questions[0].questionId) { + return response({ + nextAction: { kind: "ask_dynamic_choice", question: questions[1] }, + phase: "question", + answeredCount, + effectiveAnswerCount, + turnVersion: turn.turnVersion + 1, + }); + } + return response({ + nextAction: { kind: "present_medium_result", resultId }, + phase: "result", + answeredCount, + effectiveAnswerCount, + candidateResult: candidate, + snapshotState: "candidate", + turnVersion: turn.turnVersion + 1, + }); + } + case "unknown": { + if (action.question.questionId === questions[0].questionId) { + return response({ + nextAction: { kind: "ask_dynamic_choice", question: questions[1] }, + phase: "question", + answeredCount, + effectiveAnswerCount, + turnVersion: turn.turnVersion + 1, + }); + } + return response({ + nextAction: { kind: "present_low_result", resultId: null }, + phase: "result", + answeredCount, + effectiveAnswerCount, + snapshotState: "candidate", + turnVersion: turn.turnVersion + 1, + }); + } } } - case "reframe": return response({ nextAction: { kind: "ask_dynamic_choice", question: questions[1] }, phase: "question", answeredCount: 1, turnVersion: 3 }); - case "finish": return dynamicBirthTimePreview("low"); + case "reframe": return response({ + nextAction: { kind: "ask_dynamic_choice", question: questions[1] }, + phase: "question", + answeredCount: turn.progress.answeredCount, + effectiveAnswerCount: turn.progress.effectiveAnswerCount, + turnVersion: turn.turnVersion + 1, + }); + case "finish": return response({ + nextAction: { kind: "present_low_result", resultId: null }, + phase: "result", + answeredCount: turn.progress.answeredCount, + effectiveAnswerCount: turn.progress.effectiveAnswerCount, + snapshotState: "candidate", + turnVersion: turn.turnVersion + 1, + }); case "pause": return dynamicBirthTimePreview("paused"); case "resume": return dynamicBirthTimePreview("question"); case "retry_scoring": return dynamicBirthTimePreview("medium"); diff --git a/frontend/tests/birth-time-choice-question.test.ts b/frontend/tests/birth-time-choice-question.test.ts index 907ac2b2..d6a9090f 100644 --- a/frontend/tests/birth-time-choice-question.test.ts +++ b/frontend/tests/birth-time-choice-question.test.ts @@ -1,5 +1,4 @@ import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; import test from "node:test"; import { choiceQuestionGroups, @@ -45,33 +44,32 @@ test("unmatched requests clarification without creating scoring evidence", () => assert.equal(normalizeUnmatchedNote("甲".repeat(260)).length, 240); }); -test("the rendered component keeps one-click and optional-note semantics", () => { - const source = readFileSync( - new URL("../src/components/birth-time-choice-question.tsx", import.meta.url), - "utf8", - ); - - assert.match(source, /
{ const start = dynamicBirthTimePreview(); - const scoring = advanceDynamicBirthTimePreview(start, { kind: "select", optionId: "earlier" }); + const nextQuestion = advanceDynamicBirthTimePreview(start, { kind: "select", optionId: "earlier" }); + const result = advanceDynamicBirthTimePreview(nextQuestion, { kind: "select", optionId: "school" }); const clarification = advanceDynamicBirthTimePreview(start, { kind: "select", optionId: "unmatched" }); const reframed = advanceDynamicBirthTimePreview(clarification, { kind: "reframe" }); const terminal = advanceDynamicBirthTimePreview(reframed, { kind: "finish" }); - assert.equal(scoring.nextAction.kind, "score_pending"); + assert.equal(nextQuestion.nextAction.kind, "ask_dynamic_choice"); + assert.equal(nextQuestion.progress.effectiveAnswerCount, 1); + assert.equal(result.nextAction.kind, "present_medium_result"); assert.equal(clarification.nextAction.kind, "clarify_unmatched_answer"); assert.equal(reframed.nextAction.kind, "ask_dynamic_choice"); assert.equal(terminal.nextAction.kind, "present_low_result"); }); +test("preview unknown answers terminate instead of repeating the same question", () => { + const start = dynamicBirthTimePreview(); + const secondQuestion = advanceDynamicBirthTimePreview(start, { kind: "select", optionId: "unknown" }); + const terminal = advanceDynamicBirthTimePreview(secondQuestion, { kind: "select", optionId: "unknown-2" }); + + assert.equal(secondQuestion.nextAction.kind, "ask_dynamic_choice"); + assert.equal(terminal.nextAction.kind, "present_low_result"); + assert.equal(terminal.progress.answeredCount, 2); +}); + test("dynamic previews cover every visible result and recovery state", () => { const expected = new Map([ ["generating", "generate_dynamic_question"],