feat: The cloud synchronization feature is enhanced to support the synchronization of deleted conversations and deleted messages

This commit is contained in:
李超 2024-08-03 20:53:36 +08:00
parent 5065091b74
commit 22c79595fb
4 changed files with 96 additions and 8 deletions

View File

@ -60,6 +60,7 @@ import {
getMessageTextContent, getMessageTextContent,
getMessageImages, getMessageImages,
isVisionModel, isVisionModel,
removeOutdatedEntries,
} from "../utils"; } from "../utils";
import { uploadImage as uploadImageRemote } from "@/app/utils/chat"; import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
@ -923,10 +924,20 @@ function _Chat() {
}; };
const deleteMessage = (msgId?: string) => { const deleteMessage = (msgId?: string) => {
chatStore.updateCurrentSession( chatStore.updateCurrentSession((session) => {
(session) => session.deletedMessageIds &&
(session.messages = session.messages.filter((m) => m.id !== msgId)), removeOutdatedEntries(session.deletedMessageIds);
); session.messages = session.messages.filter((m) => {
if (m.id !== msgId) {
return true;
}
if (!session.deletedMessageIds) {
session.deletedMessageIds = {} as Record<string, number>;
}
session.deletedMessageIds[m.id] = Date.now();
return false;
});
});
}; };
const onDelete = (msgId: string) => { const onDelete = (msgId: string) => {

View File

@ -1,4 +1,8 @@
import { trimTopic, getMessageTextContent } from "../utils"; import {
trimTopic,
getMessageTextContent,
removeOutdatedEntries,
} from "../utils";
import Locale, { getLang } from "../locales"; import Locale, { getLang } from "../locales";
import { showToast } from "../components/ui-lib"; import { showToast } from "../components/ui-lib";
@ -61,6 +65,7 @@ export interface ChatSession {
lastUpdate: number; lastUpdate: number;
lastSummarizeIndex: number; lastSummarizeIndex: number;
clearContextIndex?: number; clearContextIndex?: number;
deletedMessageIds?: Record<string, number>;
mask: Mask; mask: Mask;
} }
@ -84,6 +89,7 @@ function createEmptySession(): ChatSession {
}, },
lastUpdate: Date.now(), lastUpdate: Date.now(),
lastSummarizeIndex: 0, lastSummarizeIndex: 0,
deletedMessageIds: {},
mask: createEmptyMask(), mask: createEmptyMask(),
}; };
@ -164,6 +170,7 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) {
const DEFAULT_CHAT_STATE = { const DEFAULT_CHAT_STATE = {
sessions: [createEmptySession()], sessions: [createEmptySession()],
currentSessionIndex: 0, currentSessionIndex: 0,
deletedSessionIds: {} as Record<string, number>,
}; };
export const useChatStore = createPersistStore( export const useChatStore = createPersistStore(
@ -252,7 +259,18 @@ export const useChatStore = createPersistStore(
if (!deletedSession) return; if (!deletedSession) return;
const sessions = get().sessions.slice(); const sessions = get().sessions.slice();
sessions.splice(index, 1); const deletedSessionIds = { ...get().deletedSessionIds };
removeOutdatedEntries(deletedSessionIds);
const hasDelSessions = sessions.splice(index, 1);
if (hasDelSessions?.length) {
hasDelSessions.forEach((session) => {
if (session.messages.length > 0) {
deletedSessionIds[session.id] = Date.now();
}
});
}
const currentIndex = get().currentSessionIndex; const currentIndex = get().currentSessionIndex;
let nextIndex = Math.min( let nextIndex = Math.min(
@ -269,11 +287,13 @@ export const useChatStore = createPersistStore(
const restoreState = { const restoreState = {
currentSessionIndex: get().currentSessionIndex, currentSessionIndex: get().currentSessionIndex,
sessions: get().sessions.slice(), sessions: get().sessions.slice(),
deletedSessionIds: get().deletedSessionIds,
}; };
set(() => ({ set(() => ({
currentSessionIndex: nextIndex, currentSessionIndex: nextIndex,
sessions, sessions,
deletedSessionIds,
})); }));
showToast( showToast(

View File

@ -265,3 +265,16 @@ export function isVisionModel(model: string) {
visionKeywords.some((keyword) => model.includes(keyword)) || isGpt4Turbo visionKeywords.some((keyword) => model.includes(keyword)) || isGpt4Turbo
); );
} }
export function removeOutdatedEntries(
timeMap: Record<string, number>,
): Record<string, number> {
const oneMonthAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
// Delete data from a month ago
Object.keys(timeMap).forEach((id) => {
if (timeMap[id] < oneMonthAgo) {
delete timeMap[id];
}
});
return timeMap;
}

View File

@ -8,6 +8,7 @@ import { useMaskStore } from "../store/mask";
import { usePromptStore } from "../store/prompt"; import { usePromptStore } from "../store/prompt";
import { StoreKey } from "../constant"; import { StoreKey } from "../constant";
import { merge } from "./merge"; import { merge } from "./merge";
import { removeOutdatedEntries } from "@/app/utils";
type NonFunctionKeys<T> = { type NonFunctionKeys<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? never : K; [K in keyof T]: T[K] extends (...args: any[]) => any ? never : K;
@ -66,6 +67,7 @@ const MergeStates: StateMerger = {
[StoreKey.Chat]: (localState, remoteState) => { [StoreKey.Chat]: (localState, remoteState) => {
// merge sessions // merge sessions
const localSessions: Record<string, ChatSession> = {}; const localSessions: Record<string, ChatSession> = {};
const localDeletedSessionIds = localState.deletedSessionIds || {};
localState.sessions.forEach((s) => (localSessions[s.id] = s)); localState.sessions.forEach((s) => (localSessions[s.id] = s));
remoteState.sessions.forEach((remoteSession) => { remoteState.sessions.forEach((remoteSession) => {
@ -75,29 +77,71 @@ const MergeStates: StateMerger = {
const localSession = localSessions[remoteSession.id]; const localSession = localSessions[remoteSession.id];
if (!localSession) { if (!localSession) {
// if remote session is new, just merge it // if remote session is new, just merge it
localState.sessions.push(remoteSession); if (
(localDeletedSessionIds[remoteSession.id] || -1) <
remoteSession.lastUpdate
) {
localState.sessions.push(remoteSession);
}
} else { } else {
// if both have the same session id, merge the messages // if both have the same session id, merge the messages
const localMessageIds = new Set(localSession.messages.map((v) => v.id)); const localMessageIds = new Set(localSession.messages.map((v) => v.id));
const localDeletedMessageIds = localSession.deletedMessageIds || {};
remoteSession.messages.forEach((m) => { remoteSession.messages.forEach((m) => {
if (!localMessageIds.has(m.id)) { if (!localMessageIds.has(m.id)) {
localSession.messages.push(m); if (
!localDeletedMessageIds[m.id] ||
new Date(localDeletedMessageIds[m.id]).toLocaleString() < m.date
) {
localSession.messages.push(m);
}
} }
}); });
const remoteDeletedMessageIds = remoteSession.deletedMessageIds || {};
localSession.messages = localSession.messages.filter((localMessage) => {
return (
!remoteDeletedMessageIds[localMessage.id] ||
new Date(localDeletedMessageIds[localMessage.id]).toLocaleString() <
localMessage.date
);
});
// sort local messages with date field in asc order // sort local messages with date field in asc order
localSession.messages.sort( localSession.messages.sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(), (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(),
); );
const deletedMessageIds = {
...remoteDeletedMessageIds,
...localDeletedMessageIds,
};
removeOutdatedEntries(deletedMessageIds);
localSession.deletedMessageIds = deletedMessageIds;
} }
}); });
const remoteDeletedSessionIds = remoteState.deletedSessionIds || {};
localState.sessions = localState.sessions.filter((localSession) => {
return (
(remoteDeletedSessionIds[localSession.id] || -1) <=
localSession.lastUpdate
);
});
// sort local sessions with date field in desc order // sort local sessions with date field in desc order
localState.sessions.sort( localState.sessions.sort(
(a, b) => (a, b) =>
new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime(), new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime(),
); );
const deletedSessionIds = {
...remoteDeletedSessionIds,
...localDeletedSessionIds,
};
removeOutdatedEntries(deletedSessionIds);
localState.deletedSessionIds = deletedSessionIds;
return localState; return localState;
}, },
[StoreKey.Prompt]: (localState, remoteState) => { [StoreKey.Prompt]: (localState, remoteState) => {