From e48efc36baae19840391314906e4409558ff2e03 Mon Sep 17 00:00:00 2001 From: chenhang Date: Tue, 14 Jul 2026 18:47:03 +0800 Subject: [PATCH] refactor(chat): consolidate machine modules --- src/stores/chat/chat-machine.actions.ts | 499 ---------------------- src/stores/chat/chat-machine.setup.ts | 25 -- src/stores/chat/chat-machine.ts | 526 +++++++++++++++++++++++- 3 files changed, 519 insertions(+), 531 deletions(-) delete mode 100644 src/stores/chat/chat-machine.actions.ts delete mode 100644 src/stores/chat/chat-machine.setup.ts diff --git a/src/stores/chat/chat-machine.actions.ts b/src/stores/chat/chat-machine.actions.ts deleted file mode 100644 index 5bd63bc2..00000000 --- a/src/stores/chat/chat-machine.actions.ts +++ /dev/null @@ -1,499 +0,0 @@ -import { - setup, - type ActionFunction, - type DoneActorEvent, - type ErrorActorEvent, - type EventObject, -} from "xstate"; - -import { Logger } from "@/utils/logger"; -import { todayString } from "@/utils/date"; - -import type { ChatEvent } from "./chat-events"; -import { - applyHistoryLoadedOutput, - applyHttpSendOutput, - applyNetworkHistoryLoadedOutput, - applyPromotionUnlockOutput, - applySingleUnlockOutput, - beginPendingReply, - countLockedHistoryMessages, - createChatPromotionState, - finishPendingReply, - type HttpSendOutput, - type UnlockMessageOutput, -} from "./chat-machine.helpers"; -import { baseChatMachineSetup } from "./chat-machine.setup"; -import { initialState, type ChatState } from "./chat-state"; -import type { UnlockHistoryOutput } from "./chat-unlock-flow"; - -const sendLog = new Logger("StoresChatChatSendFlow"); -const mediaLog = new Logger("StoresChatChatMediaFlow"); - -const enqueueMessageAction = baseChatMachineSetup.sendTo( - "messageQueue", - ({ event }) => event, -); - -const startGuestSessionAction = baseChatMachineSetup.assign( - ({ context }) => ({ - ...initialState, - promotion: context.promotion, - }), -); - -const startUserSessionAction = baseChatMachineSetup.assign( - ({ context }) => ({ - ...initialState, - promotion: context.promotion, - }), -); - -const clearChatSessionAction = baseChatMachineSetup.assign(() => ({ - ...initialState, -})); - -const injectPromotionAction = baseChatMachineSetup.assign(({ event }) => { - if (event.type !== "ChatPromotionInjected") return {}; - return { - promotion: createChatPromotionState( - event.promotion, - event.messageId, - ), - }; -}); - -const clearPromotionAction = baseChatMachineSetup.assign({ promotion: null }); - -const applyLocalHistoryLoadedAction = baseChatMachineSetup.assign( - ({ event }) => { - if (event.type !== "ChatLocalHistoryLoaded") return {}; - return applyHistoryLoadedOutput(event.output); - }, -); - -const applyNetworkHistoryLoadedAction = baseChatMachineSetup.assign( - ({ context, event }) => { - if (event.type !== "ChatNetworkHistoryLoaded") return {}; - return applyNetworkHistoryLoadedOutput(context, event.output); - }, -); - -const applyNetworkHistoryLoadedAndClearPaymentAction = - baseChatMachineSetup.assign(({ context, event }) => { - if (event.type !== "ChatNetworkHistoryLoaded") return {}; - return { - ...applyNetworkHistoryLoadedOutput(context, event.output), - paymentUnlockPending: false, - }; - }); - -const showUnlockHistoryPromptFromNetworkAction = baseChatMachineSetup.assign( - ({ context, event }) => { - if (event.type !== "ChatNetworkHistoryLoaded") return {}; - const nextHistory = applyNetworkHistoryLoadedOutput(context, event.output); - return { - ...nextHistory, - paymentUnlockPending: false, - unlockHistoryPromptVisible: true, - lockedHistoryCount: countLockedHistoryMessages(nextHistory.messages), - unlockHistoryError: null, - }; - }, -); - -const markHistoryLoadFailedAction = baseChatMachineSetup.assign({ - historyLoaded: true, -}); - -const appendGuestUserMessageAction = baseChatMachineSetup.assign( - ({ context, event }) => { - if (event.type !== "ChatSendMessage") return {}; - - const today = todayString(); - sendLog.debug("[chat-machine] appendGuestUserMessage", { - contentLength: event.content.length, - contentPreview: event.content.slice(0, 50), - quotaMode: "server controlled", - isReplyingAI: context.isReplyingAI, - }); - - return { - messages: [ - ...context.messages, - { - content: event.content, - isFromAI: false, - date: today, - }, - ], - upgradePromptVisible: false, - upgradeReason: null, - }; - }, -); - -const appendUserMessageAction = baseChatMachineSetup.assign( - ({ context, event }) => { - if (event.type !== "ChatSendMessage") return {}; - - const today = todayString(); - sendLog.debug("[chat-machine] appendUserMessage", { - contentLength: event.content.length, - contentPreview: event.content.slice(0, 50), - isReplyingAI: context.isReplyingAI, - }); - - return { - messages: [ - ...context.messages, - { - content: event.content, - isFromAI: false, - date: today, - }, - ], - upgradePromptVisible: false, - upgradeReason: null, - }; - }, -); - -const appendQueuedSendErrorMessageAction = baseChatMachineSetup.assign( - ({ context }) => { - const messages = [ - ...context.messages, - { - content: "Something went wrong. Try sending again?", - isFromAI: true, - date: todayString(), - }, - ]; - return { messages, ...finishPendingReply(context) }; - }, -); - -const markQueuedSendStartedAction = baseChatMachineSetup.assign( - ({ context }) => beginPendingReply(context), -); - -const applyQueuedHttpOutputAction = baseChatMachineSetup.assign( - ({ context, event }) => { - if (event.type !== "ChatQueuedHttpDone") return {}; - return applyHttpSendOutput(context, event.output); - }, -); - -const clearUpgradePromptAction = baseChatMachineSetup.assign(() => ({ - upgradePromptVisible: false, - upgradeReason: null, - canSendMessage: true, - requiredCredits: 0, - shortfallCredits: 0, -})); - -const appendGuestUserImageAction = baseChatMachineSetup.assign( - ({ context, event }) => { - if (event.type !== "ChatSendImage") return {}; - - const today = todayString(); - mediaLog.debug("[chat-machine] appendGuestUserImage", { - oldMessagesCount: context.messages.length, - quotaMode: "server controlled", - }); - return { - messages: [ - ...context.messages, - { - content: "[Image]", - isFromAI: false, - date: today, - imageUrl: event.imageBase64, - }, - ], - ...beginPendingReply(context), - upgradePromptVisible: false, - upgradeReason: null, - }; - }, -); - -const appendUserImageAction = baseChatMachineSetup.assign( - ({ context, event }) => { - if (event.type !== "ChatSendImage") return {}; - - const today = todayString(); - mediaLog.debug("[chat-machine] appendUserImage", { - oldMessagesCount: context.messages.length, - }); - - return { - messages: [ - ...context.messages, - { - content: "[Image]", - isFromAI: false, - date: today, - imageUrl: event.imageBase64, - }, - ], - ...beginPendingReply(context), - upgradePromptVisible: false, - upgradeReason: null, - }; - }, -); - -const markPaymentUnlockPendingAction = baseChatMachineSetup.assign(() => ({ - paymentUnlockPending: true, - unlockHistoryError: null, -})); - -const showUnlockHistoryPromptAction = baseChatMachineSetup.assign( - ({ context }) => ({ - paymentUnlockPending: false, - unlockHistoryPromptVisible: true, - lockedHistoryCount: countLockedHistoryMessages(context.messages), - unlockHistoryError: null, - }), -); - -const dismissUnlockHistoryPromptAction = baseChatMachineSetup.assign(() => ({ - paymentUnlockPending: false, - unlockHistoryPromptVisible: false, - unlockHistoryError: null, -})); - -const markUnlockHistoryStartedAction = baseChatMachineSetup.assign(() => ({ - paymentUnlockPending: false, - unlockHistoryPromptVisible: false, - isUnlockingHistory: true, - unlockHistoryError: null, -})); - -const markUnlockHistoryFailedAction = baseChatMachineSetup.assign(() => ({ - isUnlockingHistory: false, - unlockHistoryPromptVisible: true, - unlockHistoryError: "Failed to unlock messages. Please try again.", -})); - -const markUnlockMessageStartedAction = baseChatMachineSetup.assign( - ({ event }) => { - if (event.type !== "ChatUnlockMessageRequested") return {}; - return { - isUnlockingMessage: true, - unlockingMessageId: event.messageId, - unlockingMessageKind: event.kind, - unlockingRemoteMessageId: - event.remoteMessageId ?? (event.lockType ? null : event.messageId), - unlockingLockType: event.lockType ?? null, - unlockingClientLockId: event.clientLockId ?? null, - unlockMessageError: null, - unlockPaywallRequest: null, - }; - }, -); - -const applyUnlockMessageSucceededAction = baseChatMachineSetup.assign( - ({ context, event }) => { - const { output } = event as unknown as DoneActorEvent; - return { - messages: applySingleUnlockOutput(context.messages, output), - promotion: applyPromotionUnlockOutput(context.promotion, output), - isUnlockingMessage: false, - unlockingMessageId: null, - unlockingMessageKind: null, - unlockingRemoteMessageId: null, - unlockingLockType: null, - unlockingClientLockId: null, - unlockMessageError: null, - unlockPaywallRequest: null, - creditBalance: output.response.creditBalance, - creditsCharged: output.response.creditsCharged, - requiredCredits: 0, - shortfallCredits: 0, - }; - }, -); - -const requestUnlockPaymentFromOutputAction = baseChatMachineSetup.assign( - ({ context, event }) => { - const { output } = event as unknown as DoneActorEvent; - return { - promotion: applyPromotionUnlockOutput(context.promotion, output), - isUnlockingMessage: false, - unlockingMessageId: null, - unlockingMessageKind: null, - unlockingRemoteMessageId: null, - unlockingLockType: null, - unlockingClientLockId: null, - unlockMessageError: output.response.reason, - unlockPaywallRequest: { - displayMessageId: - output.response.messageId || output.displayMessageId, - ...(output.response.messageId || output.request.messageId - ? { - messageId: - output.response.messageId || output.request.messageId, - } - : {}), - kind: context.unlockingMessageKind ?? "private", - ...(output.request.lockType - ? { lockType: output.request.lockType } - : {}), - ...(output.request.clientLockId - ? { clientLockId: output.request.clientLockId } - : {}), - ...(context.promotion?.message.id === output.displayMessageId - ? { promotion: context.promotion.session } - : {}), - reason: output.response.reason, - creditBalance: output.response.creditBalance, - requiredCredits: output.response.requiredCredits, - shortfallCredits: output.response.shortfallCredits, - }, - creditBalance: output.response.creditBalance, - requiredCredits: output.response.requiredCredits, - shortfallCredits: output.response.shortfallCredits, - }; - }, -); - -const requestUnlockPaymentFromErrorAction = baseChatMachineSetup.assign( - ({ context }) => ({ - isUnlockingMessage: false, - unlockMessageError: "unlock_failed", - unlockPaywallRequest: - context.unlockingMessageId && context.unlockingMessageKind - ? { - displayMessageId: context.unlockingMessageId, - ...(context.unlockingRemoteMessageId - ? { messageId: context.unlockingRemoteMessageId } - : {}), - kind: context.unlockingMessageKind, - ...(context.unlockingLockType - ? { lockType: context.unlockingLockType } - : {}), - ...(context.unlockingClientLockId - ? { clientLockId: context.unlockingClientLockId } - : {}), - ...(context.promotion?.message.id === context.unlockingMessageId - ? { promotion: context.promotion.session } - : {}), - reason: "unlock_failed", - creditBalance: context.creditBalance, - requiredCredits: context.requiredCredits, - shortfallCredits: context.shortfallCredits, - } - : null, - unlockingMessageId: null, - unlockingMessageKind: null, - unlockingRemoteMessageId: null, - unlockingLockType: null, - unlockingClientLockId: null, - }), -); - -const clearUnlockPaywallRequestAction = baseChatMachineSetup.assign(() => ({ - unlockPaywallRequest: null, -})); - -type ChatActorAction = ActionFunction< - ChatState, - TEvent, - ChatEvent, - undefined, - never, - never, - never, - never, - never ->; - -function createChatActorActionSetup() { - const actorActionSetup = setup({ - types: { - context: {} as ChatState, - events: {} as TEvent, - }, - }); - return { - assign( - assignment: Parameters[0], - ): ChatActorAction { - // Actor lifecycle events are expression events, not public machine events. - return actorActionSetup.assign(assignment) as unknown as ChatActorAction; - }, - }; -} - -const sendMessageDoneActionSetup = - createChatActorActionSetup>(); -const unlockHistoryDoneActionSetup = - createChatActorActionSetup>(); -const actorErrorActionSetup = createChatActorActionSetup(); - -export const applyHttpSendOutputAction = sendMessageDoneActionSetup.assign( - ({ context, event }) => applyHttpSendOutput(context, event.output), -); - -export const markHttpSendFailedAction = actorErrorActionSetup.assign({ - isReplyingAI: false, -}); - -export const applyUnlockHistoryOutputAction = - unlockHistoryDoneActionSetup.assign(({ event }) => { - const lockedHistoryCount = countLockedHistoryMessages( - event.output.messages, - ); - const insufficientBalance = - !event.output.unlocked && - event.output.reason === "insufficient_balance"; - return { - messages: event.output.messages, - historyLoaded: true, - paymentUnlockPending: false, - unlockHistoryPromptVisible: insufficientBalance, - lockedHistoryCount, - isUnlockingHistory: false, - unlockHistoryError: insufficientBalance - ? `Insufficient credits. You still need ${event.output.shortfallCredits} credits.` - : null, - }; - }); - -export const chatMachineSetup = baseChatMachineSetup.extend({ - actions: { - enqueueMessage: enqueueMessageAction, - startGuestSession: startGuestSessionAction, - startUserSession: startUserSessionAction, - clearChatSession: clearChatSessionAction, - injectPromotion: injectPromotionAction, - clearPromotion: clearPromotionAction, - applyLocalHistoryLoaded: applyLocalHistoryLoadedAction, - applyNetworkHistoryLoaded: applyNetworkHistoryLoadedAction, - applyNetworkHistoryLoadedAndClearPayment: - applyNetworkHistoryLoadedAndClearPaymentAction, - showUnlockHistoryPromptFromNetwork: - showUnlockHistoryPromptFromNetworkAction, - markHistoryLoadFailed: markHistoryLoadFailedAction, - appendGuestUserMessage: appendGuestUserMessageAction, - appendUserMessage: appendUserMessageAction, - appendQueuedSendErrorMessage: appendQueuedSendErrorMessageAction, - markQueuedSendStarted: markQueuedSendStartedAction, - applyQueuedHttpOutput: applyQueuedHttpOutputAction, - clearUpgradePrompt: clearUpgradePromptAction, - appendGuestUserImage: appendGuestUserImageAction, - appendUserImage: appendUserImageAction, - markPaymentUnlockPending: markPaymentUnlockPendingAction, - showUnlockHistoryPrompt: showUnlockHistoryPromptAction, - dismissUnlockHistoryPrompt: dismissUnlockHistoryPromptAction, - markUnlockHistoryStarted: markUnlockHistoryStartedAction, - markUnlockHistoryFailed: markUnlockHistoryFailedAction, - markUnlockMessageStarted: markUnlockMessageStartedAction, - applyUnlockMessageSucceeded: applyUnlockMessageSucceededAction, - requestUnlockPaymentFromOutput: requestUnlockPaymentFromOutputAction, - requestUnlockPaymentFromError: requestUnlockPaymentFromErrorAction, - clearUnlockPaywallRequest: clearUnlockPaywallRequestAction, - }, -}); diff --git a/src/stores/chat/chat-machine.setup.ts b/src/stores/chat/chat-machine.setup.ts deleted file mode 100644 index dff3e018..00000000 --- a/src/stores/chat/chat-machine.setup.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { setup } from "xstate"; - -import type { ChatEvent } from "./chat-events"; -import { - httpMessageQueueActor, - loadHistoryActor, - sendMessageHttpActor, - unlockHistoryActor, - unlockMessageActor, -} from "./chat-machine.actors"; -import type { ChatState } from "./chat-state"; - -export const baseChatMachineSetup = setup({ - types: { - context: {} as ChatState, - events: {} as ChatEvent, - }, - actors: { - loadHistory: loadHistoryActor, - sendMessageHttp: sendMessageHttpActor, - httpMessageQueue: httpMessageQueueActor, - unlockHistory: unlockHistoryActor, - unlockMessage: unlockMessageActor, - }, -}); diff --git a/src/stores/chat/chat-machine.ts b/src/stores/chat/chat-machine.ts index 65498b39..1c8f2227 100644 --- a/src/stores/chat/chat-machine.ts +++ b/src/stores/chat/chat-machine.ts @@ -1,17 +1,529 @@ import { - applyHttpSendOutputAction, - applyUnlockHistoryOutputAction, - chatMachineSetup, - markHttpSendFailedAction, -} from "./chat-machine.actions"; -import { shouldPromptUnlockHistory } from "./chat-machine.helpers"; -import { initialState } from "./chat-state"; + setup, + type ActionFunction, + type DoneActorEvent, + type ErrorActorEvent, + type EventObject, +} from "xstate"; + +import { todayString } from "@/utils/date"; +import { Logger } from "@/utils/logger"; + +import type { ChatEvent } from "./chat-events"; +import { + httpMessageQueueActor, + loadHistoryActor, + sendMessageHttpActor, + unlockHistoryActor, + unlockMessageActor, +} from "./chat-machine.actors"; +import { + applyHistoryLoadedOutput, + applyHttpSendOutput, + applyNetworkHistoryLoadedOutput, + applyPromotionUnlockOutput, + applySingleUnlockOutput, + beginPendingReply, + countLockedHistoryMessages, + createChatPromotionState, + finishPendingReply, + shouldPromptUnlockHistory, + type HttpSendOutput, + type UnlockMessageOutput, +} from "./chat-machine.helpers"; +import { initialState, type ChatState } from "./chat-state"; +import type { UnlockHistoryOutput } from "./chat-unlock-flow"; // 重新导出 State / Event 类型,保持 machine 文件的公共 API 不变 export type { ChatState } from "./chat-state"; export { initialState } from "./chat-state"; export type { ChatEvent } from "./chat-events"; +const baseChatMachineSetup = setup({ + types: { + context: {} as ChatState, + events: {} as ChatEvent, + }, + actors: { + loadHistory: loadHistoryActor, + sendMessageHttp: sendMessageHttpActor, + httpMessageQueue: httpMessageQueueActor, + unlockHistory: unlockHistoryActor, + unlockMessage: unlockMessageActor, + }, +}); + +const sendLog = new Logger("StoresChatChatSendFlow"); +const mediaLog = new Logger("StoresChatChatMediaFlow"); + +const enqueueMessageAction = baseChatMachineSetup.sendTo( + "messageQueue", + ({ event }) => event, +); + +const startGuestSessionAction = baseChatMachineSetup.assign( + ({ context }) => ({ + ...initialState, + promotion: context.promotion, + }), +); + +const startUserSessionAction = baseChatMachineSetup.assign( + ({ context }) => ({ + ...initialState, + promotion: context.promotion, + }), +); + +const clearChatSessionAction = baseChatMachineSetup.assign(() => ({ + ...initialState, +})); + +const injectPromotionAction = baseChatMachineSetup.assign(({ event }) => { + if (event.type !== "ChatPromotionInjected") return {}; + return { + promotion: createChatPromotionState( + event.promotion, + event.messageId, + ), + }; +}); + +const clearPromotionAction = baseChatMachineSetup.assign({ promotion: null }); + +const applyLocalHistoryLoadedAction = baseChatMachineSetup.assign( + ({ event }) => { + if (event.type !== "ChatLocalHistoryLoaded") return {}; + return applyHistoryLoadedOutput(event.output); + }, +); + +const applyNetworkHistoryLoadedAction = baseChatMachineSetup.assign( + ({ context, event }) => { + if (event.type !== "ChatNetworkHistoryLoaded") return {}; + return applyNetworkHistoryLoadedOutput(context, event.output); + }, +); + +const applyNetworkHistoryLoadedAndClearPaymentAction = + baseChatMachineSetup.assign(({ context, event }) => { + if (event.type !== "ChatNetworkHistoryLoaded") return {}; + return { + ...applyNetworkHistoryLoadedOutput(context, event.output), + paymentUnlockPending: false, + }; + }); + +const showUnlockHistoryPromptFromNetworkAction = baseChatMachineSetup.assign( + ({ context, event }) => { + if (event.type !== "ChatNetworkHistoryLoaded") return {}; + const nextHistory = applyNetworkHistoryLoadedOutput(context, event.output); + return { + ...nextHistory, + paymentUnlockPending: false, + unlockHistoryPromptVisible: true, + lockedHistoryCount: countLockedHistoryMessages(nextHistory.messages), + unlockHistoryError: null, + }; + }, +); + +const markHistoryLoadFailedAction = baseChatMachineSetup.assign({ + historyLoaded: true, +}); + +const appendGuestUserMessageAction = baseChatMachineSetup.assign( + ({ context, event }) => { + if (event.type !== "ChatSendMessage") return {}; + + const today = todayString(); + sendLog.debug("[chat-machine] appendGuestUserMessage", { + contentLength: event.content.length, + contentPreview: event.content.slice(0, 50), + quotaMode: "server controlled", + isReplyingAI: context.isReplyingAI, + }); + + return { + messages: [ + ...context.messages, + { + content: event.content, + isFromAI: false, + date: today, + }, + ], + upgradePromptVisible: false, + upgradeReason: null, + }; + }, +); + +const appendUserMessageAction = baseChatMachineSetup.assign( + ({ context, event }) => { + if (event.type !== "ChatSendMessage") return {}; + + const today = todayString(); + sendLog.debug("[chat-machine] appendUserMessage", { + contentLength: event.content.length, + contentPreview: event.content.slice(0, 50), + isReplyingAI: context.isReplyingAI, + }); + + return { + messages: [ + ...context.messages, + { + content: event.content, + isFromAI: false, + date: today, + }, + ], + upgradePromptVisible: false, + upgradeReason: null, + }; + }, +); + +const appendQueuedSendErrorMessageAction = baseChatMachineSetup.assign( + ({ context }) => { + const messages = [ + ...context.messages, + { + content: "Something went wrong. Try sending again?", + isFromAI: true, + date: todayString(), + }, + ]; + return { messages, ...finishPendingReply(context) }; + }, +); + +const markQueuedSendStartedAction = baseChatMachineSetup.assign( + ({ context }) => beginPendingReply(context), +); + +const applyQueuedHttpOutputAction = baseChatMachineSetup.assign( + ({ context, event }) => { + if (event.type !== "ChatQueuedHttpDone") return {}; + return applyHttpSendOutput(context, event.output); + }, +); + +const clearUpgradePromptAction = baseChatMachineSetup.assign(() => ({ + upgradePromptVisible: false, + upgradeReason: null, + canSendMessage: true, + requiredCredits: 0, + shortfallCredits: 0, +})); + +const appendGuestUserImageAction = baseChatMachineSetup.assign( + ({ context, event }) => { + if (event.type !== "ChatSendImage") return {}; + + const today = todayString(); + mediaLog.debug("[chat-machine] appendGuestUserImage", { + oldMessagesCount: context.messages.length, + quotaMode: "server controlled", + }); + return { + messages: [ + ...context.messages, + { + content: "[Image]", + isFromAI: false, + date: today, + imageUrl: event.imageBase64, + }, + ], + ...beginPendingReply(context), + upgradePromptVisible: false, + upgradeReason: null, + }; + }, +); + +const appendUserImageAction = baseChatMachineSetup.assign( + ({ context, event }) => { + if (event.type !== "ChatSendImage") return {}; + + const today = todayString(); + mediaLog.debug("[chat-machine] appendUserImage", { + oldMessagesCount: context.messages.length, + }); + + return { + messages: [ + ...context.messages, + { + content: "[Image]", + isFromAI: false, + date: today, + imageUrl: event.imageBase64, + }, + ], + ...beginPendingReply(context), + upgradePromptVisible: false, + upgradeReason: null, + }; + }, +); + +const markPaymentUnlockPendingAction = baseChatMachineSetup.assign(() => ({ + paymentUnlockPending: true, + unlockHistoryError: null, +})); + +const showUnlockHistoryPromptAction = baseChatMachineSetup.assign( + ({ context }) => ({ + paymentUnlockPending: false, + unlockHistoryPromptVisible: true, + lockedHistoryCount: countLockedHistoryMessages(context.messages), + unlockHistoryError: null, + }), +); + +const dismissUnlockHistoryPromptAction = baseChatMachineSetup.assign(() => ({ + paymentUnlockPending: false, + unlockHistoryPromptVisible: false, + unlockHistoryError: null, +})); + +const markUnlockHistoryStartedAction = baseChatMachineSetup.assign(() => ({ + paymentUnlockPending: false, + unlockHistoryPromptVisible: false, + isUnlockingHistory: true, + unlockHistoryError: null, +})); + +const markUnlockHistoryFailedAction = baseChatMachineSetup.assign(() => ({ + isUnlockingHistory: false, + unlockHistoryPromptVisible: true, + unlockHistoryError: "Failed to unlock messages. Please try again.", +})); + +const markUnlockMessageStartedAction = baseChatMachineSetup.assign( + ({ event }) => { + if (event.type !== "ChatUnlockMessageRequested") return {}; + return { + isUnlockingMessage: true, + unlockingMessageId: event.messageId, + unlockingMessageKind: event.kind, + unlockingRemoteMessageId: + event.remoteMessageId ?? (event.lockType ? null : event.messageId), + unlockingLockType: event.lockType ?? null, + unlockingClientLockId: event.clientLockId ?? null, + unlockMessageError: null, + unlockPaywallRequest: null, + }; + }, +); + +const applyUnlockMessageSucceededAction = baseChatMachineSetup.assign( + ({ context, event }) => { + const { output } = event as unknown as DoneActorEvent; + return { + messages: applySingleUnlockOutput(context.messages, output), + promotion: applyPromotionUnlockOutput(context.promotion, output), + isUnlockingMessage: false, + unlockingMessageId: null, + unlockingMessageKind: null, + unlockingRemoteMessageId: null, + unlockingLockType: null, + unlockingClientLockId: null, + unlockMessageError: null, + unlockPaywallRequest: null, + creditBalance: output.response.creditBalance, + creditsCharged: output.response.creditsCharged, + requiredCredits: 0, + shortfallCredits: 0, + }; + }, +); + +const requestUnlockPaymentFromOutputAction = baseChatMachineSetup.assign( + ({ context, event }) => { + const { output } = event as unknown as DoneActorEvent; + return { + promotion: applyPromotionUnlockOutput(context.promotion, output), + isUnlockingMessage: false, + unlockingMessageId: null, + unlockingMessageKind: null, + unlockingRemoteMessageId: null, + unlockingLockType: null, + unlockingClientLockId: null, + unlockMessageError: output.response.reason, + unlockPaywallRequest: { + displayMessageId: + output.response.messageId || output.displayMessageId, + ...(output.response.messageId || output.request.messageId + ? { + messageId: + output.response.messageId || output.request.messageId, + } + : {}), + kind: context.unlockingMessageKind ?? "private", + ...(output.request.lockType + ? { lockType: output.request.lockType } + : {}), + ...(output.request.clientLockId + ? { clientLockId: output.request.clientLockId } + : {}), + ...(context.promotion?.message.id === output.displayMessageId + ? { promotion: context.promotion.session } + : {}), + reason: output.response.reason, + creditBalance: output.response.creditBalance, + requiredCredits: output.response.requiredCredits, + shortfallCredits: output.response.shortfallCredits, + }, + creditBalance: output.response.creditBalance, + requiredCredits: output.response.requiredCredits, + shortfallCredits: output.response.shortfallCredits, + }; + }, +); + +const requestUnlockPaymentFromErrorAction = baseChatMachineSetup.assign( + ({ context }) => ({ + isUnlockingMessage: false, + unlockMessageError: "unlock_failed", + unlockPaywallRequest: + context.unlockingMessageId && context.unlockingMessageKind + ? { + displayMessageId: context.unlockingMessageId, + ...(context.unlockingRemoteMessageId + ? { messageId: context.unlockingRemoteMessageId } + : {}), + kind: context.unlockingMessageKind, + ...(context.unlockingLockType + ? { lockType: context.unlockingLockType } + : {}), + ...(context.unlockingClientLockId + ? { clientLockId: context.unlockingClientLockId } + : {}), + ...(context.promotion?.message.id === context.unlockingMessageId + ? { promotion: context.promotion.session } + : {}), + reason: "unlock_failed", + creditBalance: context.creditBalance, + requiredCredits: context.requiredCredits, + shortfallCredits: context.shortfallCredits, + } + : null, + unlockingMessageId: null, + unlockingMessageKind: null, + unlockingRemoteMessageId: null, + unlockingLockType: null, + unlockingClientLockId: null, + }), +); + +const clearUnlockPaywallRequestAction = baseChatMachineSetup.assign(() => ({ + unlockPaywallRequest: null, +})); + +type ChatActorAction = ActionFunction< + ChatState, + TEvent, + ChatEvent, + undefined, + never, + never, + never, + never, + never +>; + +function createChatActorActionSetup() { + const actorActionSetup = setup({ + types: { + context: {} as ChatState, + events: {} as TEvent, + }, + }); + return { + assign( + assignment: Parameters[0], + ): ChatActorAction { + // Actor lifecycle events are expression events, not public machine events. + return actorActionSetup.assign(assignment) as unknown as ChatActorAction; + }, + }; +} + +const sendMessageDoneActionSetup = + createChatActorActionSetup>(); +const unlockHistoryDoneActionSetup = + createChatActorActionSetup>(); +const actorErrorActionSetup = createChatActorActionSetup(); + +const applyHttpSendOutputAction = sendMessageDoneActionSetup.assign( + ({ context, event }) => applyHttpSendOutput(context, event.output), +); + +const markHttpSendFailedAction = actorErrorActionSetup.assign({ + isReplyingAI: false, +}); + +const applyUnlockHistoryOutputAction = + unlockHistoryDoneActionSetup.assign(({ event }) => { + const lockedHistoryCount = countLockedHistoryMessages( + event.output.messages, + ); + const insufficientBalance = + !event.output.unlocked && + event.output.reason === "insufficient_balance"; + return { + messages: event.output.messages, + historyLoaded: true, + paymentUnlockPending: false, + unlockHistoryPromptVisible: insufficientBalance, + lockedHistoryCount, + isUnlockingHistory: false, + unlockHistoryError: insufficientBalance + ? `Insufficient credits. You still need ${event.output.shortfallCredits} credits.` + : null, + }; + }); + +const chatMachineSetup = baseChatMachineSetup.extend({ + actions: { + enqueueMessage: enqueueMessageAction, + startGuestSession: startGuestSessionAction, + startUserSession: startUserSessionAction, + clearChatSession: clearChatSessionAction, + injectPromotion: injectPromotionAction, + clearPromotion: clearPromotionAction, + applyLocalHistoryLoaded: applyLocalHistoryLoadedAction, + applyNetworkHistoryLoaded: applyNetworkHistoryLoadedAction, + applyNetworkHistoryLoadedAndClearPayment: + applyNetworkHistoryLoadedAndClearPaymentAction, + showUnlockHistoryPromptFromNetwork: + showUnlockHistoryPromptFromNetworkAction, + markHistoryLoadFailed: markHistoryLoadFailedAction, + appendGuestUserMessage: appendGuestUserMessageAction, + appendUserMessage: appendUserMessageAction, + appendQueuedSendErrorMessage: appendQueuedSendErrorMessageAction, + markQueuedSendStarted: markQueuedSendStartedAction, + applyQueuedHttpOutput: applyQueuedHttpOutputAction, + clearUpgradePrompt: clearUpgradePromptAction, + appendGuestUserImage: appendGuestUserImageAction, + appendUserImage: appendUserImageAction, + markPaymentUnlockPending: markPaymentUnlockPendingAction, + showUnlockHistoryPrompt: showUnlockHistoryPromptAction, + dismissUnlockHistoryPrompt: dismissUnlockHistoryPromptAction, + markUnlockHistoryStarted: markUnlockHistoryStartedAction, + markUnlockHistoryFailed: markUnlockHistoryFailedAction, + markUnlockMessageStarted: markUnlockMessageStartedAction, + applyUnlockMessageSucceeded: applyUnlockMessageSucceededAction, + requestUnlockPaymentFromOutput: requestUnlockPaymentFromOutputAction, + requestUnlockPaymentFromError: requestUnlockPaymentFromErrorAction, + clearUnlockPaywallRequest: clearUnlockPaywallRequestAction, + }, +}); + export const chatMachine = chatMachineSetup.createMachine({ id: "chat", initial: "idle",