mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-10-08 02:56:39 +08:00
Merge c234160b82
into 85704570f3
This commit is contained in:
commit
7f307bc299
@ -23,6 +23,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.import-config-modal {
|
||||||
|
.import-config-content {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.user-prompt-modal {
|
.user-prompt-modal {
|
||||||
min-height: 40vh;
|
min-height: 40vh;
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useMemo } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
|
|
||||||
import styles from "./settings.module.scss";
|
import styles from "./settings.module.scss";
|
||||||
|
import uiStyle from "./ui-lib.module.scss";
|
||||||
|
|
||||||
import ResetIcon from "../icons/reload.svg";
|
import ResetIcon from "../icons/reload.svg";
|
||||||
import AddIcon from "../icons/add.svg";
|
import AddIcon from "../icons/add.svg";
|
||||||
@ -80,6 +81,7 @@ import { useSyncStore } from "../store/sync";
|
|||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { useMaskStore } from "../store/mask";
|
import { useMaskStore } from "../store/mask";
|
||||||
import { ProviderType } from "../utils/cloud";
|
import { ProviderType } from "../utils/cloud";
|
||||||
|
import CancelIcon from "../icons/cancel.svg";
|
||||||
|
|
||||||
function EditPromptModal(props: { id: string; onClose: () => void }) {
|
function EditPromptModal(props: { id: string; onClose: () => void }) {
|
||||||
const promptStore = usePromptStore();
|
const promptStore = usePromptStore();
|
||||||
@ -477,6 +479,61 @@ function SyncConfigModal(props: { onClose?: () => void }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ImportConfigModal(props: { onClose?: () => void; rows?: number }) {
|
||||||
|
const [importString, setImportString] = useState("");
|
||||||
|
const syncStore = useSyncStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="modal-mask">
|
||||||
|
<Modal
|
||||||
|
title={Locale.Settings.Sync.Config.ImportModal.Title}
|
||||||
|
onClose={() => props.onClose?.()}
|
||||||
|
actions={[
|
||||||
|
<IconButton
|
||||||
|
key="cancel"
|
||||||
|
onClick={() => {
|
||||||
|
props.onClose?.();
|
||||||
|
}}
|
||||||
|
icon={<CancelIcon />}
|
||||||
|
bordered
|
||||||
|
shadow
|
||||||
|
text={Locale.UI.Cancel}
|
||||||
|
/>,
|
||||||
|
<IconButton
|
||||||
|
key="confirm"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await syncStore.importSyncConfig(importString);
|
||||||
|
showToast(Locale.Settings.Sync.ImportSuccess);
|
||||||
|
props.onClose?.();
|
||||||
|
} catch (e) {
|
||||||
|
showToast(Locale.Settings.Sync.ImportFail);
|
||||||
|
console.log("[Sync] Failed to import sync config", e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
icon={<ConfirmIcon />}
|
||||||
|
bordered
|
||||||
|
text={Locale.UI.Confirm}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div className={styles["import-config-modal"]}>
|
||||||
|
<textarea
|
||||||
|
className={uiStyle["modal-input"]}
|
||||||
|
autoFocus
|
||||||
|
placeholder={Locale.Settings.Sync.Config.ImportModal.Placeholder}
|
||||||
|
value={importString}
|
||||||
|
rows={props?.rows ?? 3}
|
||||||
|
onInput={(e) => {
|
||||||
|
setImportString(e.currentTarget.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function SyncItems() {
|
function SyncItems() {
|
||||||
const syncStore = useSyncStore();
|
const syncStore = useSyncStore();
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
@ -487,6 +544,7 @@ function SyncItems() {
|
|||||||
}, [syncStore]);
|
}, [syncStore]);
|
||||||
|
|
||||||
const [showSyncConfigModal, setShowSyncConfigModal] = useState(false);
|
const [showSyncConfigModal, setShowSyncConfigModal] = useState(false);
|
||||||
|
const [showImportModal, setShowImportModal] = useState(false);
|
||||||
|
|
||||||
const stateOverview = useMemo(() => {
|
const stateOverview = useMemo(() => {
|
||||||
const sessions = chatStore.sessions;
|
const sessions = chatStore.sessions;
|
||||||
@ -522,20 +580,36 @@ function SyncItems() {
|
|||||||
setShowSyncConfigModal(true);
|
setShowSyncConfigModal(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon={<DownloadIcon />}
|
||||||
|
text={Locale.UI.Import}
|
||||||
|
onClick={() => {
|
||||||
|
setShowImportModal(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{couldSync && (
|
{couldSync && (
|
||||||
<IconButton
|
<>
|
||||||
icon={<ResetIcon />}
|
<IconButton
|
||||||
text={Locale.UI.Sync}
|
icon={<ResetIcon />}
|
||||||
onClick={async () => {
|
text={Locale.UI.Sync}
|
||||||
try {
|
onClick={async () => {
|
||||||
await syncStore.sync();
|
try {
|
||||||
showToast(Locale.Settings.Sync.Success);
|
await syncStore.sync();
|
||||||
} catch (e) {
|
showToast(Locale.Settings.Sync.Success);
|
||||||
showToast(Locale.Settings.Sync.Fail);
|
} catch (e) {
|
||||||
console.error("[Sync]", e);
|
showToast(Locale.Settings.Sync.Fail);
|
||||||
}
|
console.error("[Sync]", e);
|
||||||
}}
|
}
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon={<UploadIcon />}
|
||||||
|
text={Locale.UI.Export}
|
||||||
|
onClick={() => {
|
||||||
|
syncStore.exportSyncConfig();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
@ -568,6 +642,10 @@ function SyncItems() {
|
|||||||
{showSyncConfigModal && (
|
{showSyncConfigModal && (
|
||||||
<SyncConfigModal onClose={() => setShowSyncConfigModal(false)} />
|
<SyncConfigModal onClose={() => setShowSyncConfigModal(false)} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showImportModal && (
|
||||||
|
<ImportConfigModal onClose={() => setShowImportModal(false)} />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -196,12 +196,19 @@ const cn = {
|
|||||||
NotSyncYet: "还没有进行过同步",
|
NotSyncYet: "还没有进行过同步",
|
||||||
Success: "同步成功",
|
Success: "同步成功",
|
||||||
Fail: "同步失败",
|
Fail: "同步失败",
|
||||||
|
ExportSuccess: "同步配置已复制到剪贴板",
|
||||||
|
ExportFail: "同步配置导出失败,请重试",
|
||||||
|
ImportSuccess: "同步配置导入成功",
|
||||||
|
ImportFail: "同步配置导入失败,请检查配置字符串",
|
||||||
Config: {
|
Config: {
|
||||||
Modal: {
|
Modal: {
|
||||||
Title: "配置云同步",
|
Title: "配置云同步",
|
||||||
Check: "检查可用性",
|
Check: "检查可用性",
|
||||||
},
|
},
|
||||||
|
ImportModal: {
|
||||||
|
Title: "导入同步配置",
|
||||||
|
Placeholder: "请输入同步配置",
|
||||||
|
},
|
||||||
SyncType: {
|
SyncType: {
|
||||||
Title: "同步类型",
|
Title: "同步类型",
|
||||||
SubTitle: "选择喜爱的同步服务器",
|
SubTitle: "选择喜爱的同步服务器",
|
||||||
|
@ -199,12 +199,19 @@ const en: LocaleType = {
|
|||||||
NotSyncYet: "Not sync yet",
|
NotSyncYet: "Not sync yet",
|
||||||
Success: "Sync Success",
|
Success: "Sync Success",
|
||||||
Fail: "Sync Fail",
|
Fail: "Sync Fail",
|
||||||
|
ExportSuccess: "Sync config copied to clipboard",
|
||||||
|
ExportFail: "Export failed, please retry",
|
||||||
|
ImportSuccess: "Sync config imported successfully",
|
||||||
|
ImportFail: "Import failed, please check config string",
|
||||||
Config: {
|
Config: {
|
||||||
Modal: {
|
Modal: {
|
||||||
Title: "Config Sync",
|
Title: "Config Sync",
|
||||||
Check: "Check Connection",
|
Check: "Check Connection",
|
||||||
},
|
},
|
||||||
|
ImportModal: {
|
||||||
|
Title: "Import Sync Config",
|
||||||
|
Placeholder: "Enter sync config",
|
||||||
|
},
|
||||||
SyncType: {
|
SyncType: {
|
||||||
Title: "Sync Type",
|
Title: "Sync Type",
|
||||||
SubTitle: "Choose your favorite sync service",
|
SubTitle: "Choose your favorite sync service",
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import pako from "pako";
|
||||||
import { getClientConfig } from "../config/client";
|
import { getClientConfig } from "../config/client";
|
||||||
import { Updater } from "../typing";
|
import { Updater } from "../typing";
|
||||||
import { ApiPath, STORAGE_KEY, StoreKey } from "../constant";
|
import { ApiPath, STORAGE_KEY, StoreKey } from "../constant";
|
||||||
@ -44,10 +45,52 @@ const DEFAULT_SYNC_STATE = {
|
|||||||
lastSyncTime: 0,
|
lastSyncTime: 0,
|
||||||
lastProvider: "",
|
lastProvider: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSyncStore = createPersistStore(
|
export const useSyncStore = createPersistStore(
|
||||||
DEFAULT_SYNC_STATE,
|
DEFAULT_SYNC_STATE,
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
|
async exportSyncConfig() {
|
||||||
|
const currentProvider = get().provider;
|
||||||
|
const exportData = {
|
||||||
|
provider: currentProvider,
|
||||||
|
config: get()[currentProvider],
|
||||||
|
useProxy: get().useProxy,
|
||||||
|
proxyUrl: get().proxyUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
const jsonString = JSON.stringify(exportData);
|
||||||
|
const compressed = pako.deflate(jsonString);
|
||||||
|
const encoded = btoa(
|
||||||
|
String.fromCharCode.apply(null, Array.from(compressed)),
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(encoded);
|
||||||
|
showToast(Locale.Settings.Sync.ExportSuccess);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("[Sync] failed to copy", e);
|
||||||
|
showToast(Locale.Settings.Sync.ExportFail);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
importSyncConfig(encodedString: string) {
|
||||||
|
try {
|
||||||
|
const decoded = atob(encodedString);
|
||||||
|
const decompressed = pako.inflate(
|
||||||
|
new Uint8Array(decoded.split("").map((char) => char.charCodeAt(0))),
|
||||||
|
{ to: "string" },
|
||||||
|
);
|
||||||
|
const importedData = JSON.parse(decompressed);
|
||||||
|
|
||||||
|
set({
|
||||||
|
provider: importedData.provider,
|
||||||
|
[importedData.provider]: importedData.config,
|
||||||
|
useProxy: importedData.useProxy,
|
||||||
|
proxyUrl: importedData.proxyUrl,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.log("[Sync] failed to set sync config", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
cloudSync() {
|
cloudSync() {
|
||||||
const config = get()[get().provider];
|
const config = get()[get().provider];
|
||||||
return Object.values(config).every((c) => c.toString().length > 0);
|
return Object.values(config).every((c) => c.toString().length > 0);
|
||||||
@ -100,15 +143,17 @@ export const useSyncStore = createPersistStore(
|
|||||||
const remoteState = await client.get(config.username);
|
const remoteState = await client.get(config.username);
|
||||||
if (!remoteState || remoteState === "") {
|
if (!remoteState || remoteState === "") {
|
||||||
await client.set(config.username, JSON.stringify(localState));
|
await client.set(config.username, JSON.stringify(localState));
|
||||||
console.log("[Sync] Remote state is empty, using local state instead.");
|
console.log(
|
||||||
return
|
"[Sync] Remote state is empty, using local state instead.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
const parsedRemoteState = JSON.parse(
|
const parsedRemoteState = JSON.parse(
|
||||||
await client.get(config.username),
|
await client.get(config.username),
|
||||||
) as AppState;
|
) as AppState;
|
||||||
mergeAppState(localState, parsedRemoteState);
|
mergeAppState(localState, parsedRemoteState);
|
||||||
setLocalAppState(localState);
|
setLocalAppState(localState);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("[Sync] failed to get remote state", e);
|
console.log("[Sync] failed to get remote state", e);
|
||||||
throw e;
|
throw e;
|
||||||
|
@ -34,6 +34,7 @@
|
|||||||
"nanoid": "^5.0.3",
|
"nanoid": "^5.0.3",
|
||||||
"next": "^14.1.1",
|
"next": "^14.1.1",
|
||||||
"node-fetch": "^3.3.1",
|
"node-fetch": "^3.3.1",
|
||||||
|
"pako": "^2.1.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-markdown": "^8.0.7",
|
"react-markdown": "^8.0.7",
|
||||||
@ -52,6 +53,7 @@
|
|||||||
"@tauri-apps/cli": "1.5.11",
|
"@tauri-apps/cli": "1.5.11",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^20.11.30",
|
"@types/node": "^20.11.30",
|
||||||
|
"@types/pako": "^2.0.3",
|
||||||
"@types/react": "^18.2.70",
|
"@types/react": "^18.2.70",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
"@types/react-katex": "^3.0.0",
|
"@types/react-katex": "^3.0.0",
|
||||||
|
10
yarn.lock
10
yarn.lock
@ -1735,6 +1735,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types "~5.26.4"
|
undici-types "~5.26.4"
|
||||||
|
|
||||||
|
"@types/pako@^2.0.3":
|
||||||
|
version "2.0.3"
|
||||||
|
resolved "https://registry.npmmirror.com/@types/pako/-/pako-2.0.3.tgz#b6993334f3af27c158f3fe0dfeeba987c578afb1"
|
||||||
|
integrity sha512-bq0hMV9opAcrmE0Byyo0fY3Ew4tgOevJmQ9grUhpXQhYfyLJ1Kqg3P33JT5fdbT2AjeAjR51zqqVjAL/HMkx7Q==
|
||||||
|
|
||||||
"@types/parse-json@^4.0.0":
|
"@types/parse-json@^4.0.0":
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||||
@ -5199,6 +5204,11 @@ p-map@^4.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
aggregate-error "^3.0.0"
|
aggregate-error "^3.0.0"
|
||||||
|
|
||||||
|
pako@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.npmmirror.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86"
|
||||||
|
integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==
|
||||||
|
|
||||||
parent-module@^1.0.0:
|
parent-module@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
|
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
|
||||||
|
Loading…
Reference in New Issue
Block a user