Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Enhance cloud synchronization functionality, support syncing delete operations for messages and conversations, and add support for automatic sync settings #5236

Open
wants to merge 45 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
1d0a40b
chore: low the google safety setting to avoid unexpected blocking
fredliang44 Dec 31, 2023
78c4084
Merge pull request #4148 from ChatGPTNextWeb/main
fred-bf Feb 27, 2024
1cce87a
Merge pull request #4181 from ChatGPTNextWeb/main
fred-bf Mar 1, 2024
cd354cf
Merge pull request #4685 from ChatGPTNextWeb/main
fred-bf May 14, 2024
d957397
Merge remote-tracking branch 'origin/main' into website
lloydzhou Jul 13, 2024
284d33b
Merge remote-tracking branch 'origin/main' into website
lloydzhou Jul 19, 2024
c440637
Merge remote-tracking branch 'origin/main' into website
lloydzhou Jul 26, 2024
22f6129
fix: Fixed an issue where the sample of the reply content was display…
Aug 2, 2024
5065091
fix: Fixed the issue that WebDAV synchronization could not check the …
Aug 3, 2024
22c7959
feat: The cloud synchronization feature is enhanced to support the sy…
Aug 3, 2024
faac0d9
Merge remote-tracking branch 'origin/main' into website
lloydzhou Aug 6, 2024
4f876f3
Merge tag 'v2.14.1' into website
Aug 8, 2024
648e600
feat: The cloud synchronization feature is enhanced to support the sy…
Aug 3, 2024
93bfb55
Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next…
actions-user Aug 14, 2024
4b22aaf
feat: Add automatic data synchronization settings and implementation,…
Aug 15, 2024
621b148
Merge branch 'ChatGPTNextWeb:main' into main
ahzmr Aug 15, 2024
eae593d
feat: Add automatic data synchronization settings and implementation,…
Aug 15, 2024
2ee2d50
Merge remote-tracking branch 'up/website' into website
Aug 15, 2024
5e1064a
Merge branch 'main' into website
lloydzhou Aug 16, 2024
0a6ddda
Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next…
actions-user Aug 17, 2024
b2336f5
更新docker.yml, 修改自动编译的镜像为自己的账号
ahzmr Aug 18, 2024
31f2829
Merge remote-tracking branch 'up/website' into website
ahzmr Aug 18, 2024
e515f0f
更新docker.yml, 修改自动编译的镜像为自己的账号
ahzmr Aug 18, 2024
fdb89af
更新docker.yml,使image名自适应,不影响主仓库
ahzmr Aug 18, 2024
fc97c4b
更新docker.yml,使image名自适应,不影响主仓库
ahzmr Aug 18, 2024
0745b64
Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next…
actions-user Aug 20, 2024
d0b7ddc
feat: 优化会话列表按最后更新时间倒序排序,更方便查看与管理
Aug 20, 2024
5c51fd2
feat: 优化会话列表按最后更新时间倒序排序,更方便查看与管理
Aug 20, 2024
2fdb35b
fix: 解决会话列表按最新操作时间倒序排序,当前会话判断失败的bug
Aug 20, 2024
31baa10
fix: 解决会话列表按最新操作时间倒序排序,当前会话判断失败的bug
Aug 20, 2024
f1d69cb
Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next…
actions-user Aug 21, 2024
2d68f17
Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next…
actions-user Aug 22, 2024
0638db1
Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next…
actions-user Aug 25, 2024
e8c7ac0
Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next…
actions-user Aug 28, 2024
2bf72d0
Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next…
actions-user Aug 30, 2024
c204031
Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next…
actions-user Sep 5, 2024
ccacfec
feat: 优化聊天窗口,使支持复制会话
ahzmr Sep 5, 2024
6dc8681
fix: 优化云同步功能,使access配置按更新时间合并,解决自定义模型配置在同步后丢失的问题
ahzmr Sep 5, 2024
6f3d753
Merge remote-tracking branch 'origin/main' into website
lloydzhou Sep 6, 2024
5ae4921
fix: 优化云同步功能,自动去除掉非首个空会话,避免多个空会话在中间,更方便管理
ahzmr Sep 6, 2024
370ce3e
Merge remote-tracking branch 'up/website' into website
ahzmr Sep 7, 2024
9551f5d
Merge branch 'website'
ahzmr Sep 7, 2024
35f5288
Merge remote-tracking branch 'up/main'
ahzmr Sep 12, 2024
144fdc9
Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next…
actions-user Sep 13, 2024
659a389
Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next…
actions-user Sep 14, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,26 @@ jobs:
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-

