main
mula.liu 2026-04-30 17:25:38 +08:00
parent ecf223f945
commit b8e958da13
2 changed files with 80 additions and 52 deletions

View File

@ -88,6 +88,48 @@ function normalizeMessageId(raw: unknown): number | undefined {
return i > 0 ? i : undefined;
}
function mergeLatestMessages(existing: ChatMessage[], latestPage: ChatMessage[]): ChatMessage[] {
if (latestPage.length <= 0) {
return existing.slice(-300);
}
const latestIds = latestPage
.map((msg) => normalizeMessageId(msg.id))
.filter((id): id is number => typeof id === 'number');
const oldestLatestId = latestIds.length > 0 ? Math.min(...latestIds) : null;
const latestIdSet = new Set(latestIds);
const latestFallbackKeys = new Set(
latestPage
.filter((msg) => !normalizeMessageId(msg.id))
.map((msg) => `k:${msg.role}:${msg.ts}:${msg.text}`),
);
const keptExisting = existing.filter((msg) => {
const id = normalizeMessageId(msg.id);
if (id && oldestLatestId !== null && id >= oldestLatestId) {
return latestIdSet.has(id);
}
if (!id && latestFallbackKeys.has(`k:${msg.role}:${msg.ts}:${msg.text}`)) {
return false;
}
return true;
});
const mergedMap = new Map<string, ChatMessage>();
[...keptExisting, ...latestPage].forEach((msg) => {
const id = normalizeMessageId(msg.id);
const key = id ? `id:${id}` : `k:${msg.role}:${msg.ts}:${msg.text}`;
mergedMap.set(key, msg);
});
return Array.from(mergedMap.values())
.sort((a, b) => {
if (a.ts !== b.ts) return a.ts - b.ts;
return Number(a.id || 0) - Number(b.id || 0);
})
.slice(-300);
}
function normalizeChannelName(raw: unknown): string {
const channel = String(raw || '').trim().toLowerCase();
if (channel === 'dashboard_channel' || channel === 'dashboard-channel') return 'dashboard';
@ -195,19 +237,8 @@ export function useBotsSync(forcedBotId?: string, enableMonitorSockets: boolean
.filter((msg) => msg.text.trim().length > 0 || (msg.attachments || []).length > 0)
.slice(-300);
// Keep already lazy-loaded history; only merge/refresh the latest page.
const existing = (activeBotsRef.current[target]?.messages || []).filter((m) => (m.kind || 'final') !== 'progress');
const mergedMap = new Map<string, ChatMessage>();
[...existing, ...latestPage].forEach((msg) => {
const key = msg.id ? `id:${msg.id}` : `k:${msg.role}:${msg.ts}:${msg.text}`;
if (!mergedMap.has(key)) mergedMap.set(key, msg);
});
const messages = Array.from(mergedMap.values())
.sort((a, b) => {
if (a.ts !== b.ts) return a.ts - b.ts;
return Number(a.id || 0) - Number(b.id || 0);
})
.slice(-300);
const messages = mergeLatestMessages(existing, latestPage);
setBotMessages(target, messages);
const lastUser = [...messages].reverse().find((m) => m.role === 'user');

View File

@ -112,42 +112,7 @@ export function useDashboardChatMessageActions({
return matched?.id || null;
}, [hydrateLatestMessages, selectedBotId]);
const removeConversationMessageLocally = useCallback((message: ChatMessage, deletedMessageId: number) => {
if (!selectedBotId) return;
const originalMessageId = Number(message.id);
const hasOriginalId = Number.isFinite(originalMessageId) && originalMessageId > 0;
const idsToRemove = new Set<number>([deletedMessageId]);
if (hasOriginalId) {
idsToRemove.add(originalMessageId);
}
const scrollBox = chatScrollRef.current;
const prevTop = scrollBox?.scrollTop ?? null;
const normalizedTargetText = message.role === 'user'
? normalizeUserMessageText(message.text)
: normalizeAssistantMessageText(message.text);
const targetAttachments = JSON.stringify(message.attachments || []);
const nextMessages = messages.filter((row) => {
const rowId = Number(row.id);
if (Number.isFinite(rowId) && rowId > 0) {
return !idsToRemove.has(rowId);
}
if (hasOriginalId || row.role !== message.role) {
return true;
}
const normalizedRowText = row.role === 'user'
? normalizeUserMessageText(row.text)
: normalizeAssistantMessageText(row.text);
return !(
normalizedRowText === normalizedTargetText
&& JSON.stringify(row.attachments || []) === targetAttachments
&& Math.abs((row.ts || 0) - (message.ts || 0)) <= 1000
);
});
setBotMessages(selectedBotId, nextMessages);
const preserveChatScrollAfterMessagesChange = useCallback((prevTop: number | null) => {
if (prevTop === null || chatAutoFollowRef.current) return;
requestAnimationFrame(() => {
const box = chatScrollRef.current;
@ -155,7 +120,33 @@ export function useDashboardChatMessageActions({
const maxTop = Math.max(0, box.scrollHeight - box.clientHeight);
box.scrollTop = Math.min(prevTop, maxTop);
});
}, [chatAutoFollowRef, chatScrollRef, messages, selectedBotId, setBotMessages]);
}, [chatAutoFollowRef, chatScrollRef]);
const removeConversationMessageLocally = useCallback((deletedMessageId: number, originalMessageId?: number) => {
if (!selectedBotId) return;
const originalId = Number(originalMessageId);
const idsToRemove = new Set<number>([deletedMessageId]);
if (Number.isFinite(originalId) && originalId > 0) {
idsToRemove.add(originalId);
}
const scrollBox = chatScrollRef.current;
const prevTop = scrollBox?.scrollTop ?? null;
const nextMessages = messages.filter((row) => {
const rowId = Number(row.id);
return !(Number.isFinite(rowId) && rowId > 0 && idsToRemove.has(rowId));
});
setBotMessages(selectedBotId, nextMessages);
preserveChatScrollAfterMessagesChange(prevTop);
}, [chatScrollRef, messages, preserveChatScrollAfterMessagesChange, selectedBotId, setBotMessages]);
const refreshConversationMessages = useCallback(async () => {
if (!selectedBotId) return;
const scrollBox = chatScrollRef.current;
const prevTop = scrollBox?.scrollTop ?? null;
await hydrateLatestMessages(selectedBotId);
preserveChatScrollAfterMessagesChange(prevTop);
}, [chatScrollRef, hydrateLatestMessages, preserveChatScrollAfterMessagesChange, selectedBotId]);
const deleteConversationMessageOnServer = useCallback(async (messageId: number) => {
await axios.delete(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/messages/${messageId}`);
@ -166,7 +157,9 @@ export function useDashboardChatMessageActions({
notify(t.deleteMessagePending, { tone: 'warning' });
return;
}
let targetMessageId = Number(message.id);
const originalMessageId = Number(message.id);
const hasOriginalMessageId = Number.isFinite(originalMessageId) && originalMessageId > 0;
let targetMessageId = originalMessageId;
if (!Number.isFinite(targetMessageId) || targetMessageId <= 0) {
targetMessageId = Number(await resolveMessageIdFromLatest(message));
}
@ -189,7 +182,11 @@ export function useDashboardChatMessageActions({
setDeletingMessageIdMap((prev) => ({ ...prev, [targetMessageId]: true }));
try {
await deleteConversationMessageOnServer(targetMessageId);
removeConversationMessageLocally(message, targetMessageId);
if (hasOriginalMessageId) {
removeConversationMessageLocally(targetMessageId, originalMessageId);
} else {
await refreshConversationMessages();
}
notify(t.deleteMessageDone, { tone: 'success' });
} catch (error: unknown) {
notify(resolveApiErrorMessage(error, t.deleteMessageFail), { tone: 'error' });
@ -200,7 +197,7 @@ export function useDashboardChatMessageActions({
return next;
});
}
}, [confirm, deleteConversationMessageOnServer, deletingMessageIdMap, notify, removeConversationMessageLocally, resolveMessageIdFromLatest, selectedBotId, t]);
}, [confirm, deleteConversationMessageOnServer, deletingMessageIdMap, notify, refreshConversationMessages, removeConversationMessageLocally, resolveMessageIdFromLatest, selectedBotId, t]);
return {
deleteConversationMessage,