refactor(home): split bootstrap, recovery polling, and shells

Move the home startup sequence and consultation recovery poll into
plain functions, and the error and onboarding screens into components.
Landing rules, the 20s fatal screen, retry, and the chart settings
deep link stay the same.
This commit is contained in:
jesse-ux
2026-09-25 02:59:11 +08:00
parent 9167d75e74
commit da35b3e4af
19 changed files with 1972 additions and 632 deletions
@@ -0,0 +1,24 @@
"use client";
import Link from "next/link";
export function HomeBootstrapError({
accountError,
onRetry,
}: {
readonly accountError: string;
readonly onRetry: () => void;
}) {
return (
<main className="app-loading app-loading-error">
<div className="app-loading-content" role="alert">
<strong>暂时无法进入 Jyotisha</strong>
<span>{accountError}</span>
<div className="app-loading-actions">
<button className="button-primary" type="button" onClick={onRetry}>重试</button>
<Link className="button-secondary" href="/login">返回登录</Link>
</div>
</div>
</main>
);
}
@@ -0,0 +1,148 @@
"use client";
import dynamic from "next/dynamic";
import type { FormEvent } from "react";
import { BirthTimeAssessmentOverlay, type BirthTimeAssessmentPhase } from "@/components/birth-time-assessment-overlay";
import { BirthLocationFields } from "@/components/birth-location-fields";
import { BirthTimeIntakeFields } from "@/components/birth-time-intake";
import { OnboardingChatMessage } from "@/components/onboarding-chat-message";
import type { BirthTimeGuidedController } from "@/hooks/use-birth-time-guided-journey";
import {
applyBirthTimeDraftPatch,
birthTimeDraftReadyHint,
isBirthTimeDraftReady,
} from "@/lib/birth-time-intake-model";
import type { JourneyClientResponse } from "@/lib/birth-time-journey-client";
import {
birthQuestion,
formatBirthMoment,
placeQuestion,
selectedBirthPlace,
} from "@/lib/home-profile";
import { presetOnboardingMessage, type OnboardingStep, type Profile } from "@/lib/home-types";
const BirthTimeRectification = dynamic(
() => import("@/components/birth-time-rectification").then((module) => module.BirthTimeRectification),
{
ssr: false,
loading: () => <p className="birth-time-assistant-intent" role="status">正在加载出生时间评估...</p>,
},
);
export type HomeOnboardingShellProps = {
readonly profileComplete: boolean;
readonly profileDraft: Profile;
readonly setProfileDraft: (update: Profile | ((current: Profile) => Profile)) => void;
readonly onboardingStep: OnboardingStep;
readonly presetMessageLength: number;
readonly currentOnboardingMessage: string;
readonly onboardingJustCompleted: boolean;
readonly onboardingCardReady: boolean;
readonly presetMessageFinished: boolean;
readonly birthTimeJourney: JourneyClientResponse | null;
readonly birthTimeAssessmentPhase: BirthTimeAssessmentPhase | null;
readonly birthTimeError: string;
readonly birthTimeGuided: BirthTimeGuidedController;
readonly accountError: string;
readonly profileSaving: boolean;
readonly saveOnboardingBirth: (event: FormEvent<HTMLFormElement>) => void;
readonly saveOnboardingPlace: (event: FormEvent<HTMLFormElement>) => void;
readonly retryBirthTimeAssessment: () => void;
};
export function HomeOnboardingShell({
profileComplete,
profileDraft,
setProfileDraft,
onboardingStep,
presetMessageLength,
currentOnboardingMessage,
onboardingJustCompleted,
onboardingCardReady,
presetMessageFinished,
birthTimeJourney,
birthTimeAssessmentPhase,
birthTimeError,
birthTimeGuided,
accountError,
profileSaving,
saveOnboardingBirth,
saveOnboardingPlace,
retryBirthTimeAssessment,
}: HomeOnboardingShellProps) {
const birthTimeContinueHint = onboardingStep === "birth" ? birthTimeDraftReadyHint(profileDraft) : "";
return (
<>
<OnboardingChatMessage role="assistant" text={presetOnboardingMessage} streaming={onboardingStep === "name" && !profileComplete} length={presetMessageLength} />
{(onboardingStep !== "name" || profileComplete) && <OnboardingChatMessage role="user" text={profileDraft.name.trim()} />}
{(onboardingStep !== "name" || profileComplete) && <OnboardingChatMessage role="assistant" text={birthQuestion(profileDraft.name.trim())} streaming={onboardingStep === "birth" && !profileComplete} length={presetMessageLength} />}
{(onboardingStep === "place" || onboardingStep === "rectification" || profileComplete) && <OnboardingChatMessage role="user" text={formatBirthMoment(profileDraft)} />}
{(onboardingStep === "place" || onboardingStep === "rectification" || profileComplete) && <OnboardingChatMessage role="assistant" text={placeQuestion(profileDraft)} streaming={onboardingStep === "place" && !profileComplete} length={presetMessageLength} />}
{!profileComplete && onboardingStep === "rectification" && selectedBirthPlace(profileDraft) && <OnboardingChatMessage role="user" text={selectedBirthPlace(profileDraft)?.label ?? ""} />}
{!profileComplete && onboardingStep === "rectification" && birthTimeJourney && <OnboardingChatMessage role="assistant" text={currentOnboardingMessage} streaming length={presetMessageLength} phraseSafe />}
{profileComplete && onboardingJustCompleted && selectedBirthPlace(profileDraft) && <OnboardingChatMessage role="user" text={selectedBirthPlace(profileDraft)?.label ?? ""} />}
{profileComplete && onboardingJustCompleted && <OnboardingChatMessage role="assistant" text={currentOnboardingMessage} streaming length={presetMessageLength} />}
{!profileComplete && onboardingStep === "birth" && onboardingCardReady && (
<div className="onboarding-card-reveal">
<div className="onboarding-card-reveal-inner">
<form className="profile-form onboarding-card onboarding-step-card birth-time-transition-card" onSubmit={saveOnboardingBirth} aria-busy={birthTimeAssessmentPhase !== null}>
<div className="onboarding-card-heading">
<div className="onboarding-card-heading-copy">
<b>出生日期与时间</b>
</div>
</div>
<fieldset className="birth-time-transition-fields" disabled={birthTimeAssessmentPhase !== null}>
<BirthTimeIntakeFields value={profileDraft} onPatch={(patch) => setProfileDraft((current) => applyBirthTimeDraftPatch(current, patch))} />
{accountError && <p className="form-error" role="alert">{accountError}</p>}
<div className="onboarding-card-actions">
{birthTimeContinueHint ? <p className="onboarding-card-action-hint">{birthTimeContinueHint}</p> : <span />}
<button className="button-primary" type="submit" disabled={profileSaving || !isBirthTimeDraftReady(profileDraft)}>{profileSaving ? "保存中" : "继续"}</button>
</div>
</fieldset>
<BirthTimeAssessmentOverlay phase={birthTimeAssessmentPhase} />
</form>
</div>
</div>
)}
{!profileComplete && onboardingStep === "place" && onboardingCardReady && (
<div className="onboarding-card-reveal">
<div className="onboarding-card-reveal-inner">
<form className="profile-form onboarding-card onboarding-step-card birth-time-transition-card" onSubmit={saveOnboardingPlace} aria-busy={birthTimeAssessmentPhase !== null}>
<div className="onboarding-card-heading"><b>出生地点</b><small>搜索全球城市或区县,系统会匹配出生当日的时区。</small></div>
<fieldset className="birth-time-transition-fields" disabled={birthTimeAssessmentPhase !== null}>
<BirthLocationFields value={profileDraft} onChange={setProfileDraft} />
{accountError && <p className="form-error" role="alert">{accountError}</p>}
<div className="onboarding-card-actions"><button className="button-primary" type="submit" disabled={profileSaving || !selectedBirthPlace(profileDraft)}>{profileSaving ? "保存中" : "确定"}</button></div>
</fieldset>
<BirthTimeAssessmentOverlay phase={birthTimeAssessmentPhase} />
</form>
</div>
</div>
)}
{!profileComplete && onboardingStep === "rectification" && presetMessageFinished && birthTimeJourney && (
<div className="onboarding-card-reveal">
<div className="onboarding-card-reveal-inner">
<BirthTimeRectification
journey={birthTimeJourney}
controller={birthTimeGuided}
externalError={birthTimeError}
/>
</div>
</div>
)}
{!profileComplete && onboardingStep === "rectification" && presetMessageFinished && !birthTimeJourney && (
<div className="onboarding-card birth-time-retry-card" role="status">
<b>出生时间尚未完成评估</b>
<p>{birthTimeError || "资料已经保留,但暂时无法恢复校正进度。系统不会应用未经验证的具体时间。"}</p>
<button className="button-primary" type="button" disabled={profileSaving} onClick={() => void retryBirthTimeAssessment()}>{profileSaving ? "评估中" : "重新评估"}</button>
</div>
)}
{!profileComplete && onboardingStep === "name" && accountError && <p className="form-error onboarding-inline-error" role="alert">{accountError}</p>}
</>
);
}