/** * 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. */ import { parseSessionUrlQuery } from "./chat-session-url.ts"; export type BootstrapPhase = "account" | "prepare"; export type LandingSession = Readonly<{ id: string; sessionType: string; messages: readonly unknown[]; }>; export type LandingUrlAction = "keep" | "replace-selected" | "replace-clear" | "none"; /** * A rectification Case is opened only when the address bar names that session * (`?c=`), including refresh and a login return that restored the URL. Bare `/` * after login must not open the latest history item in the background. */ export function shouldAutoOpenRectificationSession(input: { readonly sessionType: string | undefined; readonly sessionId: string | undefined; readonly search: string; }): boolean { if (input.sessionType !== "birth_time_rectification" || !input.sessionId) return false; return parseSessionUrlQuery(input.search).sessionId === input.sessionId; } /** * Default `/` landing prefers an empty consultation so the starter home can * center. Deep links and login-return URLs keep the requested session. */ export function resolveStarterHomeLandingSessionId( sessions: readonly LandingSession[], selectedId: string, urlAction: LandingUrlAction, ): string { if (urlAction === "keep" || urlAction === "replace-selected") return selectedId; const selected = sessions.find((session) => session.id === selectedId); if (selected?.sessionType !== "birth_time_rectification") return selectedId; const emptyConsultation = sessions.find( (session) => session.sessionType === "consultation" && session.messages.length === 0, ); if (emptyConsultation) return emptyConsultation.id; const anyConsultation = sessions.find((session) => session.sessionType === "consultation"); return anyConsultation?.id ?? selectedId; } export function starterHomeLandingNeedsConsultation( sessions: readonly LandingSession[], landingId: string, urlAction: LandingUrlAction, ): boolean { if (urlAction === "keep" || urlAction === "replace-selected") return false; const landing = sessions.find((session) => session.id === landingId); return !landing || landing.sessionType === "birth_time_rectification"; } 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; /** A URL-named rectification session must be opened and hydrated before 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; }