refactor(chat): remove pagination-related state and logic from chat components
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -36,7 +36,6 @@ export type ChatEvent =
|
||||
// 业务事件
|
||||
| { type: "ChatSendMessage"; content: string }
|
||||
| { type: "ChatSendImage"; imageBase64: string }
|
||||
| { type: "ChatLoadMoreHistory" }
|
||||
| {
|
||||
type: "ChatPromotionInjected";
|
||||
promotion: PendingChatPromotion;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user