refactor(chat): split screen orchestration
Docker Image / Build and Push Docker Image (push) Successful in 14m9s

This commit is contained in:
2026-07-08 15:33:24 +08:00
parent 18aa69bdd9
commit 21b6d346bd
15 changed files with 522 additions and 179 deletions
@@ -0,0 +1,63 @@
"use client";
import { type Dispatch, useEffect, useRef } from "react";
import type { LoginStatus } from "@/data/dto/auth";
import type { AuthEvent } from "@/stores/auth/auth-events";
export interface GuestLoginState {
hasInitialized: boolean;
isLoading: boolean;
loginStatus: LoginStatus;
}
export interface ShouldSubmitGuestLoginInput extends GuestLoginState {
alreadyRequested: boolean;
}
export interface UseChatGuestLoginInput extends GuestLoginState {
dispatch: Dispatch<AuthEvent>;
}
export function shouldSubmitGuestLogin({
alreadyRequested,
hasInitialized,
isLoading,
loginStatus,
}: ShouldSubmitGuestLoginInput): boolean {
if (!hasInitialized || isLoading) return false;
if (loginStatus !== "notLoggedIn") return false;
return !alreadyRequested;
}
export function useChatGuestLogin({
dispatch,
hasInitialized,
isLoading,
loginStatus,
}: UseChatGuestLoginInput): void {
const guestLoginRequestedRef = useRef(false);
useEffect(() => {
if (!hasInitialized || isLoading) return;
if (loginStatus !== "notLoggedIn") {
guestLoginRequestedRef.current = false;
return;
}
if (
!shouldSubmitGuestLogin({
alreadyRequested: guestLoginRequestedRef.current,
hasInitialized,
isLoading,
loginStatus,
})
) {
return;
}
guestLoginRequestedRef.current = true;
dispatch({ type: "AuthGuestLoginSubmitted" });
}, [dispatch, hasInitialized, isLoading, loginStatus]);
}
@@ -0,0 +1,57 @@
"use client";
import type { LoginStatus } from "@/data/dto/auth";
import { ROUTES } from "@/router/routes";
import { useAppNavigator } from "@/router/use-app-navigator";
import type { ChatUpgradeReason } from "@/stores/chat/chat-state";
import { useUserState } from "@/stores/user/user-context";
import {
getInsufficientCreditsMessageLimitView,
getInsufficientCreditsSubscriptionType,
shouldShowMessageLimitBanner,
type InsufficientCreditsMessageLimitView,
} from "../chat-screen.helpers";
export interface UseChatMessageLimitBannerInput {
upgradePromptVisible: boolean;
upgradeReason: ChatUpgradeReason | null;
loginStatus: LoginStatus;
}
export interface ChatMessageLimitBannerView
extends InsufficientCreditsMessageLimitView {
visible: boolean;
unlock: () => void;
}
export function useChatMessageLimitBanner({
upgradePromptVisible,
upgradeReason,
loginStatus,
}: UseChatMessageLimitBannerInput): ChatMessageLimitBannerView {
const navigator = useAppNavigator();
const userState = useUserState();
const view = getInsufficientCreditsMessageLimitView(loginStatus);
function unlock(): void {
if (view.action === "auth") {
navigator.openAuth(ROUTES.chat);
return;
}
navigator.openSubscription({
type: getInsufficientCreditsSubscriptionType(userState.isVip),
returnTo: "chat",
});
}
return {
...view,
visible: shouldShowMessageLimitBanner({
upgradePromptVisible,
upgradeReason,
}),
unlock,
};
}
@@ -0,0 +1,82 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import {
openChatInExternalBrowser,
recordExternalBrowserPromptShown,
resolveExternalBrowserPromptEligibility,
} from "@/lib/chat/chat_external_browser";
import {
type ExternalBrowserPromptState,
shouldStartExternalBrowserPrompt,
} from "../chat-screen.helpers";
const EXTERNAL_BROWSER_PROMPT_DELAY_MS = 3000;
export interface ExternalBrowserPromptController {
open: boolean;
close: () => void;
confirm: () => Promise<void>;
}
export function useExternalBrowserPrompt({
hasInitialized,
isLoading,
loginStatus,
}: ExternalBrowserPromptState): ExternalBrowserPromptController {
const [open, setOpen] = useState(false);
const promptShownRef = useRef(false);
useEffect(() => {
if (
!shouldStartExternalBrowserPrompt({
hasInitialized,
isLoading,
loginStatus,
})
) {
return;
}
let mounted = true;
let timer: number | undefined;
const init = async () => {
if (promptShownRef.current) return;
const eligibility = await resolveExternalBrowserPromptEligibility();
if (!mounted || !eligibility.canShow) return;
promptShownRef.current = true;
timer = window.setTimeout(() => {
if (!mounted) return;
setOpen(true);
void recordExternalBrowserPromptShown(eligibility.today);
}, EXTERNAL_BROWSER_PROMPT_DELAY_MS);
};
void init();
return () => {
mounted = false;
if (timer !== undefined) window.clearTimeout(timer);
};
}, [hasInitialized, isLoading, loginStatus]);
const close = useCallback(() => {
setOpen(false);
}, []);
const confirm = useCallback(async () => {
setOpen(false);
await openChatInExternalBrowser();
}, []);
return {
open,
close,
confirm,
};
}