refactor(chat): remove weekly limit flow
This commit is contained in:
@@ -58,19 +58,13 @@ export function ChatScreen() {
|
|||||||
authState.loginStatus !== "notLoggedIn" &&
|
authState.loginStatus !== "notLoggedIn" &&
|
||||||
authState.loginStatus !== "guest";
|
authState.loginStatus !== "guest";
|
||||||
|
|
||||||
// 消息数量限制由后端统一返回 lockDetail,前端不再处理本地额度。
|
// 发送能力由后端统一返回,当前只处理积分不足引导。
|
||||||
const showMessageLimitBanner =
|
const showMessageLimitBanner =
|
||||||
state.upgradePromptVisible &&
|
state.upgradePromptVisible &&
|
||||||
(state.upgradeReason === "weekly_limit" ||
|
state.upgradeReason === "insufficient_credits";
|
||||||
state.upgradeReason === "insufficient_credits");
|
|
||||||
const messageLimitTitle =
|
const messageLimitTitle =
|
||||||
state.upgradeReason === "insufficient_credits"
|
"Insufficient credits\nTop up to continue chatting";
|
||||||
? "Insufficient credits\nTop up to continue chatting"
|
const messageLimitCtaLabel = "Top up credits to continue";
|
||||||
: "The limit for free chat times\nhas been reached";
|
|
||||||
const messageLimitCtaLabel =
|
|
||||||
state.upgradeReason === "insufficient_credits"
|
|
||||||
? "Top up credits to continue"
|
|
||||||
: "Unlock your membership to continue";
|
|
||||||
|
|
||||||
const externalBrowserPromptShownRef = useRef(false);
|
const externalBrowserPromptShownRef = useRef(false);
|
||||||
|
|
||||||
@@ -188,15 +182,10 @@ export function ChatScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleMessageLimitUnlock(): void {
|
function handleMessageLimitUnlock(): void {
|
||||||
const subscriptionType =
|
|
||||||
state.upgradeReason === "insufficient_credits"
|
|
||||||
? getInsufficientCreditsSubscriptionType(userState.isVip)
|
|
||||||
: "vip";
|
|
||||||
|
|
||||||
router.push(
|
router.push(
|
||||||
getChatPaywallNavigationUrl(
|
getChatPaywallNavigationUrl(
|
||||||
authState.loginStatus,
|
authState.loginStatus,
|
||||||
subscriptionType,
|
getInsufficientCreditsSubscriptionType(userState.isVip),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
/**
|
/**
|
||||||
* ChatQuotaExhaustedBanner 游客配额耗尽横幅
|
* ChatQuotaExhaustedBanner 发送受限横幅
|
||||||
*
|
*
|
||||||
* 何时显示:
|
* 何时显示:
|
||||||
* - 后端返回 lockDetail.reason="weekly_limit" 且 showUpgrade=true
|
* - 后端返回 cannotSendReason="insufficient_credits"
|
||||||
*
|
*
|
||||||
* 视觉规格(与设计稿对齐):
|
* 视觉规格(与设计稿对齐):
|
||||||
* - 粉→品红渐变背景(与 splash 渐变一致)
|
* - 粉→品红渐变背景(与 splash 渐变一致)
|
||||||
* - 大字号粉色/白色提示文案("The limit for free chat times has been reached today")
|
* - 大字号粉色/白色提示文案
|
||||||
* - 粉→品红渐变 pill 按钮"Unlock your membership to continue" → 跳 /subscription
|
* - 粉→品红渐变 pill 按钮 → 跳 /subscription
|
||||||
*
|
*
|
||||||
* 业务关系:
|
* 业务关系:
|
||||||
* - 与 `src/app/sidebar/components/vip-benefits-card.tsx` 的 "Activate VIP Membership" 行为一致
|
* - 与 `src/app/sidebar/components/vip-benefits-card.tsx` 的 "Activate VIP Membership" 行为一致
|
||||||
@@ -36,8 +36,8 @@ export interface ChatQuotaExhaustedBannerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ChatQuotaExhaustedBanner({
|
export function ChatQuotaExhaustedBanner({
|
||||||
title = "The limit for free chat times\nhas been reached today",
|
title = "Insufficient credits\nTop up to continue chatting",
|
||||||
ctaLabel = "Unlock your membership to continue",
|
ctaLabel = "Top up credits to continue",
|
||||||
onUnlock,
|
onUnlock,
|
||||||
}: ChatQuotaExhaustedBannerProps) {
|
}: ChatQuotaExhaustedBannerProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|||||||
@@ -14,5 +14,5 @@ export class ChatStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 本地游客消息数量额度逻辑已停用:
|
// 本地游客消息数量额度逻辑已停用:
|
||||||
// 游客 / 非游客的消息数量限制统一由后端接口返回 lockDetail/daily_limit。
|
// 游客 / 非游客的发送能力统一以后端返回的积分状态为准。
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,43 +143,6 @@ describe("sendResponseToUiMessage", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("applyHttpSendOutput", () => {
|
describe("applyHttpSendOutput", () => {
|
||||||
it("removes the optimistic user message when the weekly limit is reached", () => {
|
|
||||||
const context = makeChatState({
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
content: "hello",
|
|
||||||
isFromAI: false,
|
|
||||||
date: "2026-06-25",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const nextState = applyHttpSendOutput(context, {
|
|
||||||
response: makeResponse({
|
|
||||||
reply: "",
|
|
||||||
messageId: "",
|
|
||||||
lockDetail: {
|
|
||||||
locked: true,
|
|
||||||
showContent: false,
|
|
||||||
showUpgrade: true,
|
|
||||||
reason: "weekly_limit",
|
|
||||||
hint: "Free message limit reached.",
|
|
||||||
detail: {
|
|
||||||
type: "weekly_msg_limit",
|
|
||||||
usedThisWeek: 3,
|
|
||||||
limit: 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
reply: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(nextState.messages).toEqual([]);
|
|
||||||
expect(nextState.isReplyingAI).toBe(false);
|
|
||||||
expect(nextState.upgradePromptVisible).toBe(true);
|
|
||||||
expect(nextState.upgradeReason).toBe("weekly_limit");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("stores insufficient credit state and removes failed optimistic messages", () => {
|
it("stores insufficient credit state and removes failed optimistic messages", () => {
|
||||||
const context = makeChatState({
|
const context = makeChatState({
|
||||||
messages: [
|
messages: [
|
||||||
@@ -363,7 +326,7 @@ describe("countLockedHistoryMessages", () => {
|
|||||||
isFromAI: true,
|
isFromAI: true,
|
||||||
date: "2026-06-29",
|
date: "2026-06-29",
|
||||||
locked: true,
|
locked: true,
|
||||||
lockReason: "weekly_limit",
|
lockReason: "insufficient_credits",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
content: "user text",
|
content: "user text",
|
||||||
|
|||||||
@@ -767,7 +767,7 @@ describe("chatMachine transitions", () => {
|
|||||||
actor.stop();
|
actor.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clears the weekly limit prompt after payment succeeds", async () => {
|
it("clears the insufficient credits prompt after payment succeeds", async () => {
|
||||||
const actor = createActor(createTestChatMachine()).start();
|
const actor = createActor(createTestChatMachine()).start();
|
||||||
|
|
||||||
actor.send({ type: "ChatUserLogin", token: "token" });
|
actor.send({ type: "ChatUserLogin", token: "token" });
|
||||||
@@ -789,12 +789,12 @@ describe("chatMachine transitions", () => {
|
|||||||
locked: true,
|
locked: true,
|
||||||
showContent: false,
|
showContent: false,
|
||||||
showUpgrade: true,
|
showUpgrade: true,
|
||||||
reason: "weekly_limit",
|
reason: "insufficient_credits",
|
||||||
hint: "Free message limit reached.",
|
hint: "Insufficient credits.",
|
||||||
detail: {
|
detail: {
|
||||||
type: "weekly_msg_limit",
|
requiredCredits: 2,
|
||||||
usedThisWeek: 3,
|
currentCredits: 0,
|
||||||
limit: 3,
|
shortfallCredits: 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
canSendMessage: false,
|
canSendMessage: false,
|
||||||
@@ -808,7 +808,9 @@ describe("chatMachine transitions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(actor.getSnapshot().context.upgradePromptVisible).toBe(true);
|
expect(actor.getSnapshot().context.upgradePromptVisible).toBe(true);
|
||||||
expect(actor.getSnapshot().context.upgradeReason).toBe("weekly_limit");
|
expect(actor.getSnapshot().context.upgradeReason).toBe(
|
||||||
|
"insufficient_credits",
|
||||||
|
);
|
||||||
expect(actor.getSnapshot().context.canSendMessage).toBe(false);
|
expect(actor.getSnapshot().context.canSendMessage).toBe(false);
|
||||||
|
|
||||||
actor.send({ type: "ChatPaymentSucceeded" });
|
actor.send({ type: "ChatPaymentSucceeded" });
|
||||||
|
|||||||
@@ -21,9 +21,9 @@
|
|||||||
* - guestSession / userSession:队列串行走 HTTP,限制以后端 `lockDetail` 为准
|
* - guestSession / userSession:队列串行走 HTTP,限制以后端 `lockDetail` 为准
|
||||||
* - `pendingReplyCount` 跟踪待回复数量,`isReplyingAI` 由计数派生
|
* - `pendingReplyCount` 跟踪待回复数量,`isReplyingAI` 由计数派生
|
||||||
*
|
*
|
||||||
* 配额:
|
* 发送能力:
|
||||||
* - 前端不再处理本地消息额度;游客 / 注册用户均以后端响应为准。
|
* - 前端不再处理本地消息额度;游客 / 注册用户均以后端响应为准。
|
||||||
* - 后端返回 `lockDetail.reason="weekly_limit"` 且需要升级时展示会员引导。
|
* - 后端返回 `cannotSendReason="insufficient_credits"` 时展示充值引导。
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -167,10 +167,6 @@ async function sendMessageViaHttp(content: string): Promise<{
|
|||||||
throw result.error;
|
throw result.error;
|
||||||
}
|
}
|
||||||
void chatRepo.prefetchMediaForSendResponse(result.data);
|
void chatRepo.prefetchMediaForSendResponse(result.data);
|
||||||
const isMessageLimit =
|
|
||||||
result.data.lockDetail.locked &&
|
|
||||||
result.data.lockDetail.showUpgrade &&
|
|
||||||
result.data.lockDetail.reason === "weekly_limit";
|
|
||||||
const isInsufficientCredits =
|
const isInsufficientCredits =
|
||||||
result.data.canSendMessage === false &&
|
result.data.canSendMessage === false &&
|
||||||
result.data.cannotSendReason === "insufficient_credits" &&
|
result.data.cannotSendReason === "insufficient_credits" &&
|
||||||
@@ -178,9 +174,7 @@ async function sendMessageViaHttp(content: string): Promise<{
|
|||||||
return {
|
return {
|
||||||
response: result.data,
|
response: result.data,
|
||||||
reply:
|
reply:
|
||||||
isMessageLimit || isInsufficientCredits
|
isInsufficientCredits ? null : sendResponseToUiMessage(result.data),
|
||||||
? null
|
|
||||||
: sendResponseToUiMessage(result.data),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,19 +35,12 @@ export function applyHttpSendOutput(
|
|||||||
): Partial<ChatState> {
|
): Partial<ChatState> {
|
||||||
const { response, reply } = output;
|
const { response, reply } = output;
|
||||||
|
|
||||||
const lockDetail = response.lockDetail;
|
|
||||||
const isWeeklyLimitBlocked =
|
|
||||||
lockDetail.locked &&
|
|
||||||
lockDetail.showUpgrade &&
|
|
||||||
lockDetail.reason === "weekly_limit";
|
|
||||||
const isInsufficientCredits =
|
const isInsufficientCredits =
|
||||||
response.canSendMessage === false &&
|
response.canSendMessage === false &&
|
||||||
response.cannotSendReason === "insufficient_credits";
|
response.cannotSendReason === "insufficient_credits";
|
||||||
const upgradeReason: ChatUpgradeReason | null = isWeeklyLimitBlocked
|
const upgradeReason: ChatUpgradeReason | null = isInsufficientCredits
|
||||||
? "weekly_limit"
|
? "insufficient_credits"
|
||||||
: isInsufficientCredits
|
: null;
|
||||||
? "insufficient_credits"
|
|
||||||
: null;
|
|
||||||
const sendCapabilityState = getSendCapabilityState(response);
|
const sendCapabilityState = getSendCapabilityState(response);
|
||||||
|
|
||||||
if (upgradeReason) {
|
if (upgradeReason) {
|
||||||
@@ -73,7 +66,10 @@ export function applyHttpSendOutput(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lockDetail.showUpgrade && lockDetail.reason === "private_message") {
|
if (
|
||||||
|
response.lockDetail.showUpgrade &&
|
||||||
|
response.lockDetail.reason === "private_message"
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
messages: [...context.messages, reply],
|
messages: [...context.messages, reply],
|
||||||
...finishPendingReply(context),
|
...finishPendingReply(context),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { UiMessage } from "@/data/dto/chat";
|
import type { UiMessage } from "@/data/dto/chat";
|
||||||
import type { PendingChatUnlockKind } from "@/lib/navigation/chat_unlock_session";
|
import type { PendingChatUnlockKind } from "@/lib/navigation/chat_unlock_session";
|
||||||
|
|
||||||
export type ChatUpgradeReason = "weekly_limit" | "insufficient_credits";
|
export type ChatUpgradeReason = "insufficient_credits";
|
||||||
|
|
||||||
export interface ChatUnlockPaywallRequest {
|
export interface ChatUnlockPaywallRequest {
|
||||||
messageId: string;
|
messageId: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user