From e4c06cecfae8b370d71ad436196ddd01941bfae2 Mon Sep 17 00:00:00 2001 From: cbh778899 Date: Mon, 21 Oct 2024 14:59:12 +1100 Subject: [PATCH 1/4] add function to save file Signed-off-by: cbh778899 --- electron.js | 16 +++++++++++++- preloader/file-handler.js | 44 +++++++++++++++++++++++++++++++++++++++ preloader/index.js | 8 +++++++ 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 preloader/file-handler.js diff --git a/electron.js b/electron.js index 8e16877..45fdfbb 100644 --- a/electron.js +++ b/electron.js @@ -1,5 +1,5 @@ // eslint-disable-next-line -const { app, Menu, BrowserWindow, ipcMain } = require('electron'); +const { app, Menu, BrowserWindow, ipcMain, dialog } = require('electron'); // eslint-disable-next-line const path = require('path'); @@ -49,4 +49,18 @@ app.whenReady().then(() => { ipcMain.handle('electron-settings', ()=>{ return { userDataPath: app.getPath("userData"), isPackaged: app.isPackaged } +}) + +ipcMain.handle('os-settings', ()=>{ + return { + downloads_path: app.getPath("downloads") + } +}) + +ipcMain.handle('show-save-dialog', async (event, ...args) => { + return await dialog.showSaveDialog(...args); +}) + +ipcMain.handle('show-open-dialog', async (event, ...args) => { + return await dialog.showOpenDialog(...args); }) \ No newline at end of file diff --git a/preloader/file-handler.js b/preloader/file-handler.js new file mode 100644 index 0000000..e1d5cee --- /dev/null +++ b/preloader/file-handler.js @@ -0,0 +1,44 @@ +const { ipcRenderer } = require('electron'); +const { join } = require("path") +const { writeFileSync, readFileSync } = require("fs") + +let save_path = '.'; + +async function initer() { + const {downloads_path} = await ipcRenderer.invoke('os-settings'); + save_path = downloads_path; +} +initer(); + +async function saveFile(name, content) { + const { canceled, filePath } = await ipcRenderer.invoke('show-save-dialog', { + title: "Save Chat History", + defaultPath: join(save_path, name) + }) + + if(canceled || !filePath) { + return false; + } else { + writeFileSync(filePath, content, 'utf-8'); + return true; + } +} + +async function loadFile() { + const { canceled, filePaths } = await ipcRenderer.invoke('show-open-dialog', { + title: "Load Chat History", + properties: ["openFile"] + }) + + if(canceled || !filePaths) { + return null; + } else { + const content = readFileSync(filePaths[0], 'utf-8'); + return content; + } +} + +module.exports = { + saveFile, + loadFile +} \ No newline at end of file diff --git a/preloader/index.js b/preloader/index.js index b4d8885..90a1552 100644 --- a/preloader/index.js +++ b/preloader/index.js @@ -8,7 +8,15 @@ const { updateModelSettings } = require("./node-llama-cpp-preloader.js") +const { + loadFile, saveFile +} = require("./file-handler.js") + contextBridge.exposeInMainWorld('node-llama-cpp', { loadModel, chatCompletions, updateModelSettings, abortCompletion, setClient, downloadModel +}) + +contextBridge.exposeInMainWorld('file-handler', { + loadFile, saveFile }) \ No newline at end of file From ff8bdd671d60dd04a7ab08c6fda356e67f48d31f Mon Sep 17 00:00:00 2001 From: cbh778899 Date: Mon, 21 Oct 2024 14:59:34 +1100 Subject: [PATCH 2/4] add handler to format chat history for export Signed-off-by: cbh778899 --- src/utils/chat-history-handler.js | 97 +++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 src/utils/chat-history-handler.js diff --git a/src/utils/chat-history-handler.js b/src/utils/chat-history-handler.js new file mode 100644 index 0000000..03ebc48 --- /dev/null +++ b/src/utils/chat-history-handler.js @@ -0,0 +1,97 @@ +/** + * @typedef HistoryItem + * @property {"user"|"assistant"|"system"} role + * @property {String} content + * @property {Number} createdAt + */ + +/** + * @typedef HistoryInfo + * @property {Number} createdAt + * @property {String} title + */ + +/** + * Export history by generate a file + * @param {"Human"|"JSON"} format the format to export, either human readable or json + * @param {HistoryInfo} history_info the information of the session, includes create time and title + * @param {HistoryItem[]} history the history to be exported + */ +export async function exportChatHistory(format, history_info, history) { + let formatted_sentence; + switch(format) { + case "JSON": + formatted_sentence = jsonFormator(history_info, history); + break; + case "Human": + formatted_sentence = humanFormator(history_info, history); + break; + } + // const file = new Blob([formatted_sentence], { type: "text/plain" }); + + const extension = + format === "JSON" ? 'json' : + format === "Human" ? 'txt' : + '' + + await window['file-handler'].saveFile(`${history_info.title}.${extension}`, formatted_sentence); +} + +function dateFormator(timestamp) { + const dt = new Date(timestamp); + return `${dt.toDateString()}, ${dt.toTimeString().match(/(\d{2}:\d{2}:\d{2})/)[0]}` +} + +/** + * Format history to human readable format and return the formatted string + * @param {HistoryInfo} history_info the information of the session to be formatted + * @param {HistoryItem[]} history the history to be formatted + * @returns {String} + */ +function humanFormator(history_info, history) { + const parts = [ +`${history_info.title} +${dateFormator(history_info.createdAt)}` + ] + + let current_part = ''; + for(const { role, content, createdAt } of history) { + current_part += + role === "user" ? 'user ' : + role === "system" ? 'system ': + role === 'assistant' ? 'assistant' : '' + current_part += ` (${dateFormator(createdAt)}): ` + current_part += content + + if(/^(system|user)$/.test(role)) { + current_part += '\n'; + } else { + parts.push(current_part); + current_part = '' + } + } + + return parts.join('\n\n---\n\n') +} + +/** + * Format history to json format and return the formatted string + * @param {HistoryInfo} history_info the information of the session to be formatted + * @param {HistoryItem[]} history the history to be formatted + * @returns {String} + */ +function jsonFormator(history_info, history) { + const { title, createdAt } = history_info; + const formatted = +`{ + "title": "${title}", + "createdAt": ${createdAt}, + "messages": [ + ${history.map(({role, content, createdAt})=>{ + const msg_str = content.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll("\n", "\\n") + return `{ "role": "${role}", "message": "${msg_str}", "createdAt": ${createdAt} }` + }).join(`,\n${" ".repeat(8)}`)} + ] +}` + return formatted +} \ No newline at end of file From f59d813b71706f9a20fb85d2b403ceef81b3c3d3 Mon Sep 17 00:00:00 2001 From: cbh778899 Date: Mon, 21 Oct 2024 14:59:47 +1100 Subject: [PATCH 3/4] implement export history Signed-off-by: cbh778899 --- src/components/chat/ChatPage.jsx | 3 ++- src/components/chat/TitleBar.jsx | 39 ++++++++++++++++++++++++++++---- src/components/chat/index.jsx | 9 +++++++- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/components/chat/ChatPage.jsx b/src/components/chat/ChatPage.jsx index 13c053b..55239c2 100644 --- a/src/components/chat/ChatPage.jsx +++ b/src/components/chat/ChatPage.jsx @@ -5,7 +5,7 @@ import UserMessage from "./UserMessage"; export default function ChatPage({ chat, chat_history, updateTitle, sendMessage, pending_message, abort, - updateSystemInstruction + updateSystemInstruction, saveHistory }) { return ( @@ -15,6 +15,7 @@ export default function ChatPage({ current_title={chat.title} updateTitle={updateTitle} updateSystemInstruction={updateSystemInstruction} current_instruction={chat['system-instruction']} + saveHistory={saveHistory} /> { setSystemInstruction(current_instruction); }, [current_instruction]) @@ -65,7 +77,7 @@ export default function TitleBar({current_title, updateTitle, current_instructio toggleEditSI(true)} /> - + exportFormatDialogRef.current.showModal()} /> } toggleEditSI(false)}> @@ -78,6 +90,23 @@ export default function TitleBar({current_title, updateTitle, current_instructio
toggleEditSI(false)}>Cancel
+ evt.target.close()} + ref={exportFormatDialogRef} + > +
evt.stopPropagation()}> +
Please select a format to export
+
submitSaveHistory("JSON")}> + +
Export as JSON
+
+
submitSaveHistory("Human")}> + +
Export as Plain Text
+
+
+
) } \ No newline at end of file diff --git a/src/components/chat/index.jsx b/src/components/chat/index.jsx index 3d121ee..cf64802 100644 --- a/src/components/chat/index.jsx +++ b/src/components/chat/index.jsx @@ -6,6 +6,7 @@ import DeleteConfirm from "./DeleteConfirm"; import ChatPage from "./ChatPage"; import { getCompletionFunctions } from "../../utils/workers"; import { getPlatformSettings } from "../../utils/general_settings"; +import { exportChatHistory } from "../../utils/chat-history-handler"; export default function Chat() { @@ -151,6 +152,12 @@ export default function Chat() { }) } + async function saveHistory(format) { + if(!chat.uid) return; + const full_history = await idb.getAll('messages', {where: [{'history-uid': chat.uid}], select: ['role', 'content', 'createdAt']}) + await exportChatHistory(format, chat, full_history); + } + useEffect(()=>{ conv_to_delete && toggleConfirm(true); }, [conv_to_delete]) @@ -172,7 +179,7 @@ export default function Chat() { deleteHistory={requestDelete} platform={platform.current} /> Date: Mon, 21 Oct 2024 14:59:55 +1100 Subject: [PATCH 4/4] update style Signed-off-by: cbh778899 --- src/styles/chat.css | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/styles/chat.css b/src/styles/chat.css index 7b4ad5e..1efdc61 100644 --- a/src/styles/chat.css +++ b/src/styles/chat.css @@ -417,3 +417,43 @@ input[type="text"] { .button-container > .button-icon.highlight { color: dodgerblue; } + +dialog.export-format { + background-color: transparent; + border-radius: 10px; + padding: 0; + border: unset; +} + +dialog.export-format > .export-format-main { + background-color: white; + border-radius: 10px; + padding: 20px 30px; +} + +dialog.export-format > .export-format-main > .title { + font-size: 18px; + margin-bottom: 20px; +} + +dialog.export-format > .export-format-main > .export-btn { + display: flex; + align-items: center; + height: 40px; + border: 2px solid lightgray; + margin-bottom: 15px; + border-radius: 10px; + transition-duration: .3s; +} +dialog.export-format > .export-format-main > .export-btn:hover { + border-color: gray; +} + +dialog.export-format > .export-format-main > .export-btn > .icon { + margin-left: auto; + margin-right: 10px; +} + +dialog.export-format > .export-format-main > .export-btn > .text { + margin-right: auto; +} \ No newline at end of file