refactor(app): tighten chat and sync boundaries

This commit is contained in:
2026-07-01 14:03:25 +08:00
parent f6e7adbe96
commit 760e4fe343
19 changed files with 215 additions and 677 deletions
@@ -0,0 +1,77 @@
"use client";
/**
* ChatInsufficientCreditsBanner 积分不足横幅
*
* 何时显示:
* - 后端返回 cannotSendReason="insufficient_credits"
*
* 视觉规格(与设计稿对齐):
* - 粉→品红渐变背景(与 splash 渐变一致)
* - 大字号粉色/白色提示文案
* - 粉→品红渐变 pill 按钮 → 跳 /subscription?type=topup
*
* 业务关系:
* - 与订阅页 top up 入口行为一致
*
* 不做的事:
* - 不直接管理 state machinechat 机器不感知 UI 层)
*/
import { useRouter } from "next/navigation";
import { ROUTE_BUILDERS } from "@/router/routes";
import styles from "./chat-insufficient-credits-banner.module.css";
export interface ChatInsufficientCreditsBannerProps {
title?: string;
ctaLabel?: string;
/**
* 自定义点击回调(不传则默认 router.push(topup subscription)
* - 测试时可传 mock
* - 嵌入其他场景时(如 nested overlay)可传自定义
*/
onUnlock?: () => void;
}
export function ChatInsufficientCreditsBanner({
title = "Insufficient credits\nTop up to continue chatting",
ctaLabel = "Top up credits to continue",
onUnlock,
}: ChatInsufficientCreditsBannerProps) {
const router = useRouter();
const titleLines = title.split("\n");
const handleClick = () => {
if (onUnlock) {
onUnlock();
return;
}
router.push(ROUTE_BUILDERS.subscription("topup"));
};
return (
<div
className={styles.banner}
role="status"
aria-live="polite"
data-testid="chat-insufficient-credits-banner"
>
<p className={styles.text}>
{titleLines.map((line, index) => (
<span key={`${line}-${index}`}>
{line}
{index < titleLines.length - 1 ? <br /> : null}
</span>
))}
</p>
<button
type="button"
className={styles.cta}
onClick={handleClick}
aria-label={ctaLabel}
>
{ctaLabel}
</button>
</div>
);
}