95 lines
3.1 KiB
TypeScript
95 lines
3.1 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
type Dispatch,
|
|
type ReactNode,
|
|
createContext,
|
|
useContext,
|
|
useMemo,
|
|
} from "react";
|
|
import { useMachine } from "@xstate/react";
|
|
|
|
import { chatMachine } from "./chat-machine";
|
|
import type { ChatEvent, ChatState as MachineContext } from "./chat-machine";
|
|
import { appendPromotionMessage } from "./chat-promotion";
|
|
|
|
/**
|
|
* 对外暴露的 State 形状
|
|
*
|
|
* isGuest 字段已删 —— 由 chat-screen 派生自 `auth.loginStatus === "guest"`。
|
|
* 消费方(chat-screen)通过 `useAuthState()` 取 loginStatus,本地派生 isGuest。
|
|
*/
|
|
interface ChatState {
|
|
messages: MachineContext["messages"];
|
|
historyMessages: MachineContext["messages"];
|
|
promotion: MachineContext["promotion"];
|
|
isReplyingAI: boolean;
|
|
upgradePromptVisible: boolean;
|
|
upgradeReason: MachineContext["upgradeReason"];
|
|
/** history 初始加载完成标志(local → network → save 跑完) —— UI 可用此显示 loading */
|
|
historyLoaded: boolean;
|
|
unlockHistoryPromptVisible: boolean;
|
|
lockedHistoryCount: number;
|
|
isUnlockingHistory: boolean;
|
|
unlockHistoryError: string | null;
|
|
isUnlockingMessage: boolean;
|
|
unlockingMessageId: string | null;
|
|
unlockMessageError: string | null;
|
|
unlockPaywallRequest: MachineContext["unlockPaywallRequest"];
|
|
}
|
|
|
|
const ChatStateCtx = createContext<ChatState | null>(null);
|
|
const ChatDispatchCtx = createContext<Dispatch<ChatEvent> | null>(null);
|
|
|
|
export interface ChatProviderProps {
|
|
children: ReactNode;
|
|
}
|
|
|
|
export function ChatProvider({ children }: ChatProviderProps) {
|
|
const [state, send] = useMachine(chatMachine, {});
|
|
|
|
// 映射 XState 状态机快照 → 原 ChatState 形状
|
|
const chatState = useMemo<ChatState>(
|
|
() => ({
|
|
messages: appendPromotionMessage(
|
|
state.context.messages,
|
|
state.context.promotion,
|
|
),
|
|
historyMessages: state.context.messages,
|
|
promotion: state.context.promotion,
|
|
isReplyingAI: state.context.isReplyingAI,
|
|
upgradePromptVisible: state.context.upgradePromptVisible,
|
|
upgradeReason: state.context.upgradeReason,
|
|
historyLoaded: state.context.historyLoaded,
|
|
unlockHistoryPromptVisible: state.context.unlockHistoryPromptVisible,
|
|
lockedHistoryCount: state.context.lockedHistoryCount,
|
|
isUnlockingHistory: state.context.isUnlockingHistory,
|
|
unlockHistoryError: state.context.unlockHistoryError,
|
|
isUnlockingMessage: state.context.isUnlockingMessage,
|
|
unlockingMessageId: state.context.unlockingMessageId,
|
|
unlockMessageError: state.context.unlockMessageError,
|
|
unlockPaywallRequest: state.context.unlockPaywallRequest,
|
|
}),
|
|
[state],
|
|
);
|
|
|
|
return (
|
|
<ChatStateCtx.Provider value={chatState}>
|
|
<ChatDispatchCtx.Provider value={send}>{children}</ChatDispatchCtx.Provider>
|
|
</ChatStateCtx.Provider>
|
|
);
|
|
}
|
|
|
|
export function useChatState(): ChatState {
|
|
const ctx = useContext(ChatStateCtx);
|
|
if (!ctx) throw new Error("useChatState must be used inside <ChatProvider>");
|
|
return ctx;
|
|
}
|
|
|
|
export function useChatDispatch(): Dispatch<ChatEvent> {
|
|
const ctx = useContext(ChatDispatchCtx);
|
|
if (!ctx)
|
|
throw new Error("useChatDispatch must be used inside <ChatProvider>");
|
|
return ctx;
|
|
}
|