refactor(chat): remove pagination-related state and logic from chat components

This commit is contained in:
2026-07-13 17:06:12 +08:00
parent b126b83c4c
commit cd0351b923
11 changed files with 18 additions and 169 deletions
@@ -40,9 +40,6 @@ function makeChatState(overrides: Partial<ChatState> = {}): ChatState {
creditsCharged: 0,
requiredCredits: 0,
shortfallCredits: 0,
isLoadingMore: false,
hasMore: true,
historyOffset: 0,
historyLoaded: true,
paymentUnlockPending: false,
unlockHistoryPromptVisible: false,
@@ -13,12 +13,6 @@ import type {
UnlockMessageRequest,
} from "@/stores/chat/chat-machine.helpers";
interface LoadMoreHistoryOutput {
messages: UiMessage[];
hasMore: boolean;
newOffset: number;
}
interface SendMessageHttpOutput {
response: ChatSendResponse;
reply: UiMessage | null;
@@ -29,8 +23,6 @@ interface UnlockHistoryOutput {
reason: string;
shortfallCredits: number;
messages: UiMessage[];
hasMore: boolean;
newOffset: number;
}
interface TestUnlockMessageOutput {
@@ -83,13 +75,6 @@ function createTestChatMachine(
return chatMachine.provide({
actors: {
loadHistory: createLoadHistoryCallback(options.historyMessages ?? []),
loadMoreHistory: fromPromise<LoadMoreHistoryOutput, { offset: number }>(
async () => ({
messages: [],
hasMore: false,
newOffset: 0,
}),
),
sendMessageHttp: fromPromise<
SendMessageHttpOutput,
{ content: string }
@@ -116,8 +101,6 @@ function createTestChatMachine(
reason: "ok",
shortfallCredits: 0,
messages: [],
hasMore: false,
newOffset: 0,
...options.unlockHistoryOutput,
})),
unlockMessage: fromPromise<
@@ -147,8 +130,6 @@ function createLoadHistoryCallback(
type: "ChatLocalHistoryLoaded",
output: {
messages,
hasMore: false,
newOffset: messages.length,
localCount: messages.length,
},
});
@@ -156,8 +137,6 @@ function createLoadHistoryCallback(
type: "ChatNetworkHistoryLoaded",
output: {
messages: networkMessages,
hasMore: false,
newOffset: networkMessages.length,
localOverwritten: true,
localCount: messages.length,
networkCount: networkMessages.length,
@@ -283,8 +262,6 @@ describe("chatMachine transitions", () => {
type: "ChatLocalHistoryLoaded",
output: {
messages: [localMessage],
hasMore: false,
newOffset: 1,
localCount: 1,
},
});
@@ -293,8 +270,6 @@ describe("chatMachine transitions", () => {
type: "ChatNetworkHistoryLoaded",
output: {
messages: [networkMessage],
hasMore: false,
newOffset: 1,
localOverwritten: true,
localCount: 1,
networkCount: 1,
@@ -399,13 +374,6 @@ describe("chatMachine transitions", () => {
const machine = chatMachine.provide({
actors: {
loadHistory: createLoadHistoryCallback([]),
loadMoreHistory: fromPromise<LoadMoreHistoryOutput, { offset: number }>(
async () => ({
messages: [],
hasMore: false,
newOffset: 0,
}),
),
sendMessageHttp: fromPromise<
SendMessageHttpOutput,
{ content: string }
@@ -435,8 +403,6 @@ describe("chatMachine transitions", () => {
reason: "ok",
shortfallCredits: 0,
messages: [],
hasMore: false,
newOffset: 0,
})),
},
});
@@ -526,8 +492,6 @@ describe("chatMachine transitions", () => {
lockReason: null,
},
],
hasMore: false,
newOffset: 1,
},
}),
).start();
@@ -585,8 +549,6 @@ describe("chatMachine transitions", () => {
lockReason: null,
},
],
hasMore: false,
newOffset: 1,
},
}),
).start();
-6
View File
@@ -26,9 +26,6 @@ interface ChatState {
isReplyingAI: boolean;
upgradePromptVisible: boolean;
upgradeReason: MachineContext["upgradeReason"];
isLoadingMore: boolean;
hasMore: boolean;
historyOffset: number;
/** history 初始加载完成标志(local → network → save 跑完) —— UI 可用此显示 loading */
historyLoaded: boolean;
unlockHistoryPromptVisible: boolean;
@@ -63,9 +60,6 @@ export function ChatProvider({ children }: ChatProviderProps) {
isReplyingAI: state.context.isReplyingAI,
upgradePromptVisible: state.context.upgradePromptVisible,
upgradeReason: state.context.upgradeReason,
isLoadingMore: state.context.isLoadingMore,
hasMore: state.context.hasMore,
historyOffset: state.context.historyOffset,
historyLoaded: state.context.historyLoaded,
unlockHistoryPromptVisible: state.context.unlockHistoryPromptVisible,
lockedHistoryCount: state.context.lockedHistoryCount,
-1
View File
@@ -36,7 +36,6 @@ export type ChatEvent =
// 业务事件
| { type: "ChatSendMessage"; content: string }
| { type: "ChatSendImage"; imageBase64: string }
| { type: "ChatLoadMoreHistory" }
| {
type: "ChatPromotionInjected";
promotion: PendingChatPromotion;
+2 -37
View File
@@ -1,24 +1,16 @@
import { fromCallback, fromPromise } from "xstate";
import { fromCallback } from "xstate";
import { getChatRepository } from "@/data/repositories/chat_repository";
import { resolveChatCacheOwnerKey } from "@/data/repositories/chat_cache_identity";
import { Logger, Result } from "@/utils";
import { Logger } from "@/utils";
import {
readLocalHistorySnapshot,
resolveHistoryCacheIdentity,
syncNetworkHistory,
} from "./chat-history-sync";
import {
PAGE_SIZE,
localMessagesToUi,
} from "./chat-machine.helpers";
import type { ChatEvent } from "./chat-events";
const log = new Logger("StoresChatChatHistoryFlow");
type UiMessage = import("@/data/dto/chat").UiMessage;
export const loadHistoryActor = fromCallback<ChatEvent>(({ sendBack }) => {
let cancelled = false;
@@ -50,30 +42,3 @@ export const loadHistoryActor = fromCallback<ChatEvent>(({ sendBack }) => {
cancelled = true;
};
});
export const loadMoreHistoryActor = fromPromise<
{ messages: UiMessage[]; hasMore: boolean; newOffset: number },
{ offset: number }
>(async ({ input }) => {
const chatRepo = getChatRepository();
const cacheIdentityResult = await resolveChatCacheOwnerKey();
const result = await chatRepo.getHistory(PAGE_SIZE, input.offset);
if (Result.isErr(result)) {
log.error("[chat-machine] loadMoreHistoryActor failed", {
error: result.error,
});
throw result.error;
}
const page = localMessagesToUi(result.data.messages);
if (Result.isOk(cacheIdentityResult)) {
void chatRepo.prefetchMediaForMessages(
result.data.messages,
cacheIdentityResult.data,
);
}
return {
messages: page,
hasMore: page.length >= PAGE_SIZE,
newOffset: input.offset + page.length,
};
});
+5 -12
View File
@@ -3,15 +3,16 @@ import { resolveChatCacheOwnerKey } from "@/data/repositories/chat_cache_identit
import { getChatRepository } from "@/data/repositories/chat_repository";
import { Logger, Result, todayString } from "@/utils";
import { localMessagesToUi, PAGE_SIZE } from "./chat-machine.helpers";
import {
CHAT_HISTORY_LIMIT,
localMessagesToUi,
} from "./chat-machine.helpers";
const log = new Logger("StoresChatChatHistorySync");
export type ReadAndSyncHistoryOutput = {
/** Network-authoritative messages. Empty network history includes a UI greeting. */
messages: UiMessage[];
hasMore: boolean;
newOffset: number;
/** True when network history was written back to local storage. */
localOverwritten: boolean;
localCount: number;
@@ -21,8 +22,6 @@ export type ReadAndSyncHistoryOutput = {
export type LocalHistorySnapshotOutput = {
/** Local cached messages. Empty local history includes a UI greeting. */
messages: UiMessage[];
hasMore: boolean;
newOffset: number;
localCount: number;
};
@@ -69,8 +68,6 @@ export async function readLocalHistorySnapshot(
return {
messages: snapshotMessages,
hasMore: localMessages.length >= PAGE_SIZE,
newOffset: snapshotMessages.length,
localCount: localMessages.length,
};
}
@@ -82,7 +79,7 @@ export async function syncNetworkHistory(
const chatRepo = getChatRepository();
const greetingMessage = createGreetingMessage();
const networkResult = await chatRepo.getHistory(PAGE_SIZE, 0);
const networkResult = await chatRepo.getHistory(CHAT_HISTORY_LIMIT, 0);
if (Result.isErr(networkResult)) {
log.error("[chat-machine] loadHistory NETWORK FAILED", {
error: networkResult.error,
@@ -120,8 +117,6 @@ export async function syncNetworkHistory(
return {
messages: finalMessages,
hasMore: finalMessages.length >= PAGE_SIZE,
newOffset: finalMessages.length,
localOverwritten,
localCount,
networkCount: networkUi.length,
@@ -145,8 +140,6 @@ export async function readAndSyncHistory(): Promise<ReadAndSyncHistoryOutput> {
return {
messages: localSnapshot.messages,
hasMore: localSnapshot.hasMore,
newOffset: localSnapshot.newOffset,
localOverwritten: false,
localCount: localSnapshot.localCount,
networkCount: 0,
+1 -4
View File
@@ -6,10 +6,7 @@
* - chat-send-flow
* - chat-unlock-flow
*/
export {
loadHistoryActor,
loadMoreHistoryActor,
} from "./chat-history-flow";
export { loadHistoryActor } from "./chat-history-flow";
export {
httpMessageQueueActor,
sendMessageHttpActor,
+7 -19
View File
@@ -2,8 +2,8 @@ import type { UiMessage } from "@/data/dto/chat";
import type { ChatState } from "./chat-state";
// History pagination size used by the history actors.
export const PAGE_SIZE = 50;
// The initial history request remains bounded even though pagination is disabled.
export const CHAT_HISTORY_LIMIT = 50;
export * from "./chat-message-mappers";
export * from "./chat-promotion";
@@ -13,17 +13,10 @@ export * from "./chat-unlock-helpers";
export function applyHistoryLoadedOutput(
output: {
messages: UiMessage[];
hasMore: boolean;
newOffset: number;
},
): Pick<
ChatState,
"messages" | "hasMore" | "historyOffset" | "historyLoaded"
> {
): Pick<ChatState, "messages" | "historyLoaded"> {
return {
messages: output.messages,
hasMore: output.hasMore,
historyOffset: output.newOffset,
historyLoaded: true,
};
}
@@ -32,18 +25,13 @@ export function applyNetworkHistoryLoadedOutput(
context: ChatState,
output: {
messages: UiMessage[];
hasMore: boolean;
newOffset: number;
localCount: number;
},
): Pick<
ChatState,
"messages" | "hasMore" | "historyOffset" | "historyLoaded"
> {
const optimisticTail = context.messages.slice(context.historyOffset);
): Pick<ChatState, "messages" | "historyLoaded"> {
const localSnapshotSize = output.localCount === 0 ? 1 : output.localCount;
const optimisticTail = context.messages.slice(localSnapshotSize);
return {
messages: [...output.messages, ...optimisticTail],
hasMore: output.hasMore,
historyOffset: output.newOffset,
historyLoaded: true,
};
}
+2 -35
View File
@@ -39,10 +39,7 @@ import {
shouldPromptUnlockHistory,
createChatPromotionState,
} from "./chat-machine.helpers";
import {
loadHistoryActor,
loadMoreHistoryActor,
} from "./chat-history-flow";
import { loadHistoryActor } from "./chat-history-flow";
import {
appendGuestUserImageAction,
appendUserImageAction,
@@ -87,7 +84,6 @@ export const chatMachine = setup({
},
actors: {
loadHistory: loadHistoryActor,
loadMoreHistory: loadMoreHistoryActor,
sendMessageHttp: sendMessageHttpActor,
httpMessageQueue: httpMessageQueueActor,
unlockHistory: unlockHistoryActor,
@@ -154,7 +150,6 @@ export const chatMachine = setup({
if (event.type !== "ChatNetworkHistoryLoaded") return {};
return {
...applyNetworkHistoryLoadedOutput(context, event.output),
isLoadingMore: false,
paymentUnlockPending: false,
};
}),
@@ -167,7 +162,6 @@ export const chatMachine = setup({
);
return {
...nextHistory,
isLoadingMore: false,
paymentUnlockPending: false,
unlockHistoryPromptVisible: true,
lockedHistoryCount: countLockedHistoryMessages(nextHistory.messages),
@@ -175,10 +169,7 @@ export const chatMachine = setup({
};
}),
markHistoryLoadFailed: assign({
isLoadingMore: false,
historyLoaded: true,
}),
markHistoryLoadFailed: assign({ historyLoaded: true }),
},
}).createMachine({
id: "chat",
@@ -404,9 +395,6 @@ export const chatMachine = setup({
ChatSendImage: {
actions: "appendUserImage",
},
ChatLoadMoreHistory: {
target: "loadingMore",
},
ChatUnlockMessageRequested: {
guard: ({ event }) => event.messageId.trim().length > 0,
target: "unlockingMessage",
@@ -432,25 +420,6 @@ export const chatMachine = setup({
},
},
},
loadingMore: {
invoke: {
src: "loadMoreHistory",
input: ({ context }) => ({ offset: context.historyOffset }),
onDone: {
target: "ready",
actions: assign(({ context, event }) => ({
messages: [...event.output.messages, ...context.messages],
isLoadingMore: false,
hasMore: event.output.hasMore,
historyOffset: event.output.newOffset,
})),
},
onError: {
target: "ready",
actions: assign({ isLoadingMore: false }),
},
},
},
unlockingHistory: {
invoke: {
id: "unlockHistory",
@@ -466,8 +435,6 @@ export const chatMachine = setup({
event.output.reason === "insufficient_balance";
return {
messages: event.output.messages,
hasMore: event.output.hasMore,
historyOffset: event.output.newOffset,
historyLoaded: true,
paymentUnlockPending: false,
unlockHistoryPromptVisible: insufficientBalance,
+1 -10
View File
@@ -31,13 +31,7 @@ export interface ChatState {
creditsCharged: number;
requiredCredits: number;
shortfallCredits: number;
isLoadingMore: boolean;
hasMore: boolean;
historyOffset: number;
/** history 首屏可展示标志(本地快照加载完成即可进入 ready)
* - 网络历史加载完成后会再次刷新消息列表
* - 不被 `loadMoreHistoryActor` 设置(翻页是另一码事)
*/
/** history 首屏可展示标志(本地快照加载完成即可进入 ready)。 */
historyLoaded: boolean;
paymentUnlockPending: boolean;
unlockHistoryPromptVisible: boolean;
@@ -66,9 +60,6 @@ export const initialState: ChatState = {
creditsCharged: 0,
requiredCredits: 0,
shortfallCredits: 0,
isLoadingMore: false,
hasMore: true,
historyOffset: 0,
historyLoaded: false,
paymentUnlockPending: false,
unlockHistoryPromptVisible: false,
-4
View File
@@ -27,8 +27,6 @@ export const unlockHistoryActor = fromPromise<{
reason: string;
shortfallCredits: number;
messages: UiMessage[];
hasMore: boolean;
newOffset: number;
}>(async () => {
const chatRepo = getChatRepository();
const unlockResult = await chatRepo.unlockHistory();
@@ -45,8 +43,6 @@ export const unlockHistoryActor = fromPromise<{
reason: unlockResult.data.reason,
shortfallCredits: unlockResult.data.shortfallCredits,
messages: history.messages,
hasMore: history.hasMore,
newOffset: history.newOffset,
};
});