Add Kobold Lite chats import

79700fd983a3297407807848755e5a003886b165

Cohee <18619528+Cohee1207@users.noreply.github.com>

3 files changed, +127 -72Ignore whitespace
public/script.js+30 -24
@@ -7754,25 +7754,31 @@ export async function saveChatConditional() {
77547754 }
77557755}
77567756
7757-async function importCharacterChat(formData) {
7757+/**
7758- await jQuery.ajax({
7758+ * Saves the chat to the server.
7759- type: 'POST',
7759+ * @param {FormData} formData Form data to send to the server.
7760- url: '/api/chats/import',
7760+ * @param {EventTarget} eventTarget Event target to trigger the event on.
7761- data: formData,
7761+ */
77627762 beforeSend:async function importCharacterChat(formData, eventTarget) {
7763- },
7763+ const headers = getRequestHeaders();
7764- cache: false,
7764+ delete headers['Content-Type'];
7765- contentType: false,
7765+ const fetchResult = await fetch('/api/chats/import', {
77667766 processDatamethod: false'POST',
7767- success: async function (data) {
7767+ body: formData,
7768- if (data.res) {
7768+ headers: headers,
7769- await displayPastChats();
7769+ cache: 'no-cache',
7770- }
7771- },
7772- error: function () {
7773- $('#create_button').removeAttr('disabled');
7774- },
77757770 });
7771+
7772+ if (fetchResult.ok) {
7773+ const data = await fetchResult.json();
7774+ if (data.res) {
7775+ await displayPastChats();
7776+ }
7777+ }
7778+
7779+ if (eventTarget instanceof HTMLInputElement) {
7780+ eventTarget.value = '';
7781+ }
77767782}
77777783
77787784function updateViewMessageIds(startFromZero = false) {
@@ -10829,13 +10835,13 @@ jQuery(async function () {
1082910835 });
1083010836
1083110837 $('#chat_import_file').on('change', async function (e) {
1083210838 varconst file = e.target.files[0];
1083310839
1083410840 if (!file) {
1083510841 return;
1083610842 }
1083710843
1083810844 varconst ext = file.name.match(/\.(\w+)$/);
1083910845 if (
1084010846 !ext ||
1084110847 (ext[1].toLowerCase() != 'json' && ext[1].toLowerCase() != 'jsonl')
@@ -10848,17 +10854,17 @@ jQuery(async function () {
1084810854 return;
1084910855 }
1085010856
1085110857 varconst format = ext[1].toLowerCase();
1085210858 $('#chat_import_file_type').val(format);
1085310859
1085410860 varconst formData = new FormData($('#form_import_chat').get(0));
1085510861 formData.append('user_name', name1);
1085610862 $('#select_chat_div').html('');
1085710863
1085810864 if (selected_group) {
1085910865 await importGroupChat(formData, e.originalEvent.target);
1086010866 } else {
1086110867 await importCharacterChat(formData, e.originalEvent.target);
1086210868 }
1086310869 });
1086410870
public/scripts/group-chats.js+31 -25
@@ -1863,32 +1863,38 @@ export async function deleteGroupChat(groupId, chatId) {
18631863 }
18641864}
18651865
1866-export async function importGroupChat(formData) {
1866+/**
1867- await jQuery.ajax({
1867+ * Imports a group chat from a file and adds it to the group.
1868- type: 'POST',
1868+ * @param {FormData} formData Form data to send to the server
1869- url: '/api/chats/group/import',
1869+ * @param {EventTarget} eventTarget Element that triggered the import
1870- data: formData,
1870+ */
18711871 beforeSend:export async function importGroupChat(formData, eventTarget) {
1872- },
1872+ const headers = getRequestHeaders();
1873- cache: false,
1873+ delete headers['Content-Type'];
1874- contentType: false,
1874+ const fetchResult = await fetch('/api/chats/group/import', {
18751875 processDatamethod: false'POST',
1876- success: async function (data) {
1876+ headers: headers,
1877- if (data.res) {
1877+ body: formData,
1878- const chatId = data.res;
1878+ cache: 'no-cache',
1879- const group = groups.find(x => x.id == selected_group);
1880-
1881- if (group) {
1882- group.chats.push(chatId);
1883- await editGroup(selected_group, true, true);
1884- await displayPastChats();
1885- }
1886- }
1887- },
1888- error: function () {
1889- $('#create_button').removeAttr('disabled');
1890- },
18911879 });
1880+
1881+ if (fetchResult.ok) {
1882+ const data = await fetchResult.json();
1883+ if (data.res) {
1884+ const chatId = data.res;
1885+ const group = groups.find(x => x.id == selected_group);
1886+
1887+ if (group) {
1888+ group.chats.push(chatId);
1889+ await editGroup(selected_group, true, true);
1890+ await displayPastChats();
1891+ }
1892+ }
1893+ }
1894+
1895+ if (eventTarget instanceof HTMLInputElement) {
1896+ eventTarget.value = '';
1897+ }
18921898}
18931899
18941900export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) {
src/endpoints/chats.js+66 -23
@@ -191,6 +191,44 @@ function importCAIChat(userName, characterName, jsonData) {
191191}
192192
193193/**
194+ * Imports a chat from Kobold Lite format.
195+ * @param {string} _userName User name
196+ * @param {string} _characterName Character name
197+ * @param {object} data JSON data
198+ * @returns {string} Chat data
199+ */
200+function importKoboldLiteChat(_userName, _characterName, data) {
201+ const inputToken = '{{[INPUT]}}';
202+ const outputToken = '{{[OUTPUT]}}';
203+
204+ /** @type {function(string): object} */
205+ function processKoboldMessage(msg) {
206+ const isUser = msg.includes(inputToken) || msg.includes(outputToken);
207+ return {
208+ name: isUser ? header.user_name : header.character_name,
209+ is_user: isUser,
210+ mes: msg.replace(inputToken, '').replace(outputToken, '').trim(),
211+ send_date: Date.now(),
212+ };
213+ }
214+
215+ // Create the header
216+ const header = {
217+ user_name: data.savedsettings.chatname,
218+ character_name: data.savedsettings.chatopponent,
219+ };
220+ // Format messages
221+ const formattedMessages = data.actions.map(processKoboldMessage);
222+ // Add prompt if available
223+ if (data.prompt) {
224+ formattedMessages.unshift(processKoboldMessage(data.prompt));
225+ }
226+ // Combine header and messages
227+ const chatData = [header, ...formattedMessages];
228+ return chatData.map(obj => JSON.stringify(obj)).join('\n');
229+}
230+
231+/**
194232 * Flattens `msg` and `swipes` data from Chub Chat format.
195233 * Only changes enough to make it compatible with the standard chat serialization format.
196234 * @param {string} userName User name
@@ -413,7 +451,7 @@ router.post('/import', urlencodedParser, function (request, response) {
413451 const format = request.body.file_type;
414452 const avatarUrl = (request.body.avatar_url).replace('.png', '');
415453 const characterName = request.body.character_name;
416454 const userName = request.body.user_name || 'YouUser';
417455
418456 if (!request.file) {
419457 return response.sendStatus(400);
@@ -426,33 +464,38 @@ router.post('/import', urlencodedParser, function (request, response) {
426464 if (format === 'json') {
427465 fs.unlinkSync(pathToUpload);
428466 const jsonData = JSON.parse(data);
429- if (jsonData.histories !== undefined) {
467+
430- // CAI Tools format
468+ /** @type {function(string, string, object): string|string[]} */
431- const chats = importCAIChat(userName, characterName, jsonData);
469+ let importFunc;
432- for (const chat of chats) {
470+
433- const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`;
471+ if (jsonData.savedsettings !== undefined) { // Kobold Lite format
434- const filePath = path.join(request.user.directories.chats, avatarUrl, fileName);
472+ importFunc = importKoboldLiteChat;
435- writeFileAtomicSync(filePath, chat, 'utf8');
473+ } else if (jsonData.histories !== undefined) { // CAI Tools format
436- }
474+ importFunc = importCAIChat;
437- return response.send({ res: true });
475+ } else if (Array.isArray(jsonData.data_visible)) { // oobabooga's format
438- } else if (Array.isArray(jsonData.data_visible)) {
476+ importFunc = importOobaChat;
439- // oobabooga's format
477+ } else if (Array.isArray(jsonData.messages)) { // Agnai's format
440- const chat = importOobaChat(userName, characterName, jsonData);
478+ importFunc = importAgnaiChat;
441- const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`;
479+ } else { // Unknown format
442- const filePath = path.join(request.user.directories.chats, avatarUrl, fileName);
480+ console.log('Incorrect chat format .json');
443- writeFileAtomicSync(filePath, chat, 'utf8');
481+ return response.send({ error: true });
444- return response.send({ res: true });
482+ }
445- } else if (Array.isArray(jsonData.messages)) {
483+
446- // Agnai format
484+ const handleChat = (chat) => {
447- const chat = importAgnaiChat(userName, characterName, jsonData);
448485 const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`;
449486 const filePath = path.join(request.user.directories.chats, avatarUrl, fileName);
450487 writeFileAtomicSync(filePath, chat, 'utf8');
451- return response.send({ res: true });
488+ };
489+
490+ const chat = importFunc(userName, characterName, jsonData);
491+
492+ if (Array.isArray(chat)) {
493+ chat.forEach(handleChat);
452494 } else {
453- console.log('Incorrect chat format .json');
495+ handleChat(chat);
454- return response.send({ error: true });
455496 }
497+
498+ return response.send({ res: true });
456499 }
457500
458501 if (format === 'jsonl') {