fix(rectification): exhaustion exit, explain layer, range reading, unknown-time scan (BUG-565–568)
Keep askable cards after exhaustion, explain each probe, read the adopted credible range in reports and chat, and compare declared periods before the minute grid when the clock is unknown. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -221,12 +221,7 @@ export function BirthTimeIntakeFields({ value, onPatch }: BirthTimeIntakeProps)
|
||||
|
||||
{source === "unknown" && (
|
||||
<div className="birth-time-detail-note onboarding-card-reveal" role="status">
|
||||
<p>已跳过具体出生时间。保存后可以直接使用首页功能,生时校正以后需要时再做。</p>
|
||||
<button
|
||||
className="button-secondary"
|
||||
type="button"
|
||||
onClick={() => onPatch({ birthTimeSource: "period_only", birthTimeStatus: "reported" })}
|
||||
>我可以选一段时间范围</button>
|
||||
<p>可以直接开始生时校正:先从你记得的经历比出大致时段。</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -37,8 +37,10 @@ import {
|
||||
isRecommendedRectificationCandidate,
|
||||
natalRecastMeaning,
|
||||
parseRectificationCandidateResult,
|
||||
parseRectificationStepState,
|
||||
workingRectificationHouseTable,
|
||||
type RectificationCandidateResult,
|
||||
type RectificationStepState,
|
||||
} from "@/lib/rectification-candidate-result";
|
||||
import {
|
||||
diffRectificationBoard,
|
||||
@@ -167,26 +169,13 @@ function questionSourceFromSnapshot(value: unknown): "focus" | "unavailable" | n
|
||||
|
||||
function RectificationReadonlyRange({
|
||||
range,
|
||||
onStop,
|
||||
}: Readonly<{
|
||||
range: readonly [string, string];
|
||||
onStop?: () => void;
|
||||
}>) {
|
||||
if (!onStop) {
|
||||
return (
|
||||
<p className="rectification-readonly-range" role="status">
|
||||
目前范围 {range[0]}–{range[1]},还在收窄
|
||||
</p>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="rectification-readonly-range"
|
||||
onClick={onStop}
|
||||
>
|
||||
<p className="rectification-readonly-range" role="status">
|
||||
目前范围 {range[0]}–{range[1]},还在收窄
|
||||
</button>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -446,6 +435,7 @@ type CaseSnapshotState = Readonly<{
|
||||
question: CurrentQuestionModel | null;
|
||||
questionSource: "focus" | "unavailable" | null;
|
||||
choice: ChoiceCardModel | null;
|
||||
stepState: RectificationStepState | null;
|
||||
caseStatus: RectificationCaseStatus | null;
|
||||
savedTime: string | null;
|
||||
savedStatus: "accepted" | "confirmed" | null;
|
||||
@@ -467,6 +457,7 @@ function caseSnapshotState(payload: RectificationCaseSnapshotPayload | null): Ca
|
||||
question: currentQuestionFromSnapshot(payload.current_question),
|
||||
questionSource: questionSourceFromSnapshot(payload.question_source),
|
||||
choice: parseRectificationChoiceCard(payload.choice_card),
|
||||
stepState: parseRectificationStepState(payload.step_state),
|
||||
caseStatus: isRectificationCaseStatus(payload.case?.status) ? payload.case.status : null,
|
||||
savedTime: confirmedTime ?? acceptedTime,
|
||||
savedStatus: confirmedTime ? "confirmed" : acceptedTime ? "accepted" : null,
|
||||
@@ -508,6 +499,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
const [savedStatus, setSavedStatus] = useState<"accepted" | "confirmed" | null>(() => caseSnapshotState(initialSnapshot)?.savedStatus ?? null);
|
||||
const [candidateResult, setCandidateResult] = useState<CandidateResult>(() => caseSnapshotState(initialSnapshot)?.candidate ?? null);
|
||||
const [choiceCard, setChoiceCard] = useState<ChoiceCardModel | null>(() => caseSnapshotState(initialSnapshot)?.choice ?? null);
|
||||
const [stepState, setStepState] = useState<RectificationStepState | null>(() => caseSnapshotState(initialSnapshot)?.stepState ?? null);
|
||||
const [currentQuestion, setCurrentQuestion] = useState<CurrentQuestionModel | null>(() => caseSnapshotState(initialSnapshot)?.question ?? null);
|
||||
const [questionSource, setQuestionSource] = useState<"focus" | "unavailable" | null>(() => caseSnapshotState(initialSnapshot)?.questionSource ?? null);
|
||||
const [caseStatus, setCaseStatus] = useState<RectificationCaseStatus | null>(() => caseSnapshotState(initialSnapshot)?.caseStatus ?? null);
|
||||
@@ -654,6 +646,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
latest_result?: unknown;
|
||||
current_question?: unknown;
|
||||
choice_card?: unknown;
|
||||
step_state?: unknown;
|
||||
turns?: unknown;
|
||||
question_source?: unknown;
|
||||
next_user_action?: { id?: unknown };
|
||||
@@ -679,6 +672,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
setCurrentQuestion(nextQuestion);
|
||||
setQuestionSource(questionSourceFromSnapshot(payload.question_source));
|
||||
setChoiceCard(nextChoice);
|
||||
setStepState(parseRectificationStepState(payload.step_state));
|
||||
setCaseStatus(nextCaseStatus);
|
||||
setNextUserActionId(nextActionId || null);
|
||||
setCaseSnapshotLoaded(true);
|
||||
@@ -1718,7 +1712,6 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
{showReadonlyRange && message.renderKey === latestSettledAssistant?.renderKey && candidateResult?.credibleRange && (
|
||||
<RectificationReadonlyRange
|
||||
range={candidateResult.credibleRange}
|
||||
onStop={readonly || busy ? undefined : submitStop}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -1779,15 +1772,22 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
<span className="rectification-adopt-status__hint">之后新建对话即按此时间排盘。</span>
|
||||
</div>
|
||||
)}
|
||||
{currentQuestion?.kind === "collect_spoken" && !readonly && !busy && (
|
||||
<button
|
||||
type="button"
|
||||
className="rectification-collect-stop"
|
||||
onClick={submitStop}
|
||||
>
|
||||
{CHOICE_STOP_LABEL}
|
||||
</button>
|
||||
)}
|
||||
<div className="rectification-composer-meta">
|
||||
{stepState ? (
|
||||
<p className="rectification-step-state" role="status">
|
||||
{stepState.headline} — {stepState.reason} — 下一步:{stepState.next}
|
||||
</p>
|
||||
) : null}
|
||||
{currentQuestion?.kind === "collect_spoken" && !readonly && !busy && (
|
||||
<button
|
||||
type="button"
|
||||
className="rectification-collect-stop"
|
||||
onClick={submitStop}
|
||||
>
|
||||
{CHOICE_STOP_LABEL}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<ChatComposer
|
||||
inputRef={composer}
|
||||
value={draft}
|
||||
|
||||
@@ -21,8 +21,13 @@ type RectificationChoiceCardProps = Readonly<{
|
||||
|
||||
export function RectificationChoiceCard(props: RectificationChoiceCardProps) {
|
||||
const [localSelected, setLocalSelected] = useState<ChoiceKey | "stop" | "skip_probe" | "">("");
|
||||
const [hoveredKey, setHoveredKey] = useState<ChoiceKey | "">("");
|
||||
const selectedKey = props.selectedKey || localSelected;
|
||||
const answered = Boolean(selectedKey);
|
||||
const impactKey = (selectedKey === "A" || selectedKey === "B" || selectedKey === "C" || selectedKey === "D"
|
||||
? selectedKey
|
||||
: hoveredKey) as ChoiceKey | "";
|
||||
const impactLine = impactKey ? props.card.answer_impact?.[impactKey] ?? "" : "";
|
||||
|
||||
function select(key: ChoiceKey) {
|
||||
if (props.pending || props.disabled || selectedKey) return;
|
||||
@@ -60,7 +65,12 @@ export function RectificationChoiceCard(props: RectificationChoiceCardProps) {
|
||||
{hidePrompt
|
||||
? <legend className="sr-only">{props.card.prompt}</legend>
|
||||
: <legend>{props.card.prompt}</legend>}
|
||||
{!hidePrompt && props.card.why ? <p className="rectification-choice-why">{props.card.why}</p> : null}
|
||||
{!hidePrompt && props.card.why_user ? (
|
||||
<details className="rectification-choice-why-user">
|
||||
<summary>为什么问这题</summary>
|
||||
<p>{props.card.why_user}</p>
|
||||
</details>
|
||||
) : null}
|
||||
<div className="birth-time-primary-choices">
|
||||
{props.card.options.map((option) => (
|
||||
<button
|
||||
@@ -68,6 +78,10 @@ export function RectificationChoiceCard(props: RectificationChoiceCardProps) {
|
||||
type="button"
|
||||
className="birth-time-choice-option is-primary"
|
||||
data-selected={selectedKey === option.key ? "true" : "false"}
|
||||
onMouseEnter={() => setHoveredKey(option.key)}
|
||||
onMouseLeave={() => setHoveredKey("")}
|
||||
onFocus={() => setHoveredKey(option.key)}
|
||||
onBlur={() => setHoveredKey("")}
|
||||
onClick={() => select(option.key)}
|
||||
>
|
||||
<strong>{option.key}.</strong> {option.label}
|
||||
@@ -84,6 +98,9 @@ export function RectificationChoiceCard(props: RectificationChoiceCardProps) {
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
{impactLine ? (
|
||||
<p className="rectification-choice-impact" role="status">{impactLine}</p>
|
||||
) : null}
|
||||
</fieldset>
|
||||
</section>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user