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

Chat ahzmr #5341

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
621b148
Merge branch 'ChatGPTNextWeb:main' into main
ahzmr Aug 15, 2024
eae593d
feat: Add automatic data synchronization settings and implementation,…
Aug 15, 2024
0a6ddda
Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next…
actions-user Aug 17, 2024
b2336f5
更新docker.yml, 修改自动编译的镜像为自己的账号
ahzmr Aug 18, 2024
fdb89af
更新docker.yml,使image名自适应,不影响主仓库
ahzmr Aug 18, 2024
0745b64
Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next…
actions-user Aug 20, 2024
5c51fd2
feat: 优化会话列表按最后更新时间倒序排序,更方便查看与管理
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
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),
);
}}
></input>
</ListItem>
Comment on lines +360 to +373
Copy link
Contributor

Choose a reason for hiding this comment

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

Refactor assignment within expression and improve code readability.

The new checkbox for enabling or disabling automatic synchronization is implemented correctly in terms of functionality. However, the static analysis tool flagged the use of an assignment within an expression in the onChange handler (line 369). This can lead to confusion and is generally considered bad practice as it can lead to side effects that are hard to track.

Consider refactoring the code to separate the assignment from the expression to improve readability and maintainability.

Here's a suggested refactor:

- syncStore.update((config) => (config.enableAutoSync = e.currentTarget.checked));
+ const isChecked = e.currentTarget.checked;
+ syncStore.update((config) => {
+   config.enableAutoSync = isChecked;
+   return config;
+ });
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
<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),
);
}}
></input>
</ListItem>
<ListItem
title={Locale.Settings.Sync.Config.EnableAutoSync.Title}
subTitle={Locale.Settings.Sync.Config.EnableAutoSync.SubTitle}
>
<input
type="checkbox"
checked={syncStore.enableAutoSync}
onChange={(e) => {
const isChecked = e.currentTarget.checked;
syncStore.update((config) => {
config.enableAutoSync = isChecked;
return config;
});
}}
></input>
</ListItem>
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)


<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";
import { indexedDBStorage } from "@/app/utils/indexedDB-storage";

Expand Down Expand Up @@ -63,6 +68,7 @@ export interface ChatSession {
lastUpdate: number;
lastSummarizeIndex: number;
clearContextIndex?: number;
deletedMessageIds?: Record<string, number>;

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

mask: createEmptyMask(),
};
Expand Down Expand Up @@ -163,9 +170,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 @@ -254,7 +271,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 @@ -271,19 +299,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 @@ -304,13 +337,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 === currentSession.id;
});

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;
}
Comment on lines +274 to +285
Copy link
Contributor

Choose a reason for hiding this comment

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

Review of removeOutdatedEntries Function

The removeOutdatedEntries function is designed to filter out entries from a timeMap that are older than one month. Here are some observations and suggestions:

  1. Correctness: The function correctly calculates the timestamp for one month ago and iterates over the keys of the timeMap to remove outdated entries. This logic is sound for the intended functionality.

  2. Performance: The use of forEach for iterating over the keys and conditionally deleting properties is efficient for this use case. However, if timeMap can be very large, consider potential performance implications of deleting properties in a large object.

  3. Maintainability: The function is straightforward and easy to understand. The comments are helpful for understanding the purpose of the code.

  4. Improvement Suggestion: Consider returning a new object instead of mutating the input timeMap. This would make the function pure and avoid side effects, which is generally a good practice in functional programming and can help prevent bugs in larger applications.

Here's a suggested refactor to return a new object:

-export function removeOutdatedEntries(timeMap: Record<string, number>): Record<string, number> {
+export function removeOutdatedEntries(timeMap: Record<string, number>): Record<string, number> {
  const oneMonthAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
  const newTimeMap = {...timeMap};
  Object.keys(newTimeMap).forEach((id) => {
    if (newTimeMap[id] < oneMonthAgo) {
      delete newTimeMap[id];
    }
  });
  return newTimeMap;
}

This change ensures that the original timeMap is not modified, which can be beneficial if the original data needs to be retained for other operations or logging.

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
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;
}
export function removeOutdatedEntries(timeMap: Record<string, number>): Record<string, number> {
const oneMonthAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
const newTimeMap = {...timeMap};
Object.keys(newTimeMap).forEach((id) => {
if (newTimeMap[id] < oneMonthAgo) {
delete newTimeMap[id];
}
});
return newTimeMap;
}

Loading
Loading