Bare `/` was treating the latest history session as active, so a returning account could auto-open a failed rectification Case and left-align the starter home. Co-authored-by: Cursor <cursoragent@cursor.com>
122 lines
4.5 KiB
TypeScript
122 lines
4.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.
|
|
*/
|
|
|
|
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;
|
|
}
|