diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 18da97d6..4fee4bb5 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -412,6 +412,11 @@ button:disabled { cursor: default; opacity: .45; } .birth-time-confirmation-panel { display: grid; gap: var(--space-3); padding: var(--space-4); border-left: 2px solid var(--color-action); background: var(--color-action-soft); } .birth-time-confirmation-panel > b { color: var(--color-ink); font-size: var(--type-body-sm); } .birth-time-confirmation-panel > p { margin: 0; color: var(--color-ink-secondary); font-size: var(--type-caption); line-height: 1.6; word-break: keep-all; overflow-wrap: break-word; } +.birth-time-next-step { box-sizing: border-box; width: 100%; display: grid; gap: var(--space-3); padding: var(--space-4); border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-action-soft); } +.birth-time-next-step > b { color: var(--color-ink); } +.birth-time-next-step > p { margin: 0; color: var(--color-ink-secondary); } +.birth-time-next-step > .birth-time-guided-action { width: 100%; min-height: 48px; } +.birth-time-next-step > .form-error { width: 100%; color: var(--color-danger); } .birth-time-retry-card { display: grid; justify-items: start; gap: var(--space-3); } .birth-time-retry-card p { margin: 0; color: var(--color-ink-secondary); font-size: var(--type-body-sm); line-height: 1.6; } .birth-time-guide-turn, .birth-time-evidence-draft-card, .birth-time-scoring-status, .birth-time-candidate-terminal { min-width: 0; max-width: 100%; display: grid; gap: var(--space-4); } diff --git a/frontend/src/components/birth-time-candidate-result.tsx b/frontend/src/components/birth-time-candidate-result.tsx index 0b3471dd..61a94a23 100644 --- a/frontend/src/components/birth-time-candidate-result.tsx +++ b/frontend/src/components/birth-time-candidate-result.tsx @@ -7,6 +7,7 @@ import { guidedTerminalPath } from "@/lib/birth-time-guided-terminal"; type CandidateResultProps = { readonly journey: JourneyClientResponse; readonly controller: BirthTimeGuidedController; + readonly error: string; }; const confidenceLabels = { @@ -15,7 +16,7 @@ const confidenceLabels = { high: "较高置信", } as const; -export function BirthTimeCandidateResult({ journey, controller }: CandidateResultProps) { +export function BirthTimeCandidateResult({ journey, controller, error }: CandidateResultProps) { const result = journey.candidateResult; const action = journey.nextAction; const dynamic = journey.journeyProtocol === "dynamic-choice-v2"; @@ -24,7 +25,8 @@ export function BirthTimeCandidateResult({ journey, controller }: CandidateResul return (

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

- {terminalPath && } + {terminalPath && } + {!terminalPath && error ?

{error}

: null}
); } @@ -52,13 +54,13 @@ export function BirthTimeCandidateResult({ journey, controller }: CandidateResul {action.kind === "present_low_result" && (

{dynamic ? "目前没有足够的新信息继续稳定缩小范围,本次评估已结束并保存当前候选范围。" : "本轮校正已安全结束,只保留候选范围,当前排盘使用时间保持不变。"}

- {terminalPath && } + {terminalPath && }
)} {action.kind === "present_medium_result" && winner && dynamic && (

已形成较窄的候选范围,本次评估已结束;它不会自动改动当前排盘使用时间

- {terminalPath && } + {terminalPath && }
)} {action.kind === "present_medium_result" && winner && !dynamic && ( @@ -72,7 +74,7 @@ export function BirthTimeCandidateResult({ journey, controller }: CandidateResul {action.kind === "candidate_saved" && (

候选时间范围已保存。当前排盘使用时间没有改变。

- {terminalPath && } + {terminalPath && }
)} {action.kind === "request_candidate_confirmation" && winner && ( @@ -90,27 +92,32 @@ export function BirthTimeCandidateResult({ journey, controller }: CandidateResul )} + {!terminalPath && error ?

{error}

: null} ); } -function TerminalAction({ controller, path }: { +function TerminalAction({ controller, error, path }: { readonly controller: BirthTimeGuidedController; + readonly error: string; readonly path: NonNullable>; }) { if (path.kind === "complete_with_candidate") { return ( -
+
+ 评估已完成,下一步 +

点击后将使用 {path.time} 作为当前排盘时间并进入对话;原始填报和本次候选结果仍会保留。

- 将以 {path.time} 作为当前工作排盘时间;候选状态和原始资料都会保留。 + {error ?

{error}

: null}
); } return (
+ {error ?

{error}

: null} 会建立新的记录,当前结果仍会保留。
); diff --git a/frontend/src/components/birth-time-legacy-rectification.tsx b/frontend/src/components/birth-time-legacy-rectification.tsx index e6e1d56d..5e3dc65b 100644 --- a/frontend/src/components/birth-time-legacy-rectification.tsx +++ b/frontend/src/components/birth-time-legacy-rectification.tsx @@ -76,8 +76,14 @@ export function BirthTimeLegacyRectification(props: LegacyRectificationProps) { {action.kind === "score_pending" &&

正在使用已确认的关键经历评分…

} {action.kind === "retry_scoring" && } {action.kind === "paused" && } - {showsCandidate && } - {error &&

{error}

} + {showsCandidate ? ( + + ) : null} + {error && !showsCandidate ?

{error}

: null} ); } diff --git a/frontend/src/components/birth-time-rectification.tsx b/frontend/src/components/birth-time-rectification.tsx index a59e8206..c7a07940 100644 --- a/frontend/src/components/birth-time-rectification.tsx +++ b/frontend/src/components/birth-time-rectification.tsx @@ -115,8 +115,14 @@ export function BirthTimeRectification(props: BirthTimeRectificationProps) { )} {action.kind === "retry_scoring" && } {action.kind === "paused" && } - {showsCandidate && } - {error && action.kind !== "ask_dynamic_choice" && action.kind !== "clarify_unmatched_answer" &&

{error}

} + {showsCandidate ? ( + + ) : null} + {error && !showsCandidate ?

{error}

: null} ); } diff --git a/frontend/tests/birth-time-guided-review-fixes.test.ts b/frontend/tests/birth-time-guided-review-fixes.test.ts index f96acedc..f7d23106 100644 --- a/frontend/tests/birth-time-guided-review-fixes.test.ts +++ b/frontend/tests/birth-time-guided-review-fixes.test.ts @@ -119,6 +119,23 @@ test("ready completion is explicit and terminal low has no finish mutation", () assert.doesNotMatch(candidateSource, /controller\.finish/); }); +test("terminal candidate owns one explicit next step and its completion error", () => { + const candidateResultSource = readFileSync(new URL("../src/components/birth-time-candidate-result.tsx", import.meta.url), "utf8"); + const rectificationSource = readFileSync(new URL("../src/components/birth-time-rectification.tsx", import.meta.url), "utf8"); + const legacyRectificationSource = readFileSync(new URL("../src/components/birth-time-legacy-rectification.tsx", import.meta.url), "utf8"); + const globalCssSource = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8"); + + assert.match(candidateResultSource, /评估已完成,下一步/); + assert.match(candidateResultSource, /采用 \$\{path\.time\} 并进入对话/); + assert.match(candidateResultSource, /正在采用 \$\{path\.time\}…/); + assert.match(candidateResultSource, /birth-time-next-step/); + assert.match(rectificationSource, /error=\{error\}/); + assert.match(rectificationSource, /error && !showsCandidate/); + assert.match(legacyRectificationSource, /error=\{error\}/); + assert.match(legacyRectificationSource, /error && !showsCandidate/); + assert.match(globalCssSource, /\.birth-time-next-step/); +}); + test("completed rectification transcript does not repeat the birth place turn", () => { const pageSource = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");