-
name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: yidadaa/chatgpt-next-web
images: ${{ secrets.DOCKER_USERNAME }}/chatgpt-next-web
tags: |
type=raw,value=latest
type=ref,event=tag
-

-
name: Set up QEMU
uses: docker/setup-qemu-action@v2

-
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
-

-
name: Build and push Docker image
uses: docker/build-push-action@v4
with:
Expand All @@ -49,4 +49,4 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

19 changes: 15 additions & 4 deletions app/components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import {
getMessageImages,
isVisionModel,
isDalle3,
removeOutdatedEntries,
} from "../utils";

import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
Expand Down Expand Up @@ -1023,10 +1024,20 @@ function _Chat() {
};

const deleteMessage = (msgId?: string) => {
chatStore.updateCurrentSession(
(session) =>
(session.messages = session.messages.filter((m) => m.id !== msgId)),
);
chatStore.updateCurrentSession((session) => {
session.deletedMessageIds &&
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) => {
Expand Down
15 changes: 15 additions & 0 deletions app/components/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,21 @@ function SyncConfigModal(props: { onClose?: () => void }) {
</select>
</ListItem>

<ListItem
title={Locale.Settings.Sync.Config.EnableAutoSync.Title}
subTitle={Locale.Settings.Sync.Config.EnableAutoSync.SubTitle}
>
<input
type="checkbox"
checked={syncStore.enableAutoSync}
onChange={(e) => {
syncStore.update(
(config) => (config.enableAutoSync = e.currentTarget.checked),
);
Comment on lines +368 to +370
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid assignments within expressions.

The assignment within the expression can lead to confusion and potential side effects. Consider refactoring to separate the assignment from the update function call.

- syncStore.update(
-   (config) => (config.enableAutoSync = e.currentTarget.checked),
- );
+ const enableAutoSync = e.currentTarget.checked;
+ syncStore.update((config) => {
+   config.enableAutoSync = enableAutoSync;
+ });
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
syncStore.update(
(config) => (config.enableAutoSync = e.currentTarget.checked),
);
const enableAutoSync = e.currentTarget.checked;
syncStore.update((config) => {
config.enableAutoSync = enableAutoSync;
});
Tools
Biome

[error] 369-369: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

}}
></input>
</ListItem>

<ListItem
title={Locale.Settings.Sync.Config.Proxy.Title}
subTitle={Locale.Settings.Sync.Config.Proxy.SubTitle}
Expand Down
4 changes: 4 additions & 0 deletions app/locales/cn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ const cn = {
Title: "同步类型",
SubTitle: "选择喜爱的同步服务器",
},
EnableAutoSync: {
Title: "自动同步设置",
SubTitle: "在回复完成或删除消息后自动同步数据",
},
Proxy: {
Title: "启用代理",
SubTitle: "在浏览器中同步时,必须启用代理以避免跨域限制",
Expand Down
5 changes: 5 additions & 0 deletions app/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,11 @@ const en: LocaleType = {
Title: "Sync Type",
SubTitle: "Choose your favorite sync service",
},
EnableAutoSync: {
Title: "Auto Sync Settings",
SubTitle:
"Automatically synchronize data after replying or deleting messages",
},
Proxy: {
Title: "Enable CORS Proxy",
SubTitle: "Enable a proxy to avoid cross-origin restrictions",
Expand Down
57 changes: 55 additions & 2 deletions app/store/chat.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { trimTopic, getMessageTextContent } from "../utils";
import {
trimTopic,
getMessageTextContent,
removeOutdatedEntries,
} from "../utils";

import Locale, { getLang } from "../locales";
import { showToast } from "../components/ui-lib";
Expand Down Expand Up @@ -26,6 +30,7 @@ import { nanoid } from "nanoid";
import { createPersistStore } from "../utils/store";
import { collectModelsWithDefaultModel } from "../utils/model";
import { useAccessStore } from "./access";
import { useSyncStore } from "./sync";
import { isDalle3 } from "../utils";

export type ChatMessage = RequestMessage & {
Expand Down Expand Up @@ -62,6 +67,7 @@ export interface ChatSession {
lastUpdate: number;
lastSummarizeIndex: number;
clearContextIndex?: number;
deletedMessageIds?: Record<string, number>;

mask: Mask;
}
Expand All @@ -85,6 +91,7 @@ function createEmptySession(): ChatSession {
},
lastUpdate: Date.now(),
lastSummarizeIndex: 0,
deletedMessageIds: {},

mask: createEmptyMask(),
};
Expand Down Expand Up @@ -162,9 +169,19 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) {
return output;
}

let cloudSyncTimer: any = null;
function noticeCloudSync(): void {
const syncStore = useSyncStore.getState();
cloudSyncTimer && clearTimeout(cloudSyncTimer);
cloudSyncTimer = setTimeout(() => {
syncStore.autoSync();
}, 500);
}

const DEFAULT_CHAT_STATE = {
sessions: [createEmptySession()],
currentSessionIndex: 0,
deletedSessionIds: {} as Record<string, number>,
};

export const useChatStore = createPersistStore(
Expand Down Expand Up @@ -253,7 +270,18 @@ export const useChatStore = createPersistStore(
if (!deletedSession) return;

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;
let nextIndex = Math.min(
Expand All @@ -270,19 +298,24 @@ export const useChatStore = createPersistStore(
const restoreState = {
currentSessionIndex: get().currentSessionIndex,
sessions: get().sessions.slice(),
deletedSessionIds: get().deletedSessionIds,
};

set(() => ({
currentSessionIndex: nextIndex,
sessions,
deletedSessionIds,
}));

noticeCloudSync();

showToast(
Locale.Home.DeleteToast,
{
text: Locale.Home.Revert,
onClick() {
set(() => restoreState);
noticeCloudSync();
},
},
5000,
Expand All @@ -303,13 +336,33 @@ export const useChatStore = createPersistStore(
return session;
},

sortSessions() {
const currentSession = get().currentSession();
const sessions = get().sessions.slice();

sessions.sort(
(a, b) =>
new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime(),
);
const currentSessionIndex = sessions.findIndex((session) => {
return session && currentSession && session.id === session.id;
});
ahzmr marked this conversation as resolved.
Show resolved Hide resolved

set((state) => ({
currentSessionIndex,
sessions,
}));
},

onNewMessage(message: ChatMessage) {
get().updateCurrentSession((session) => {
session.messages = session.messages.concat();
session.lastUpdate = Date.now();
});
get().updateStat(message);
get().summarizeSession();
get().sortSessions();
noticeCloudSync();
},

async onUserInput(content: string, attachImages?: string[]) {
Expand Down
33 changes: 27 additions & 6 deletions app/store/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type SyncStore = GetStoreState<typeof useSyncStore>;

const DEFAULT_SYNC_STATE = {
provider: ProviderType.WebDAV,
enableAutoSync: true,
useProxy: true,
proxyUrl: corsPath(ApiPath.Cors),

Expand All @@ -45,6 +46,8 @@ const DEFAULT_SYNC_STATE = {
lastProvider: "",
};

let lastSyncTime = 0;

export const useSyncStore = createPersistStore(
DEFAULT_SYNC_STATE,
(set, get) => ({
Expand Down Expand Up @@ -91,6 +94,16 @@ export const useSyncStore = createPersistStore(
},

async sync() {
if (lastSyncTime && lastSyncTime >= Date.now() - 800) {
return;
}
lastSyncTime = Date.now();

const enableAutoSync = get().enableAutoSync;
if (!enableAutoSync) {
return;
}

const localState = getLocalAppState();
const provider = get().provider;
const config = get()[provider];
Expand All @@ -100,15 +113,15 @@ export const useSyncStore = createPersistStore(
const remoteState = await client.get(config.username);
if (!remoteState || remoteState === "") {
await client.set(config.username, JSON.stringify(localState));
console.log("[Sync] Remote state is empty, using local state instead.");
return
console.log(
"[Sync] Remote state is empty, using local state instead.",
);
return;
} else {
const parsedRemoteState = JSON.parse(
await client.get(config.username),
) as AppState;
const parsedRemoteState = JSON.parse(remoteState) as AppState;
mergeAppState(localState, parsedRemoteState);
setLocalAppState(localState);
}
}
} catch (e) {
console.log("[Sync] failed to get remote state", e);
throw e;
Expand All @@ -123,6 +136,14 @@ export const useSyncStore = createPersistStore(
const client = this.getClient();
return await client.check();
},

async autoSync() {
const { lastSyncTime, provider } = get();
const syncStore = useSyncStore.getState();
if (lastSyncTime && syncStore.cloudSync()) {
syncStore.sync();
}
},
}),
{
name: StoreKey.Sync,
Expand Down
13 changes: 13 additions & 0 deletions app/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,3 +270,16 @@ export function isVisionModel(model: string) {
export function isDalle3(model: string) {
return "dall-e-3" === model;
}

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;
}
Loading
Loading