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
+33 -127
View File
@@ -1,18 +1,10 @@
"use client";
import { useEffect, useRef, useState } from "react";
import Image from "next/image";
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
import { useChatDispatch, useChatState } from "@/stores/chat/chat-context";
import { useUserState } from "@/stores/user/user-context";
import { useChatState } from "@/stores/chat/chat-context";
import { ROUTES } from "@/router/routes";
import { useAppNavigator } from "@/router/use-app-navigator";
import {
openChatInExternalBrowser,
recordExternalBrowserPromptShown,
resolveExternalBrowserPromptEligibility,
} from "@/lib/chat/chat_external_browser";
import { MobileShell } from "@/app/_components/core";
@@ -22,32 +14,26 @@ import {
ChatHeader,
ChatInputBar,
ChatInsufficientCreditsBanner,
ChatUnlockDialogs,
ExternalBrowserDialog,
FirstRechargeOfferBanner,
HistoryUnlockDialog,
InsufficientCreditsDialog,
PwaInstallOverlay,
} from "./components";
import {
deriveIsGuest,
getInsufficientCreditsMessageLimitView,
getInsufficientCreditsSubscriptionType,
isChatDevelopmentEnvironment,
shouldStartExternalBrowserPrompt,
} from "./chat-screen.helpers";
import { useFirstRechargeOfferBanner } from "./hooks/use-first-recharge-offer-banner";
import { useChatUnlockNavigationFlow } from "./hooks/use-chat-unlock-navigation-flow";
import { useChatGuestLogin } from "./hooks/use-chat-guest-login";
import { useChatMessageLimitBanner } from "./hooks/use-chat-message-limit-banner";
import { useExternalBrowserPrompt } from "./hooks/use-external-browser-prompt";
import styles from "./components/chat-screen.module.css";
export function ChatScreen() {
const state = useChatState();
const chatDispatch = useChatDispatch();
const authState = useAuthState();
const authDispatch = useAuthDispatch();
const userState = useUserState();
const navigator = useAppNavigator();
const [showExternalBrowserDialog, setShowExternalBrowserDialog] =
useState(false);
const {
unlockPaywallRequest,
requestMessageUnlock,
@@ -59,100 +45,33 @@ export function ChatScreen() {
const isGuest = deriveIsGuest(authState.loginStatus);
// 发送能力由后端统一返回,当前只处理积分不足引导。
const showMessageLimitBanner =
state.upgradePromptVisible &&
state.upgradeReason === "insufficient_credits";
const messageLimitView = getInsufficientCreditsMessageLimitView(
authState.loginStatus,
);
const messageLimitBanner = useChatMessageLimitBanner({
upgradePromptVisible: state.upgradePromptVisible,
upgradeReason: state.upgradeReason,
loginStatus: authState.loginStatus,
});
const firstRechargeOfferBanner = useFirstRechargeOfferBanner({
historyLoaded: state.historyLoaded,
loginStatus: authState.loginStatus,
});
const shouldShowPwaInstall = state.historyLoaded && state.messages.length >= 10;
const externalBrowserPrompt = useExternalBrowserPrompt({
hasInitialized: authState.hasInitialized,
isLoading: authState.isLoading,
loginStatus: authState.loginStatus,
});
const externalBrowserPromptShownRef = useRef(false);
const guestLoginRequestedRef = useRef(false);
useEffect(() => {
if (!authState.hasInitialized || authState.isLoading) return;
if (authState.loginStatus !== "notLoggedIn") {
guestLoginRequestedRef.current = false;
return;
}
if (guestLoginRequestedRef.current) return;
guestLoginRequestedRef.current = true;
authDispatch({ type: "AuthGuestLoginSubmitted" });
}, [
authDispatch,
authState.hasInitialized,
authState.isLoading,
authState.loginStatus,
]);
useEffect(() => {
if (
!shouldStartExternalBrowserPrompt({
hasInitialized: authState.hasInitialized,
isLoading: authState.isLoading,
loginStatus: authState.loginStatus,
})
) {
return;
}
let mounted = true;
let timer: number | undefined;
const init = async () => {
if (externalBrowserPromptShownRef.current) return;
const eligibility = await resolveExternalBrowserPromptEligibility();
if (!mounted || !eligibility.canShow) return;
externalBrowserPromptShownRef.current = true;
timer = window.setTimeout(() => {
if (!mounted) return;
setShowExternalBrowserDialog(true);
void recordExternalBrowserPromptShown(eligibility.today);
}, 3000);
};
void init();
return () => {
mounted = false;
if (timer !== undefined) window.clearTimeout(timer);
};
}, [
authState.hasInitialized,
authState.isLoading,
authState.loginStatus,
]);
async function handleOpenExternalBrowser(): Promise<void> {
setShowExternalBrowserDialog(false);
await openChatInExternalBrowser();
}
useChatGuestLogin({
dispatch: authDispatch,
hasInitialized: authState.hasInitialized,
isLoading: authState.isLoading,
loginStatus: authState.loginStatus,
});
function handleUnlockPrivateMessage(messageId: string): void {
requestMessageUnlock(messageId, "private");
}
function handleMessageLimitUnlock(): void {
if (messageLimitView.action === "auth") {
navigator.openAuth(ROUTES.chat);
return;
}
navigator.openSubscription({
type: getInsufficientCreditsSubscriptionType(userState.isVip),
returnTo: "chat",
});
}
function handleUnlockVoiceMessage(messageId: string): void {
requestMessageUnlock(messageId, "voice");
}
@@ -187,19 +106,18 @@ export function ChatScreen() {
<ChatArea
messages={state.messages}
isReplyingAI={state.isReplyingAI}
isGuest={isGuest}
isUnlockingMessage={state.isUnlockingMessage}
unlockingMessageId={state.unlockingMessageId}
onUnlockPrivateMessage={handleUnlockPrivateMessage}
onUnlockVoiceMessage={handleUnlockVoiceMessage}
/>
{showMessageLimitBanner ? (
{messageLimitBanner.visible ? (
<ChatInsufficientCreditsBanner
title={messageLimitView.title}
description={messageLimitView.description}
ctaLabel={messageLimitView.ctaLabel}
onUnlock={handleMessageLimitUnlock}
title={messageLimitBanner.title}
description={messageLimitBanner.description}
ctaLabel={messageLimitBanner.ctaLabel}
onUnlock={messageLimitBanner.unlock}
/>
) : (
<ChatInputBar disabled={!state.historyLoaded} />
@@ -213,27 +131,15 @@ export function ChatScreen() {
<PwaInstallOverlay enabled={shouldShowPwaInstall} />
<ExternalBrowserDialog
open={showExternalBrowserDialog}
onClose={() => setShowExternalBrowserDialog(false)}
onConfirm={() => void handleOpenExternalBrowser()}
open={externalBrowserPrompt.open}
onClose={externalBrowserPrompt.close}
onConfirm={() => void externalBrowserPrompt.confirm()}
/>
<HistoryUnlockDialog
open={state.unlockHistoryPromptVisible}
lockedCount={state.lockedHistoryCount}
isLoading={state.isUnlockingHistory}
errorMessage={state.unlockHistoryError}
onClose={() => chatDispatch({ type: "ChatUnlockHistoryDismissed" })}
onConfirm={() => chatDispatch({ type: "ChatUnlockHistoryConfirmed" })}
/>
<InsufficientCreditsDialog
open={unlockPaywallRequest !== null}
creditBalance={unlockPaywallRequest?.creditBalance ?? 0}
requiredCredits={unlockPaywallRequest?.requiredCredits ?? 0}
shortfallCredits={unlockPaywallRequest?.shortfallCredits ?? 0}
onClose={closeInsufficientCreditsDialog}
onConfirm={confirmInsufficientCreditsDialog}
<ChatUnlockDialogs
unlockPaywallRequest={unlockPaywallRequest}
onCloseInsufficientCreditsDialog={closeInsufficientCreditsDialog}
onConfirmInsufficientCreditsDialog={confirmInsufficientCreditsDialog}
/>
</div>
</MobileShell>