refactor(chat): remove QuotaDialog and related state/effects

Drop the QuotaDialog component invocation in chat-screen along with
its useState/useEffect wiring, and delete the component files. Update
JSDoc in chat-screen and routes to reflect the removed consumer.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-15 16:24:25 +08:00
parent 1f3980d461
commit 9b28404673
4 changed files with 2 additions and 211 deletions
+1 -45
View File
@@ -7,14 +7,12 @@
* 职责(本文件):
* - **订阅 auth 状态机**loginStatus)—— 派生 isGuest / **派鉴权生命周期事件**给 chat 机器
* - 屏幕生命周期事件(init / visible / invisible
* - 配额告警监听(**仅**游客 —— "非游客无消息限制"业务事实)
* - **不**直接管理 WebSocket / **不**直接读 AuthStorage(除 token 取值)
*
* 子组件职责:
* - ChatHeader:顶部栏(游客 banner / 菜单按钮)
* - ChatArea:消息列表
* - ChatInputBar:输入栏(文字 + 发送 + More)
* - QuotaDialog:配额告警弹窗(**仅**游客)
* - PwaInstallOverlayPWA 安装提示触发器
* - BrowserHintOverlay:浏览器提示
*
@@ -26,14 +24,13 @@
* - `ChatLogout` → 登出(机器自动 cleanup WS actor
* - chat 机器**内部**用 fromCallback actor 完整管理 WS 生命周期
*/
import { useEffect, useRef, useState } from "react";
import { useEffect, useRef } from "react";
import Image from "next/image";
import { useAuthState } from "@/stores/auth/auth-context";
import type { LoginStatus } from "@/models/auth/login-status";
import { AuthStorage } from "@/data/storage/auth/auth_storage";
import { useChatDispatch, useChatState } from "@/stores/chat/chat-context";
import { GuestChatQuota } from "@/stores/chat";
import { MobileShell } from "@/app/_components/core/mobile-shell";
@@ -42,7 +39,6 @@ import { ChatArea } from "./chat-area";
import { ChatHeader } from "./chat-header";
import { ChatInputBar } from "./chat-input-bar";
import { PwaInstallOverlay } from "./pwa-install-overlay";
import { QuotaDialog } from "./quota-dialog";
import styles from "./chat-screen.module.css";
/**
@@ -88,9 +84,6 @@ export function ChatScreen() {
// isGuest 派生(chat-screen 是**唯一**知道这个值的地方 —— chat 机器无 isGuest 字段)
const isGuest = deriveIsGuest(authState.loginStatus);
const [quotaDialogOpen, setQuotaDialogOpen] = useState(false);
const [quotaExhausted, setQuotaExhausted] = useState(false);
// ─────────────────────────────────────────────────────────────
// 屏幕生命周期 + 派初次鉴权生命周期事件
// ─────────────────────────────────────────────────────────────
@@ -121,36 +114,6 @@ export function ChatScreen() {
dispatchAuthLifecycle(authState.loginStatus, chatDispatch);
}, [authState.loginStatus, chatDispatch]);
// ─────────────────────────────────────────────────────────────
// 配额耗尽触发器(**仅**游客 —— "非游客无消息限制"
// ─────────────────────────────────────────────────────────────
const prevTriggerRef = useRef(state.quotaExceededTrigger);
useEffect(() => {
if (!isGuest) return; // ← 防御:非游客 backend 不会发 ChatQuotaExceeded
if (
state.quotaExceededTrigger !== prevTriggerRef.current &&
state.quotaExceededTrigger > 0
) {
setQuotaExhausted(true);
setQuotaDialogOpen(true);
}
prevTriggerRef.current = state.quotaExceededTrigger;
}, [state.quotaExceededTrigger, isGuest]);
// 配额警告(剩余 = 5)—— **仅**游客
const prevRemainingRef = useRef(state.guestRemainingQuota);
useEffect(() => {
if (
isGuest && // ← 派生自 auth.loginStatus
state.guestRemainingQuota === GuestChatQuota.warningThreshold &&
prevRemainingRef.current !== state.guestRemainingQuota
) {
setQuotaExhausted(false);
setQuotaDialogOpen(true);
}
prevRemainingRef.current = state.guestRemainingQuota;
}, [state.guestRemainingQuota, isGuest]);
return (
<MobileShell>
<div className={styles.shell}>
@@ -183,13 +146,6 @@ export function ChatScreen() {
{/* PWA 安装提示触发器(无可见 UI3.5s 后弹 dialog */}
<PwaInstallOverlay />
{/* 配额告警弹窗(**仅**游客,gate 在 isGuest */}
<QuotaDialog
open={quotaDialogOpen}
onClose={() => setQuotaDialogOpen(false)}
isExhausted={quotaExhausted}
/>
</div>
</MobileShell>
);
@@ -1,85 +0,0 @@
/* QuotaDialog 配额提示弹窗样式 */
.overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 50;
}
.dialog {
width: calc(100% - 40px);
max-width: 360px;
background: var(--color-surface, #1f1a2e);
border-radius: var(--radius-lg, 12px);
padding: var(--spacing-5, 20px);
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.icon {
width: 64px;
height: 64px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
margin-bottom: var(--spacing-4, 16px);
}
.iconExhausted {
background: rgba(255, 80, 80, 0.2);
color: #ff5050;
}
.iconWarning {
background: rgba(248, 77, 150, 0.2);
color: var(--color-accent, #f84d96);
}
.title {
font-size: var(--font-size-lg, 18px);
font-weight: 600;
margin: 0 0 var(--spacing-2, 8px);
color: var(--color-text-primary, #fff);
}
.content {
font-size: var(--font-size-sm, 14px);
line-height: 1.5;
color: var(--color-text-secondary, #9e9e9e);
margin: 0 0 var(--spacing-5, 20px);
}
.actions {
display: flex;
gap: var(--spacing-3, 12px);
width: 100%;
}
.btn {
flex: 1 1 auto;
height: 40px;
border: 0;
border-radius: var(--radius-md, 8px);
font-size: var(--font-size-base, 14px);
font-weight: 600;
cursor: pointer;
}
.btnSecondary {
background: transparent;
color: var(--color-text-primary, #fff);
}
.btnPrimary {
background: var(--color-accent, #f84d96);
color: #fff;
}
-80
View File
@@ -1,80 +0,0 @@
"use client";
/**
* QuotaDialog 配额提示弹窗
*
* 原始 Dart: lib/ui/chat/widgets/quota_dialog.dart130 行)
*
* 两种模式:
* - 警告模式(isExhausted=false):剩余配额 < 5 时,提示用户登录
* - 耗尽模式(isExhausted=true):配额已用完,强制登录
*/
import { useRouter } from "next/navigation";
import { ROUTES } from "@/router/routes";
import styles from "./quota-dialog.module.css";
export interface QuotaDialogProps {
open: boolean;
onClose: () => void;
isExhausted: boolean;
}
export function QuotaDialog({ open, onClose, isExhausted }: QuotaDialogProps) {
const router = useRouter();
if (!open) return null;
const title = isExhausted
? "Daily quota exhausted"
: "Login for unlimited access!";
const content = isExhausted
? "Login to continue chatting~"
: "Dear, sign up now and all your chats with Elio will be saved forever. In guest mode, your chat history will be deleted when you leave, and Elio will forget you.";
const handleLogin = () => {
onClose();
router.push(ROUTES.auth);
};
return (
<div
className={styles.overlay}
role="dialog"
aria-modal="true"
aria-labelledby="quota-dialog-title"
>
<div className={styles.dialog}>
<div
className={`${styles.icon} ${isExhausted ? styles.iconExhausted : styles.iconWarning}`}
aria-hidden="true"
>
{isExhausted ? "🚫" : "⚠️"}
</div>
<h2 id="quota-dialog-title" className={styles.title}>
{title}
</h2>
<p className={styles.content}>{content}</p>
<div className={styles.actions}>
<button
type="button"
className={`${styles.btn} ${styles.btnSecondary}`}
onClick={onClose}
>
Cancel
</button>
<button
type="button"
className={`${styles.btn} ${styles.btnPrimary}`}
onClick={handleLogin}
>
Go to Login
</button>
</div>
</div>
</div>
);
}