Add Kobold Lite chats import

79700fd983a3297407807848755e5a003886b165

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

3 files changed, +115 -60Showing whitespace changes
public/script.js+28 -22
@@ -7754,25 +7754,31 @@ export async function saveChatConditional() {
7754 }7754 }
7755}7755}
77567756
7757async 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 */
7762 beforeSend: function () {7762async 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', {
7766 processData: false,7766 method: 'POST',
7767 success: async function (data) {7767 body: formData,
7768 headers: headers,
7769 cache: 'no-cache',
7770 });
7771
7772 if (fetchResult.ok) {
7773 const data = await fetchResult.json();
7768 if (data.res) {7774 if (data.res) {
7769 await displayPastChats();7775 await displayPastChats();
7770 }7776 }
7771 },7777 }
7772 error: function () {7778
7773 $('#create_button').removeAttr('disabled');7779 if (eventTarget instanceof HTMLInputElement) {
7774 },7780 eventTarget.value = '';
7775 });7781 }
7776}7782}
77777783
7778function updateViewMessageIds(startFromZero = false) {7784function updateViewMessageIds(startFromZero = false) {
@@ -10829,13 +10835,13 @@ jQuery(async function () {
10829 });10835 });
1083010836
10831 $('#chat_import_file').on('change', async function (e) {10837 $('#chat_import_file').on('change', async function (e) {
10832 var file = e.target.files[0];10838 const file = e.target.files[0];
1083310839
10834 if (!file) {10840 if (!file) {
10835 return;10841 return;
10836 }10842 }
1083710843
10838 var ext = file.name.match(/\.(\w+)$/);10844 const ext = file.name.match(/\.(\w+)$/);
10839 if (10845 if (
10840 !ext ||10846 !ext ||
10841 (ext[1].toLowerCase() != 'json' && ext[1].toLowerCase() != 'jsonl')10847 (ext[1].toLowerCase() != 'json' && ext[1].toLowerCase() != 'jsonl')
@@ -10848,17 +10854,17 @@ jQuery(async function () {
10848 return;10854 return;
10849 }10855 }
1085010856
10851 var format = ext[1].toLowerCase();10857 const format = ext[1].toLowerCase();
10852 $('#chat_import_file_type').val(format);10858 $('#chat_import_file_type').val(format);
1085310859
10854 var formData = new FormData($('#form_import_chat').get(0));10860 const formData = new FormData($('#form_import_chat').get(0));
10855 formData.append('user_name', name1);10861 formData.append('user_name', name1);
10856 $('#select_chat_div').html('');10862 $('#select_chat_div').html('');
1085710863
10858 if (selected_group) {10864 if (selected_group) {
10859 await importGroupChat(formData);10865 await importGroupChat(formData, e.originalEvent.target);
10860 } else {10866 } else {
10861 await importCharacterChat(formData);10867 await importCharacterChat(formData, e.originalEvent.target);
10862 }10868 }
10863 });10869 });
1086410870
public/scripts/group-chats.js+22 -16
@@ -1863,17 +1863,23 @@ export async function deleteGroupChat(groupId, chatId) {
1863 }1863 }
1864}1864}
18651865
1866export 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 */
1871 beforeSend: function () {1871export 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', {
1875 processData: false,1875 method: 'POST',
1876 success: async function (data) {1876 headers: headers,
1877 body: formData,
1878 cache: 'no-cache',
1879 });
1880
1881 if (fetchResult.ok) {
1882 const data = await fetchResult.json();
1877 if (data.res) {1883 if (data.res) {
1878 const chatId = data.res;1884 const chatId = data.res;
1879 const group = groups.find(x => x.id == selected_group);1885 const group = groups.find(x => x.id == selected_group);
@@ -1884,11 +1890,11 @@ export async function importGroupChat(formData) {
1884 await displayPastChats();1890 await displayPastChats();
1885 }1891 }
1886 }1892 }
1887 },1893 }
1888 error: function () {1894
1889 $('#create_button').removeAttr('disabled');1895 if (eventTarget instanceof HTMLInputElement) {
1890 },1896 eventTarget.value = '';
1891 });1897 }
1892}1898}
18931899
1894export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) {1900export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) {
src/endpoints/chats.js+65 -22
@@ -191,6 +191,44 @@ function importCAIChat(userName, characterName, jsonData) {
191}191}
192192
193/**193/**
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 */
200function 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/**
194 * Flattens `msg` and `swipes` data from Chub Chat format.232 * Flattens `msg` and `swipes` data from Chub Chat format.
195 * Only changes enough to make it compatible with the standard chat serialization format.233 * Only changes enough to make it compatible with the standard chat serialization format.
196 * @param {string} userName User name234 * @param {string} userName User name
@@ -413,7 +451,7 @@ router.post('/import', urlencodedParser, function (request, response) {
413 const format = request.body.file_type;451 const format = request.body.file_type;
414 const avatarUrl = (request.body.avatar_url).replace('.png', '');452 const avatarUrl = (request.body.avatar_url).replace('.png', '');
415 const characterName = request.body.character_name;453 const characterName = request.body.character_name;
416 const userName = request.body.user_name || 'You';454 const userName = request.body.user_name || 'User';
417455
418 if (!request.file) {456 if (!request.file) {
419 return response.sendStatus(400);457 return response.sendStatus(400);
@@ -426,33 +464,38 @@ router.post('/import', urlencodedParser, function (request, response) {
426 if (format === 'json') {464 if (format === 'json') {
427 fs.unlinkSync(pathToUpload);465 fs.unlinkSync(pathToUpload);
428 const jsonData = JSON.parse(data);466 const jsonData = JSON.parse(data);
429 if (jsonData.histories !== undefined) {467
430 // CAI Tools format468 /** @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
474 importFunc = importCAIChat;
475 } else if (Array.isArray(jsonData.data_visible)) { // oobabooga's format
476 importFunc = importOobaChat;
477 } else if (Array.isArray(jsonData.messages)) { // Agnai's format
478 importFunc = importAgnaiChat;
479 } else { // Unknown format
480 console.log('Incorrect chat format .json');
481 return response.send({ error: true });
436 }482 }
437 return response.send({ res: true });483
438 } else if (Array.isArray(jsonData.data_visible)) {484 const handleChat = (chat) => {
439 // oobabooga's format
440 const chat = importOobaChat(userName, characterName, jsonData);
441 const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`;
442 const filePath = path.join(request.user.directories.chats, avatarUrl, fileName);
443 writeFileAtomicSync(filePath, chat, 'utf8');
444 return response.send({ res: true });
445 } else if (Array.isArray(jsonData.messages)) {
446 // Agnai format
447 const chat = importAgnaiChat(userName, characterName, jsonData);
448 const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`;485 const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`;
449 const filePath = path.join(request.user.directories.chats, avatarUrl, fileName);486 const filePath = path.join(request.user.directories.chats, avatarUrl, fileName);
450 writeFileAtomicSync(filePath, chat, 'utf8');487 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);
452 } else {494 } else {
453 console.log('Incorrect chat format .json');495 handleChat(chat);
454 return response.send({ error: true });
455 }496 }
497
498 return response.send({ res: true });
456 }499 }
457500
458 if (format === 'jsonl') {501 if (format === 'jsonl') {