Files
Jyotisha/frontend/src/lib/home-bootstrap.ts
T
Jesse_Chen 53017d4153 fix(rectification): reveal the surface once — hydrate the Case before the switch, never remount, static entry feedback
Opening a rectification Case used to switch sessions first and read the
turns afterwards, so the reader saw a plain transcript, then an empty
panel, then a remount when the turns arrived (and again after the first
turn settled, because the panel key carried a ready/loading suffix).
The surface hook now reads turns and snapshot in one Case request under
the home reveal budget and only then makes the session active; the
sidebar defers the switch the same way; a session selected at bootstrap
(deep link, refresh) is hydrated during the prepare phase so the reveal
shows the surface itself; the panel key is the session/Case binding only,
later turns fill an empty transcript as a prop update, and unmounting
aborts any stream or snapshot read. The homepage card and the sidebar row
say 正在打开 statically while the Case opens — no spinner after the reveal.

BUG-505

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
2026-09-03 07:07:31 +00:00

68 lines
2.5 KiB
TypeScript

/**
* Home bootstrap reveal policy.
*
* The loading screen owns every wait before the page appears. Phase "account"
* fetches account, model catalog and the session list; phase "prepare" fetches
* the starter questions, today's starlanguage and the rectification entry
* summary in parallel. The page is revealed once every applicable item has
* settled, or when the prepare budget runs out — never with a spinner inside.
*/
export type BootstrapPhase = "account" | "prepare";
export const BOOTSTRAP_PREPARE_TIMEOUT_MS = 4000;
export const SESSION_PREFETCH_COUNT = 5;
export type BootstrapPrepareState = Readonly<{
profileComplete: boolean;
onboardingSettled: boolean;
dailyStarlanguageApplicable: boolean;
dailyStarlanguageSettled: boolean;
entrySummarySettled: boolean;
/** The selected session is a rectification session: its Case must be open and hydrated before the reveal. */
rectificationApplicable?: boolean;
rectificationSettled?: boolean;
}>;
export function bootstrapPrepareSettled(state: BootstrapPrepareState): boolean {
if (!state.entrySummarySettled) return false;
if (state.rectificationApplicable && !state.rectificationSettled) return false;
if (state.profileComplete && !state.onboardingSettled) return false;
if (state.dailyStarlanguageApplicable && !state.dailyStarlanguageSettled) return false;
return true;
}
export function bootstrapRevealDelayMs(now: number, prepareStartedAt: number | null, settled: boolean): number {
if (settled) return 0;
if (prepareStartedAt === null) return BOOTSTRAP_PREPARE_TIMEOUT_MS;
return Math.max(0, BOOTSTRAP_PREPARE_TIMEOUT_MS - (now - prepareStartedAt));
}
export function bootstrapLoadingCopy(phase: BootstrapPhase): Readonly<{ title: string; detail: string }> {
return phase === "prepare"
? { title: "正在准备对话", detail: "整理推荐问题、今日星语与对话记录" }
: { title: "正在载入账户", detail: "同步个人资料与对话记录" };
}
export type PrefetchableSession = Readonly<{
id: string;
sessionType: string;
messagesHydrated?: boolean;
}>;
export function sessionIdsToPrefetch(
sessions: readonly PrefetchableSession[],
activeSessionId: string,
limit = SESSION_PREFETCH_COUNT,
): string[] {
const ids: string[] = [];
for (const session of sessions) {
if (ids.length >= limit) break;
if (session.id === activeSessionId) continue;
if (session.messagesHydrated) continue;
if (session.sessionType !== "consultation") continue;
ids.push(session.id);
}
return ids;
}