From 748bf7b63a718d388841b63f69ddd1db1b6b6c35 Mon Sep 17 00:00:00 2001 From: tanchekwei Date: Sun, 19 Nov 2023 12:20:32 +0800 Subject: [PATCH 1/4] Export chats and settings --- package-lock.json | 30 +++---- package.json | 2 + src/components/ChatSetting.vue | 138 ++++++++++++++++----------------- src/i18n/locales/en.json | 3 +- src/i18n/locales/zh.json | 3 +- 5 files changed, 87 insertions(+), 89 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1bb7f2c9c..8cb0d91ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,9 @@ "compare-versions": "^6.1.0", "core-js": "^3.32.2", "dexie": "^4.0.1-alpha.25", + "dexie-export-import": "^4.0.7", "electron-builder": "^24.6.4", + "jszip": "^3.10.1", "katex": "^0.16.8", "langchain": "~0.0.156", "localforage": "^1.10.0", @@ -7529,8 +7531,7 @@ "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, "node_modules/cors": { "version": "2.8.5", @@ -9118,6 +9119,14 @@ "resolved": "https://registry.npmmirror.com/dexie/-/dexie-4.0.1-beta.1.tgz", "integrity": "sha512-+IAkY8U09pavDFecWFOfTkwVN3NTmIsWZN2wh+RQ+9ya35Kr6AicQp+Tt1NDCM4UG2P/PDFBq/Mp4dt151LmuQ==" }, + "node_modules/dexie-export-import": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/dexie-export-import/-/dexie-export-import-4.0.7.tgz", + "integrity": "sha512-h22soiockhhWch6edw8XL/JNfn7akPLuLf6kPQdR4uneG/P0XQus4I8wpjV86dck61oEYKPHm36jyft/zVK0jQ==", + "peerDependencies": { + "dexie": "^2.0.4 || ^3.0.0 || ^4.0.1-alpha.5" + } + }, "node_modules/digest-fetch": { "version": "1.3.0", "resolved": "https://registry.npmmirror.com/digest-fetch/-/digest-fetch-1.3.0.tgz", @@ -13372,9 +13381,8 @@ }, "node_modules/jszip": { "version": "3.10.1", - "resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "dev": true, "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", @@ -13386,7 +13394,6 @@ "version": "3.3.0", "resolved": "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz", "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dev": true, "dependencies": { "immediate": "~3.0.5" } @@ -15988,8 +15995,7 @@ "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, "node_modules/param-case": { "version": "3.0.4", @@ -17039,8 +17045,7 @@ "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "node_modules/progress": { "version": "2.0.3", @@ -17504,7 +17509,6 @@ "version": "2.3.8", "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -17518,8 +17522,7 @@ "node_modules/readable-stream/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/readdirp": { "version": "3.6.0", @@ -18332,8 +18335,7 @@ "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" }, "node_modules/setprototypeof": { "version": "1.2.0", diff --git a/package.json b/package.json index 332930651..b75a588a2 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,9 @@ "compare-versions": "^6.1.0", "core-js": "^3.32.2", "dexie": "^4.0.1-alpha.25", + "dexie-export-import": "^4.0.7", "electron-builder": "^24.6.4", + "jszip": "^3.10.1", "katex": "^0.16.8", "langchain": "~0.0.156", "localforage": "^1.10.0", diff --git a/src/components/ChatSetting.vue b/src/components/ChatSetting.vue index e9b247f6d..04b0e813e 100644 --- a/src/components/ChatSetting.vue +++ b/src/components/ChatSetting.vue @@ -8,11 +8,10 @@ @click="deleteChats" > @@ -154,6 +153,9 @@ import { ref, computed } from "vue"; import { useStore } from "vuex"; import i18n from "@/i18n"; +import localForage from "localforage"; +import Dexie from "dexie"; +import { exportDB } from "dexie-export-import"; import ChatPrompt from "@/components/Messages/ChatPrompt.vue"; import ConfirmModal from "@/components/ConfirmModal.vue"; import bots from "@/bots"; @@ -163,6 +165,8 @@ import { templatePlaceholder, suffixPlaceholder, } from "../helpers/template-helper"; +import db from "@/store/db"; + const emit = defineEmits(["close-dialog"]); const confirmModal = ref(); const formRef = ref(null); @@ -206,83 +210,71 @@ let isEdit = false; const required = (value) => value?.trim() ? true : i18n.global.t("prompt.required"); -// This function downloads the chat history as a JSON file. -const downloadJson = () => { - // Get the chat history from localStorage. - const chatallMessages = localStorage.getItem("chatall-messages"); - if (!chatallMessages) { - console.error("chatall-messages not found in localStorage"); - return; - } - - const chats = JSON.parse(chatallMessages)?.chats ?? []; - - // Create an array of messages from the chat history. - const messages = chats - .filter((d) => !d.hide) - .map((chat) => ({ - // The title of the chat. - title: chat.title, - // The messages in the chat. - messages: chat.messages - .filter((d) => !d.hide) - .reduce((arr, message) => { - const t = message.type; - const content = message.content; - if (t == "prompt") { - arr.push({ - prompt: content, - responses: [], - }); - } else { - const botClassname = message.className; - const bot = bots.getBotByClassName(botClassname); - const botName = bot.getFullname(); - arr.at(-1).responses.push({ - content, - botName, - botClassname, - botModel: message.model, - highlight: message.highlight, - }); - } - return arr; - }, []), - })); +const SETTING_FILE_NAME = "localforage.json"; +const CHAT_HISTORY_FILE_NAME = "ChatALL.json"; +async function exportData() { + try { + const settingBlob = getSettingWithoutBotSetting(); + const chatHistoryBlob = await exportDB(db); - // Create a blob that contains the JSON data. - // The space parameter specifies the indentation of nested objects in the string representation. - const blob = new Blob([JSON.stringify({ chats: messages }, null, 2)], { - // The type of the blob. - type: "application/json", - }); + const { default: JSZip } = await import("jszip"); + const zip = new JSZip(); + zip.file(SETTING_FILE_NAME, settingBlob); + zip.file(CHAT_HISTORY_FILE_NAME, chatHistoryBlob); - const url = URL.createObjectURL(blob); + // Create a file name for the ZIP file. + const date = new Date(); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); // months are 0-based in JavaScript + const day = String(date.getDate()).padStart(2, "0"); + const hour = String(date.getHours()).padStart(2, "0"); + const minute = String(date.getMinutes()).padStart(2, "0"); + const second = String(date.getSeconds()).padStart(2, "0"); + const fileName = `chatall-history-${year}${month}${day}-${hour}${minute}${second}`; + zip.generateAsync({ type: "blob" }).then(function (zipBlob) { + const url = URL.createObjectURL(zipBlob); + const a = document.createElement("a"); + a.href = url; + a.download = `${fileName}.zip`; + document.body.appendChild(a); - // Create a file name for the JSON file. - const date = new Date(); - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, "0"); // months are 0-based in JavaScript - const day = String(date.getDate()).padStart(2, "0"); - const hour = String(date.getHours()).padStart(2, "0"); - const minute = String(date.getMinutes()).padStart(2, "0"); - const second = String(date.getSeconds()).padStart(2, "0"); - const fileName = `chatall-history-${year}${month}${day}-${hour}${minute}${second}`; + // Click the anchor element to download the file. + a.click(); - const a = document.createElement("a"); - a.href = url; - a.download = `${fileName}.json`; - document.body.appendChild(a); + // Remove the anchor element from the document body. + document.body.removeChild(a); - // Click the anchor element to download the file. - a.click(); + // Revoke the URL for the blob. + URL.revokeObjectURL(url); + }); + } catch (error) { + console.error(error); + snackbarWithCloseButton.text = `${i18n.global.t("chat.exportFailed")}: ${ + error.message + }`; + snackbarWithReloadButton.color = "error"; + snackbarWithCloseButton.show = true; + } +} - // Remove the anchor element from the document body. - document.body.removeChild(a); +async function getSettingWithoutBotSetting() { + await localForage._dbInfo.db.close(); + const settingDb = await new Dexie("localforage").open(); + const userSettingBlob = await exportDB(settingDb); + const userSettingText = await userSettingBlob.text(); + const userSettingJson = JSON.parse(userSettingText); + const allBotBrandId = bots.all.map((bot) => bot.constructor._brandId); + for (const brandId of allBotBrandId) { + // delete bot related setting + delete userSettingJson.data.data[0].rows[0].$[1][brandId]; + } + const str = JSON.stringify(userSettingJson); + const bytes = new TextEncoder().encode(str); + return new Blob([bytes], { + type: "application/json;charset=utf-8", + }); +} - // Revoke the URL for the blob. - URL.revokeObjectURL(url); -}; async function deleteChats() { const confirm = await confirmModal.value.showModal( "", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 8d1c7a7df..230d77f19 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -55,6 +55,7 @@ "newChat": "New Chat", "deleteAllChatHistory": "Delete All Chat History", "downloadAllChatHistory": "Save All Chat History", + "exportData": "Export Data (without secrets)", "confirmDeleteAllChatHistory": "Are you sure you want to delete all chat history? This action cannot be undone.", "actions": "Actions", "addAction": "Add Action", @@ -308,4 +309,4 @@ "25": "25", "50": "50", "100": "100" -} \ No newline at end of file +} diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index f55763499..39aac3282 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -55,6 +55,7 @@ "newChat": "新对话", "deleteAllChatHistory": "删除所有对话记录", "downloadAllChatHistory": "保存所有对话记录", + "exportData": "导出数据(不含机密)", "confirmDeleteAllChatHistory": "你确定要删除所有对话记录吗?此操作无法撤消。", "actions": "操作", "addAction": "添加操作", @@ -299,4 +300,4 @@ "25": "25", "50": "50", "100": "100" -} \ No newline at end of file +} From 98cff20306c17eaf214784147c24784b425e1c37 Mon Sep 17 00:00:00 2001 From: tanchekwei Date: Sun, 19 Nov 2023 14:43:42 +0800 Subject: [PATCH 2/4] Migrate setting array item index to use UUID for importing and exporting --- src/components/PromptModal.vue | 6 ++--- src/main.js | 1 + src/store/index.js | 42 +++++++++++++++++++++++++++------- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/src/components/PromptModal.vue b/src/components/PromptModal.vue index 1a5179eef..615454953 100644 --- a/src/components/PromptModal.vue +++ b/src/components/PromptModal.vue @@ -77,14 +77,14 @@ size="x-small" icon="mdi-pencil" @click="edit(item)" - v-if="item.index >= 0" + v-if="item.index" > @@ -197,7 +197,7 @@ const closeDialog = (value) => { }; function pin(row) { - if (row.index >= 0) { + if (row.index) { store.commit("editPrompt", { ...row, isPin: !row.isPin, diff --git a/src/main.js b/src/main.js index 2bcca5058..d5a103e79 100644 --- a/src/main.js +++ b/src/main.js @@ -42,6 +42,7 @@ const { ipcRenderer } = window.require("electron"); await store.restored; // wait for state to be restore store.commit("migrateSettingsPrompts"); +store.commit("migrateSettingArrayIndexUseUUID"); await migrateChatsMessagesThreads(); await Chats.addFirstChatIfEmpty(); diff --git a/src/store/index.js b/src/store/index.js index 3fb49be3c..47e8876c0 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -316,26 +316,34 @@ export default createStore({ }); }, addPrompt(state, values) { - const addPrompt = { ...values }; - addPrompt.index = state.prompts.push(addPrompt) - 1; + state.prompts.push({ ...values, index: uuidv4() }); }, editPrompt(state, values) { const { index } = values; - state.prompts[index] = { ...state.prompts[index], ...values }; + const prompt = state.prompts.find((item) => item.index === index); + for (const key in values) { + prompt[key] = values[key]; + } }, deletePrompt(state, values) { - state.prompts[values.index].hide = true; + const { index } = values; + let prompt = state.prompts.find((item) => item.index === index); + prompt.hide = true; }, addAction(state, values) { - const addAction = { ...values }; - addAction.index = state.actions.push(addAction) - 1; + state.actions.push({ ...values, index: uuidv4() }); }, editAction(state, values) { const { index } = values; - state.actions[index] = { ...state.actions[index], ...values }; + const action = state.actions.find((item) => item.index === index); + for (const key in values) { + action[key] = values[key]; + } }, deleteAction(state, values) { - state.actions[values.index].hide = true; + const { index } = values; + let action = state.actions.find((item) => item.index === index); + action.hide = true; }, addSelectedResponses(state, value) { value.selectedIndex = state.selectedResponses.push(value) - 1; @@ -361,6 +369,24 @@ export default createStore({ state.prompts = promptsData ? promptsData.prompts : []; localStorage.setItem("isMigratedSettingsPrompts", true); }, + migrateSettingArrayIndexUseUUID(state) { + if ( + localStorage.getItem("isMigrateSettingArrayIndexUseUUID") === "true" + ) { + return; + } + const settings = toRaw(state); + for (const key in settings) { + if (Array.isArray(state[key])) { + for (const item of state[key]) { + if (typeof item.index === "number" || !item.index) { + item.index = uuidv4(); + } + } + } + } + localStorage.setItem("isMigrateSettingArrayIndexUseUUID", true); + }, }, actions: { async sendPrompt({ commit, dispatch }, { prompt, bots, promptIndex }) { From e7d09b16c6e970b6b82886dc55714666d30d89bb Mon Sep 17 00:00:00 2001 From: tanchekwei Date: Sun, 19 Nov 2023 15:00:31 +0800 Subject: [PATCH 3/4] Import chats and settings --- src/components/ChatSetting.vue | 162 +++++++++++++++++++++++++++++++-- src/i18n/locales/en.json | 4 + src/i18n/locales/zh.json | 4 + src/store/index.js | 11 +++ 4 files changed, 171 insertions(+), 10 deletions(-) diff --git a/src/components/ChatSetting.vue b/src/components/ChatSetting.vue index 04b0e813e..d7332d7ed 100644 --- a/src/components/ChatSetting.vue +++ b/src/components/ChatSetting.vue @@ -14,6 +14,14 @@ @click="exportData" style="margin-left: 10px" > + + @@ -147,25 +155,49 @@ + + {{ snackbarWithReloadButton.text }} + + + + {{ snackbarWithCloseButton.text }} + +