feat(chat): add quota-exhausted banner for guest users

Introduce ChatQuotaExhaustedBanner component that displays a pink gradient
banner with an "Unlock your membership to continue" CTA when a guest user's
free chat quota has been exhausted. The banner is shown in ChatScreen when
isGuest is true, quota has loaded, and the chat state machine's
quotaExceededTrigger has been incremented by a quota guard. Default click
navigates to /subscription, with an optional onUnlock callback for
overrides/tests. Styles align with the existing splash and sidebar VIP
card visual language.
This commit is contained in:
2026-06-16 15:17:44 +08:00
parent 9ffa30cc03
commit dcb6312fa3
8 changed files with 153 additions and 39 deletions
+1 -12
View File
@@ -1,16 +1,5 @@
"use client";
/**
* ChatContext:基于 XState v5 的 React Context Provider
*
* 业务事件通过 `useMachine(chatMachine).send()` 派发。
* XState 自动处理 HTTP 副作用(invoke + fromPromise actors)。
*
* 兼容性:保持原 `useChatState()` / `useChatDispatch()` API。
* 内部将 XState 状态机快照映射回原 `ChatState` 形状。
*
* 注:WebSocket 长生命周期 actor 由 chat-screen.tsx 监听 auth 状态机驱动——
* 鉴权解耦(loginStatus 在 authchat 机器不感知鉴权)。
*/
import {
type Dispatch,
type ReactNode,
+8 -3
View File
@@ -33,7 +33,8 @@
import { setup, assign } from "xstate";
import { ChatStorage } from "@/data/storage/chat/chat_storage";
import { formatDate } from "@/utils/date";
import { formatDate, todayString } from "@/utils/date";
import { ChatState, initialState } from "./chat-state";
import type { ChatEvent } from "./chat-events";
@@ -85,8 +86,9 @@ export const chatMachine = setup({
// 持久化两个额度( fire-and-forget,不 await —— assign 是 sync
// daily quota 需要 today 字符串(让 ChatStorage 跨天判断 reset
const today = formatDate();
const today = todayString();
ChatStorage.getInstance().setGuestDailyChatQuota(newRemaining, today);
ChatStorage.getInstance().setGuestTotalQuota(newTotal);
console.log("[chat-machine] appendUserMessage", {
@@ -124,9 +126,12 @@ export const chatMachine = setup({
const newTotal = context.wsConnected
? context.guestTotalQuota
: Math.max(0, context.guestTotalQuota - 1);
const today = formatDate();
const today = todayString();
void ChatStorage.getInstance().setGuestDailyChatQuota(newRemaining, today);
void ChatStorage.getInstance().setGuestTotalQuota(newTotal);
console.log("[chat-machine] appendUserImage", {
oldMessagesCount: context.messages.length,
wsConnected: context.wsConnected,
-7
View File
@@ -3,13 +3,6 @@
*
* 注:GuestChatQuota 仍位于 chat-machine.ts(属于业务常量,与上下文配对紧密)。
*
* 历史:
* - `isGuest: boolean` 字段已删(由 chat-screen 派生自 `auth.loginStatus === "guest"`
* - `guestRemainingQuota` / `guestTotalQuota` default 改 0
* - 游客:chat-screen 派 `ChatInit` → readInitData 调 ChatStorage → onDone 条件赋
* - 非游客:永不被赋值(业务事实"非游客无消息限制")—— 0 - 1 = max(0, -1) = 0 天然 no-op
* - 0 同时表示"未初始化"和"已耗尽" —— UI 用 `if (isGuest && ...)` 天然过滤
*
* 新增 `wsConnected: boolean`(仅 userSession 期间 true):
* - WS 已连 → 消息发送走 WS,不消耗次数
* - WS 未连 → 消息发送走 HTTP,消耗次数