diff --git a/public/script.js b/public/script.js index 6f34c2440..5205a27cd 100644 --- a/public/script.js +++ b/public/script.js @@ -7754,25 +7754,31 @@ export async function saveChatConditional() { } } -async function importCharacterChat(formData) { - await jQuery.ajax({ - type: 'POST', - url: '/api/chats/import', - data: formData, - beforeSend: function () { - }, - cache: false, - contentType: false, - processData: false, - success: async function (data) { - if (data.res) { - await displayPastChats(); - } - }, - error: function () { - $('#create_button').removeAttr('disabled'); - }, +/** + * Saves the chat to the server. + * @param {FormData} formData Form data to send to the server. + * @param {EventTarget} eventTarget Event target to trigger the event on. + */ +async function importCharacterChat(formData, eventTarget) { + const headers = getRequestHeaders(); + delete headers['Content-Type']; + const fetchResult = await fetch('/api/chats/import', { + method: 'POST', + body: formData, + headers: headers, + cache: 'no-cache', }); + + if (fetchResult.ok) { + const data = await fetchResult.json(); + if (data.res) { + await displayPastChats(); + } + } + + if (eventTarget instanceof HTMLInputElement) { + eventTarget.value = ''; + } } function updateViewMessageIds(startFromZero = false) { @@ -10829,13 +10835,13 @@ jQuery(async function () { }); $('#chat_import_file').on('change', async function (e) { - var file = e.target.files[0]; + const file = e.target.files[0]; if (!file) { return; } - var ext = file.name.match(/\.(\w+)$/); + const ext = file.name.match(/\.(\w+)$/); if ( !ext || (ext[1].toLowerCase() != 'json' && ext[1].toLowerCase() != 'jsonl') @@ -10848,17 +10854,17 @@ jQuery(async function () { return; } - var format = ext[1].toLowerCase(); + const format = ext[1].toLowerCase(); $('#chat_import_file_type').val(format); - var formData = new FormData($('#form_import_chat').get(0)); + const formData = new FormData($('#form_import_chat').get(0)); formData.append('user_name', name1); $('#select_chat_div').html(''); if (selected_group) { - await importGroupChat(formData); + await importGroupChat(formData, e.originalEvent.target); } else { - await importCharacterChat(formData); + await importCharacterChat(formData, e.originalEvent.target); } }); diff --git a/public/scripts/group-chats.js b/public/scripts/group-chats.js index a74ef0d72..0195b1d8d 100644 --- a/public/scripts/group-chats.js +++ b/public/scripts/group-chats.js @@ -1863,32 +1863,38 @@ export async function deleteGroupChat(groupId, chatId) { } } -export async function importGroupChat(formData) { - await jQuery.ajax({ - type: 'POST', - url: '/api/chats/group/import', - data: formData, - beforeSend: function () { - }, - cache: false, - contentType: false, - processData: false, - success: async function (data) { - if (data.res) { - const chatId = data.res; - const group = groups.find(x => x.id == selected_group); - - if (group) { - group.chats.push(chatId); - await editGroup(selected_group, true, true); - await displayPastChats(); - } - } - }, - error: function () { - $('#create_button').removeAttr('disabled'); - }, +/** + * Imports a group chat from a file and adds it to the group. + * @param {FormData} formData Form data to send to the server + * @param {EventTarget} eventTarget Element that triggered the import + */ +export async function importGroupChat(formData, eventTarget) { + const headers = getRequestHeaders(); + delete headers['Content-Type']; + const fetchResult = await fetch('/api/chats/group/import', { + method: 'POST', + headers: headers, + body: formData, + cache: 'no-cache', }); + + if (fetchResult.ok) { + const data = await fetchResult.json(); + if (data.res) { + const chatId = data.res; + const group = groups.find(x => x.id == selected_group); + + if (group) { + group.chats.push(chatId); + await editGroup(selected_group, true, true); + await displayPastChats(); + } + } + } + + if (eventTarget instanceof HTMLInputElement) { + eventTarget.value = ''; + } } export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) { diff --git a/src/endpoints/chats.js b/src/endpoints/chats.js index aec0ba6e4..dd12adf5b 100644 --- a/src/endpoints/chats.js +++ b/src/endpoints/chats.js @@ -190,6 +190,44 @@ function importCAIChat(userName, characterName, jsonData) { return newChats; } +/** + * Imports a chat from Kobold Lite format. + * @param {string} _userName User name + * @param {string} _characterName Character name + * @param {object} data JSON data + * @returns {string} Chat data + */ +function importKoboldLiteChat(_userName, _characterName, data) { + const inputToken = '{{[INPUT]}}'; + const outputToken = '{{[OUTPUT]}}'; + + /** @type {function(string): object} */ + function processKoboldMessage(msg) { + const isUser = msg.includes(inputToken) || msg.includes(outputToken); + return { + name: isUser ? header.user_name : header.character_name, + is_user: isUser, + mes: msg.replace(inputToken, '').replace(outputToken, '').trim(), + send_date: Date.now(), + }; + } + + // Create the header + const header = { + user_name: data.savedsettings.chatname, + character_name: data.savedsettings.chatopponent, + }; + // Format messages + const formattedMessages = data.actions.map(processKoboldMessage); + // Add prompt if available + if (data.prompt) { + formattedMessages.unshift(processKoboldMessage(data.prompt)); + } + // Combine header and messages + const chatData = [header, ...formattedMessages]; + return chatData.map(obj => JSON.stringify(obj)).join('\n'); +} + /** * Flattens `msg` and `swipes` data from Chub Chat format. * Only changes enough to make it compatible with the standard chat serialization format. @@ -413,7 +451,7 @@ router.post('/import', urlencodedParser, function (request, response) { const format = request.body.file_type; const avatarUrl = (request.body.avatar_url).replace('.png', ''); const characterName = request.body.character_name; - const userName = request.body.user_name || 'You'; + const userName = request.body.user_name || 'User'; if (!request.file) { return response.sendStatus(400); @@ -426,33 +464,38 @@ router.post('/import', urlencodedParser, function (request, response) { if (format === 'json') { fs.unlinkSync(pathToUpload); const jsonData = JSON.parse(data); - if (jsonData.histories !== undefined) { - // CAI Tools format - const chats = importCAIChat(userName, characterName, jsonData); - for (const chat of chats) { - const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`; - const filePath = path.join(request.user.directories.chats, avatarUrl, fileName); - writeFileAtomicSync(filePath, chat, 'utf8'); - } - return response.send({ res: true }); - } else if (Array.isArray(jsonData.data_visible)) { - // oobabooga's format - const chat = importOobaChat(userName, characterName, jsonData); - const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`; - const filePath = path.join(request.user.directories.chats, avatarUrl, fileName); - writeFileAtomicSync(filePath, chat, 'utf8'); - return response.send({ res: true }); - } else if (Array.isArray(jsonData.messages)) { - // Agnai format - const chat = importAgnaiChat(userName, characterName, jsonData); - const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`; - const filePath = path.join(request.user.directories.chats, avatarUrl, fileName); - writeFileAtomicSync(filePath, chat, 'utf8'); - return response.send({ res: true }); - } else { + + /** @type {function(string, string, object): string|string[]} */ + let importFunc; + + if (jsonData.savedsettings !== undefined) { // Kobold Lite format + importFunc = importKoboldLiteChat; + } else if (jsonData.histories !== undefined) { // CAI Tools format + importFunc = importCAIChat; + } else if (Array.isArray(jsonData.data_visible)) { // oobabooga's format + importFunc = importOobaChat; + } else if (Array.isArray(jsonData.messages)) { // Agnai's format + importFunc = importAgnaiChat; + } else { // Unknown format console.log('Incorrect chat format .json'); return response.send({ error: true }); } + + const handleChat = (chat) => { + const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`; + const filePath = path.join(request.user.directories.chats, avatarUrl, fileName); + writeFileAtomicSync(filePath, chat, 'utf8'); + }; + + const chat = importFunc(userName, characterName, jsonData); + + if (Array.isArray(chat)) { + chat.forEach(handleChat); + } else { + handleChat(chat); + } + + return response.send({ res: true }); } if (format === 'jsonl') {