feat(chat): unlock history after payment

This commit is contained in:
2026-06-29 10:53:52 +08:00
parent 58cd2a7545
commit b7779878cf
15 changed files with 591 additions and 18 deletions
+142 -16
View File
@@ -42,14 +42,19 @@ import { ChatState, initialState } from "./chat-state";
import type { ChatEvent } from "./chat-events";
import {
applyHttpSendOutput,
applyHistoryLoadedOutput,
beginPendingReply,
countLockedHistoryMessages,
finishPendingReply,
shouldAutoUnlockHistory,
shouldPromptUnlockHistory,
} from "./chat-machine.helpers";
import {
loadHistoryActor,
sendMessageHttpActor,
loadMoreHistoryActor,
httpMessageQueueActor,
unlockHistoryActor,
} from "./chat-machine.actors";
const log = new Logger("StoresChatChatMachine");
@@ -72,6 +77,7 @@ export const chatMachine = setup({
sendMessageHttp: sendMessageHttpActor,
loadMoreHistory: loadMoreHistoryActor,
httpMessageQueue: httpMessageQueueActor,
unlockHistory: unlockHistoryActor,
},
actions: {
enqueueMessage: sendTo("messageQueue", ({ event }) => event),
@@ -211,6 +217,37 @@ export const chatMachine = setup({
if (event.type !== "ChatQueuedHttpDone") return {};
return applyHttpSendOutput(context, event.output);
}),
markPaymentUnlockPending: assign(() => ({
paymentUnlockPending: true,
unlockHistoryError: null,
})),
showUnlockHistoryPrompt: assign(({ context }) => ({
paymentUnlockPending: false,
unlockHistoryPromptVisible: true,
lockedHistoryCount: countLockedHistoryMessages(context.messages),
unlockHistoryError: null,
})),
dismissUnlockHistoryPrompt: assign(() => ({
paymentUnlockPending: false,
unlockHistoryPromptVisible: false,
unlockHistoryError: null,
})),
markUnlockHistoryStarted: assign(() => ({
paymentUnlockPending: false,
unlockHistoryPromptVisible: false,
isUnlockingHistory: true,
unlockHistoryError: null,
})),
markUnlockHistoryFailed: assign(() => ({
isUnlockingHistory: false,
unlockHistoryPromptVisible: true,
unlockHistoryError: "Failed to unlock messages. Please try again.",
})),
},
}).createMachine({
id: "chat",
@@ -264,12 +301,9 @@ export const chatMachine = setup({
id: "loadHistory",
src: "loadHistory",
onDone: {
actions: assign(({ event }) => ({
messages: event.output.messages,
hasMore: event.output.hasMore,
historyOffset: event.output.newOffset,
historyLoaded: true,
})),
actions: assign(({ event }) =>
applyHistoryLoadedOutput(event.output),
),
},
onError: {
// 失败也标 loaded,不卡 init(让 UI 还是能进 ready,屏幕可能空)
@@ -337,22 +371,80 @@ export const chatMachine = setup({
ChatQueuedSendError: {
actions: "appendQueuedSendErrorMessage",
},
ChatPaymentSucceeded: [
{
guard: ({ context }) =>
context.historyLoaded && shouldAutoUnlockHistory(context.messages),
target: ".unlockingHistory",
actions: "markUnlockHistoryStarted",
},
{
guard: ({ context }) =>
context.historyLoaded &&
shouldPromptUnlockHistory(context.messages),
actions: "showUnlockHistoryPrompt",
},
{
guard: ({ context }) => !context.historyLoaded,
actions: "markPaymentUnlockPending",
},
{
guard: ({ context }) => context.historyLoaded,
actions: "dismissUnlockHistoryPrompt",
},
],
ChatUnlockHistoryConfirmed: {
target: ".unlockingHistory",
actions: "markUnlockHistoryStarted",
},
ChatUnlockHistoryDismissed: {
actions: "dismissUnlockHistoryPrompt",
},
},
initial: "initializing",
states: {
initializing: {
invoke: {
src: "loadHistory",
onDone: {
target: "ready",
actions: assign(({ event }) => ({
messages: event.output.messages,
isLoadingMore: false,
hasMore: event.output.hasMore,
historyOffset: event.output.newOffset,
historyLoaded: true,
})),
},
onDone: [
{
guard: ({ context, event }) =>
context.paymentUnlockPending &&
shouldAutoUnlockHistory(event.output.messages),
target: "unlockingHistory",
actions: [
assign(({ event }) => ({
...applyHistoryLoadedOutput(event.output),
isLoadingMore: false,
})),
"markUnlockHistoryStarted",
],
},
{
guard: ({ context, event }) =>
context.paymentUnlockPending &&
shouldPromptUnlockHistory(event.output.messages),
target: "ready",
actions: assign(({ event }) => ({
...applyHistoryLoadedOutput(event.output),
isLoadingMore: false,
paymentUnlockPending: false,
unlockHistoryPromptVisible: true,
lockedHistoryCount: countLockedHistoryMessages(
event.output.messages,
),
unlockHistoryError: null,
})),
},
{
target: "ready",
actions: assign(({ event }) => ({
...applyHistoryLoadedOutput(event.output),
isLoadingMore: false,
paymentUnlockPending: false,
})),
},
],
onError: {
target: "ready",
actions: assign({
@@ -413,6 +505,40 @@ export const chatMachine = setup({
},
},
},
unlockingHistory: {
invoke: {
id: "unlockHistory",
src: "unlockHistory",
onDone: {
target: "ready",
actions: assign(({ event }) => {
const lockedHistoryCount = countLockedHistoryMessages(
event.output.messages,
);
const insufficientBalance =
!event.output.unlocked &&
event.output.reason === "insufficient_balance";
return {
messages: event.output.messages,
hasMore: event.output.hasMore,
historyOffset: event.output.newOffset,
historyLoaded: true,
paymentUnlockPending: false,
unlockHistoryPromptVisible: insufficientBalance,
lockedHistoryCount,
isUnlockingHistory: false,
unlockHistoryError: insufficientBalance
? `Insufficient credits. You still need ${event.output.shortfallCredits} credits.`
: null,
};
}),
},
onError: {
target: "ready",
actions: "markUnlockHistoryFailed",
},
},
},
},
},
},