Files
cozsweet-frontend-nextjs/src/stores/chat/chat-context.tsx
T

90 lines
2.9 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";
/**
* 对外暴露的 State 形状
*
* isGuest 字段已删 —— 由 chat-screen 派生自 `auth.loginStatus === "guest"`。
* 消费方(chat-screen)通过 `useAuthState()` 取 loginStatus,本地派生 isGuest。
*/
interface ChatState {
messages: MachineContext["messages"];
isReplyingAI: boolean;
/** 游客剩余配额(次/天)—— 业务展示用,鉴权判断由 chat-screen 派生 loginStatus */
guestRemainingQuota: number;
/** 游客总配额 */
guestTotalQuota: number;
quotaExceededTrigger: number;
paywallTriggered: boolean;
paywallReason: MachineContext["paywallReason"];
paywallDetail: MachineContext["paywallDetail"];
isLoadingMore: boolean;
hasMore: boolean;
historyOffset: number;
/** 游客配额加载完成标志(仅 guestSession 期间) —— UI 可用此显示 loading */
quotaLoaded: boolean;
/** history 初始加载完成标志(local → network → save 跑完) —— UI 可用此显示 loading */
historyLoaded: boolean;
}
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: state.context.messages,
isReplyingAI: state.context.isReplyingAI,
guestRemainingQuota: state.context.guestRemainingQuota,
guestTotalQuota: state.context.guestTotalQuota,
quotaExceededTrigger: state.context.quotaExceededTrigger,
paywallTriggered: state.context.paywallTriggered,
paywallReason: state.context.paywallReason,
paywallDetail: state.context.paywallDetail,
isLoadingMore: state.context.isLoadingMore,
hasMore: state.context.hasMore,
historyOffset: state.context.historyOffset,
quotaLoaded: state.context.quotaLoaded,
historyLoaded: state.context.historyLoaded,
}),
[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;
}