Files
Jyotisha/frontend/src/components/birth-time-rectification.tsx
T
2026-07-19 20:24:44 +08:00

131 lines
7.4 KiB
TypeScript

"use client";
import { useEffect, useRef } from "react";
import { BirthTimeCandidateResult } from "@/components/birth-time-candidate-result";
import {
BirthTimeChoiceQuestion,
BirthTimeQuestionProgress,
BirthTimeUnmatchedClarification,
} from "@/components/birth-time-choice-question";
import { BirthTimeLegacyRectification } from "@/components/birth-time-legacy-rectification";
import type { BirthTimeGuidedController } from "@/hooks/use-birth-time-guided-journey";
import type { JourneyClientResponse } from "@/lib/birth-time-journey-client";
import type { DynamicNextAction } from "@/lib/birth-time-journey-turn-protocol";
type BirthTimeRectificationProps = {
readonly journey: JourneyClientResponse;
readonly controller: BirthTimeGuidedController;
readonly externalError: string;
};
function dynamicHeading(action: DynamicNextAction): { readonly title: string; readonly badge: string } {
switch (action.kind) {
case "generate_dynamic_question": return { title: "准备下一道问题", badge: "正在生成" };
case "retry_question_generation": return { title: "换一道问题", badge: "可重试" };
case "ask_dynamic_choice": return { title: "选择更接近的情况", badge: "点击作答" };
case "clarify_unmatched_answer": return { title: "换一道更合适的问题", badge: "可选补充" };
case "score_pending": return { title: "正在缩小候选范围", badge: "评分中" };
case "retry_scoring": return { title: "评分需要重试", badge: "可恢复" };
case "present_low_result": return { title: "本次评估已结束", badge: "范围已保存" };
case "present_medium_result": return { title: "候选范围已形成", badge: "评估已结束" };
case "request_candidate_confirmation": return { title: "确认候选时间", badge: "待确认" };
case "ready": return { title: "排盘时间已更新", badge: "已完成" };
case "paused": return { title: "评估已暂停", badge: "进度已保存" };
}
}
function statusCopy(action: DynamicNextAction): string {
switch (action.kind) {
case "generate_dynamic_question": return "系统正在根据当前候选范围准备更有区分度的问题。";
case "retry_question_generation": return "这道问题暂时没有生成成功,可以原地重试。";
case "ask_dynamic_choice": return "点击最接近的选项即可,不需要再填写日期或时间。";
case "clarify_unmatched_answer": return "补充说明完全可选,也可以直接换一道题。";
case "score_pending": return "正在应用刚才的选择,完成后会自动进入下一步。";
case "retry_scoring": return "刚才的选择已经保存,可以安全重试同一评分任务。";
case "present_low_result": return "目前没有足够的新信息继续稳定缩小范围,本次评估已结束并保存当前候选范围。";
case "present_medium_result": return "已形成较窄的候选范围,本次评估已结束;它不会自动改动当前排盘时间。";
case "request_candidate_confirmation": return "只有明确确认后,候选时间才会用于当前排盘。";
case "ready": return `当前排盘使用时间已更新为 ${action.activeTime}`;
case "paused": return "当前问题和候选范围已保存,可以稍后从这里继续。";
}
}
export function BirthTimeRectification(props: BirthTimeRectificationProps) {
const assessmentHeadingRef = useRef<HTMLHeadingElement | null>(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 <BirthTimeLegacyRectification {...props} journey={props.journey} />;
}
const action = props.journey.nextAction;
const heading = dynamicHeading(action);
const error = props.controller.error || props.externalError;
const showsProgress = action.kind !== "ask_dynamic_choice" && action.kind !== "clarify_unmatched_answer";
const childOwnsError = action.kind === "ask_dynamic_choice"
|| action.kind === "clarify_unmatched_answer";
const showsCandidate = action.kind === "present_low_result"
|| action.kind === "present_medium_result"
|| action.kind === "request_candidate_confirmation"
|| action.kind === "ready";
return (
<section className="birth-time-rectification onboarding-card" aria-labelledby="birth-time-assessment-title">
<div className="birth-time-assessment-heading">
<div><span></span><h2 id="birth-time-assessment-title" ref={assessmentHeadingRef} tabIndex={-1}>{heading.title}</h2></div>
<span className="birth-time-status-badge">{heading.badge}</span>
</div>
{showsProgress && <BirthTimeQuestionProgress progress={props.journey.progress} />}
<p className="birth-time-assistant-intent" role="status">{statusCopy(action)}</p>
{action.kind === "ask_dynamic_choice" && (
<BirthTimeChoiceQuestion
key={`${props.journey.turnVersion}:${action.question.questionId}`}
error={error}
onFinish={props.controller.finish}
onPause={props.controller.pause}
onSelect={props.controller.selectOption}
pending={props.controller.pending}
progress={props.journey.progress}
question={action.question}
/>
)}
{action.kind === "clarify_unmatched_answer" && (
<BirthTimeUnmatchedClarification
error={error}
onFinish={props.controller.finish}
onPause={props.controller.pause}
onReframe={props.controller.submitUnmatchedContext}
pending={props.controller.pending}
progress={props.journey.progress}
/>
)}
{(action.kind === "generate_dynamic_question" || action.kind === "retry_question_generation") && error && (
<button className="button-secondary birth-time-guided-action" disabled={props.controller.pending} onClick={props.controller.retryQuestionGeneration} type="button"></button>
)}
{action.kind === "score_pending" && (
<div className="birth-time-scoring-status" aria-live="polite">
<b></b>
{props.controller.pollRecoverable && <button className="button-secondary birth-time-guided-action" onClick={props.controller.retryScoring} type="button"></button>}
</div>
)}
{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}
error={error}
journey={props.journey}
/>
) : null}
{error && !showsCandidate && !childOwnsError ? <p className="form-error" role="alert">{error}</p> : null}
</section>
);
}