"use client"; import { type Dispatch, type ReactNode, useMemo } from "react"; import { createActorContext, shallowEqual } from "@xstate/react"; import type { SnapshotFrom } from "xstate"; import { chatMachine } from "./chat-machine"; import type { ChatEvent, ChatState as MachineContext } from "./chat-machine"; import { appendPromotionMessage } from "./helper/promotion"; /** * 对外暴露的 State 形状 * * isGuest 字段已删 —— 由 chat-screen 派生自 `auth.loginStatus === "guest"`。 * 消费方(chat-screen)通过 `useAuthState()` 取 loginStatus,本地派生 isGuest。 */ interface ChatState { characterId: string; messages: MachineContext["messages"]; historyMessages: MachineContext["messages"]; promotion: MachineContext["promotion"]; outgoingMessageRevision: number; isReplyingAI: boolean; upgradePromptVisible: boolean; upgradeReason: MachineContext["upgradeReason"]; /** history 初始加载完成标志(local → network → save 跑完) —— UI 可用此显示 loading */ historyLoaded: boolean; hasMoreHistory: boolean; isLoadingMoreHistory: boolean; unlockHistoryPromptVisible: boolean; lockedHistoryCount: number; isUnlockingHistory: boolean; unlockHistoryError: string | null; isUnlockingMessage: boolean; unlockingMessageId: string | null; unlockMessageError: string | null; unlockPaywallRequest: MachineContext["unlockPaywallRequest"]; } type ChatSnapshot = SnapshotFrom; type ChatSelector = (snapshot: ChatSnapshot) => T; const ChatActorContext = createActorContext(chatMachine); export interface ChatProviderProps { children: ReactNode; characterId: string; emptyChatGreeting: string; } export function ChatProvider({ children, characterId, emptyChatGreeting, }: ChatProviderProps) { return ( {children} ); } export function useChatState(): ChatState { const selected = useChatSelector(selectChatState, shallowEqual); const messages = useMemo( () => appendPromotionMessage(selected.historyMessages, selected.promotion), [selected.historyMessages, selected.promotion], ); return useMemo(() => ({ ...selected, messages }), [messages, selected]); } export function useChatDispatch(): Dispatch { return ChatActorContext.useActorRef().send; } export function useChatSelector( selector: ChatSelector, compare?: (previous: T, next: T) => boolean, ): T { return ChatActorContext.useSelector(selector, compare); } type SelectedChatState = Omit; function selectChatState(state: ChatSnapshot): SelectedChatState { return { characterId: state.context.characterId, historyMessages: state.context.messages, promotion: state.context.promotion, outgoingMessageRevision: state.context.outgoingMessageRevision, isReplyingAI: state.context.isReplyingAI, upgradePromptVisible: state.context.upgradePromptVisible, upgradeReason: state.context.upgradeReason, historyLoaded: state.context.historyLoaded, hasMoreHistory: state.context.historyLoaded && state.context.nextHistoryOffset < state.context.historyTotal, isLoadingMoreHistory: state.context.isLoadingMoreHistory, unlockHistoryPromptVisible: state.context.unlockHistoryPromptVisible, lockedHistoryCount: state.context.lockedHistoryCount, isUnlockingHistory: state.matches({ userSession: "unlockingHistory" }), unlockHistoryError: state.context.unlockHistoryError, isUnlockingMessage: state.matches({ userSession: "unlockingMessage" }), unlockingMessageId: state.context.unlockingMessage?.displayMessageId ?? null, unlockMessageError: state.context.unlockMessageError, unlockPaywallRequest: state.context.unlockPaywallRequest, }; }