Clarify birth time terminal next step

This commit is contained in:
Jesse_Chen
2026-07-19 19:18:40 +08:00
parent 18992eb42a
commit 9d1c085303
5 changed files with 54 additions and 13 deletions
+5
View File
@@ -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); }
@@ -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 (
<div className="birth-time-candidate-result" aria-live="polite">
<p className="birth-time-evidence-boundary"><span className="phrase-nowrap">使</span></p>
{terminalPath && <TerminalAction controller={controller} path={terminalPath} />}
{terminalPath && <TerminalAction controller={controller} error={error} path={terminalPath} />}
{!terminalPath && error ? <p className="form-error" role="alert">{error}</p> : null}
</div>
);
}
@@ -52,13 +54,13 @@ export function BirthTimeCandidateResult({ journey, controller }: CandidateResul
{action.kind === "present_low_result" && (
<div className="birth-time-candidate-terminal" role="status">
<p>{dynamic ? "目前没有足够的新信息继续稳定缩小范围,本次评估已结束并保存当前候选范围。" : "本轮校正已安全结束,只保留候选范围,当前排盘使用时间保持不变。"}</p>
{terminalPath && <TerminalAction controller={controller} path={terminalPath} />}
{terminalPath && <TerminalAction controller={controller} error={error} path={terminalPath} />}
</div>
)}
{action.kind === "present_medium_result" && winner && dynamic && (
<div className="birth-time-candidate-terminal" role="status">
<p><span className="phrase-nowrap">使</span></p>
{terminalPath && <TerminalAction controller={controller} path={terminalPath} />}
{terminalPath && <TerminalAction controller={controller} error={error} path={terminalPath} />}
</div>
)}
{action.kind === "present_medium_result" && winner && !dynamic && (
@@ -72,7 +74,7 @@ export function BirthTimeCandidateResult({ journey, controller }: CandidateResul
{action.kind === "candidate_saved" && (
<div className="birth-time-candidate-terminal" role="status">
<p className="birth-time-success-note"><span className="phrase-nowrap">使</span></p>
{terminalPath && <TerminalAction controller={controller} path={terminalPath} />}
{terminalPath && <TerminalAction controller={controller} error={error} path={terminalPath} />}
</div>
)}
{action.kind === "request_candidate_confirmation" && winner && (
@@ -90,27 +92,32 @@ export function BirthTimeCandidateResult({ journey, controller }: CandidateResul
<button className="button-primary birth-time-guided-action" disabled={controller.pending} type="button" onClick={controller.acknowledgeReady}>使</button>
</div>
)}
{!terminalPath && error ? <p className="form-error" role="alert">{error}</p> : null}
</div>
);
}
function TerminalAction({ controller, path }: {
function TerminalAction({ controller, error, path }: {
readonly controller: BirthTimeGuidedController;
readonly error: string;
readonly path: NonNullable<ReturnType<typeof guidedTerminalPath>>;
}) {
if (path.kind === "complete_with_candidate") {
return (
<div className="birth-time-new-assessment">
<div className="birth-time-next-step">
<b></b>
<p>使 {path.time} </p>
<button className="button-primary birth-time-guided-action" disabled={controller.pending} onClick={() => controller.completeCandidate(path.time)} type="button">
{controller.pending ? "保存中…" : "完成并继续对话"}
{controller.pending ? `正在采用 ${path.time}` : `采用 ${path.time} 并进入对话`}
</button>
<small> {path.time} </small>
{error ? <p className="form-error" role="alert">{error}</p> : null}
</div>
);
}
return (
<div className="birth-time-new-assessment">
<button className="button-secondary birth-time-guided-action" disabled={controller.pending} onClick={controller.editBirthTimeDetails} type="button"></button>
{error ? <p className="form-error" role="alert">{error}</p> : null}
<small></small>
</div>
);
@@ -76,8 +76,14 @@ export function BirthTimeLegacyRectification(props: LegacyRectificationProps) {
{action.kind === "score_pending" && <p className="birth-time-scoring-status" role="status">使</p>}
{action.kind === "retry_scoring" && <button className="button-primary birth-time-guided-action" disabled={props.controller.pending} onClick={props.controller.retryScoring} type="button"></button>}
{action.kind === "paused" && <button className="button-primary birth-time-guided-action" disabled={props.controller.pending} onClick={props.controller.resume} type="button"></button>}
{showsCandidate && <BirthTimeCandidateResult controller={props.controller} journey={props.journey} />}
{error && <p className="form-error" role="alert">{error}</p>}
{showsCandidate ? (
<BirthTimeCandidateResult
controller={props.controller}
error={error}
journey={props.journey}
/>
) : null}
{error && !showsCandidate ? <p className="form-error" role="alert">{error}</p> : null}
</section>
);
}
@@ -115,8 +115,14 @@ export function BirthTimeRectification(props: BirthTimeRectificationProps) {
)}
{action.kind === "retry_scoring" && <button className="button-primary birth-time-guided-action" disabled={props.controller.pending} onClick={props.controller.retryScoring} type="button"></button>}
{action.kind === "paused" && <button className="button-primary birth-time-guided-action" disabled={props.controller.pending} onClick={props.controller.resume} type="button"></button>}
{showsCandidate && <BirthTimeCandidateResult controller={props.controller} journey={props.journey} />}
{error && action.kind !== "ask_dynamic_choice" && action.kind !== "clarify_unmatched_answer" && <p className="form-error" role="alert">{error}</p>}
{showsCandidate ? (
<BirthTimeCandidateResult
controller={props.controller}
error={error}
journey={props.journey}
/>
) : null}
{error && !showsCandidate ? <p className="form-error" role="alert">{error}</p> : null}
</section>
);
}
@@ -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");