Add Kobold Lite chats import
| @@ -7754,25 +7754,31 @@ export async function saveChatConditional() { | ||
| 7754 | 7754 | } |
| 7755 | 7755 | } |
| 7756 | 7756 | |
| 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 | + */ | |
| 7762 | 7762 | 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', { | |
| 7766 | 7766 | 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 | - }, | |
| 7775 | 7770 | }); |
| 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 | + } | |
| 7776 | 7782 | } |
| 7777 | 7783 | |
| 7778 | 7784 | function updateViewMessageIds(startFromZero = false) { |
| @@ -10829,13 +10835,13 @@ jQuery(async function () { | ||
| 10829 | 10835 | }); |
| 10830 | 10836 | |
| 10831 | 10837 | $('#chat_import_file').on('change', async function (e) { |
| 10832 | 10838 | varconst file = e.target.files[0]; |
| 10833 | 10839 | |
| 10834 | 10840 | if (!file) { |
| 10835 | 10841 | return; |
| 10836 | 10842 | } |
| 10837 | 10843 | |
| 10838 | 10844 | varconst ext = file.name.match(/\.(\w+)$/); |
| 10839 | 10845 | if ( |
| 10840 | 10846 | !ext || |
| 10841 | 10847 | (ext[1].toLowerCase() != 'json' && ext[1].toLowerCase() != 'jsonl') |
| @@ -10848,17 +10854,17 @@ jQuery(async function () { | ||
| 10848 | 10854 | return; |
| 10849 | 10855 | } |
| 10850 | 10856 | |
| 10851 | 10857 | varconst format = ext[1].toLowerCase(); |
| 10852 | 10858 | $('#chat_import_file_type').val(format); |
| 10853 | 10859 | |
| 10854 | 10860 | varconst formData = new FormData($('#form_import_chat').get(0)); |
| 10855 | 10861 | formData.append('user_name', name1); |
| 10856 | 10862 | $('#select_chat_div').html(''); |
| 10857 | 10863 | |
| 10858 | 10864 | if (selected_group) { |
| 10859 | 10865 | await importGroupChat(formData, e.originalEvent.target); |
| 10860 | 10866 | } else { |
| 10861 | 10867 | await importCharacterChat(formData, e.originalEvent.target); |
| 10862 | 10868 | } |
| 10863 | 10869 | }); |
| 10864 | 10870 | |
| @@ -1863,32 +1863,38 @@ export async function deleteGroupChat(groupId, chatId) { | ||
| 1863 | 1863 | } |
| 1864 | 1864 | } |
| 1865 | 1865 | |
| 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 | + */ | |
| 1871 | 1871 | 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', { | |
| 1875 | 1875 | 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 | - }, | |
| 1891 | 1879 | }); |
| 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 | + } | |
| 1892 | 1898 | } |
| 1893 | 1899 | |
| 1894 | 1900 | export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) { |
| @@ -191,6 +191,44 @@ function importCAIChat(userName, characterName, jsonData) { | ||
| 191 | 191 | } |
| 192 | 192 | |
| 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 | + */ | |
| 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 | +/** | |
| 194 | 232 | * Flattens `msg` and `swipes` data from Chub Chat format. |
| 195 | 233 | * Only changes enough to make it compatible with the standard chat serialization format. |
| 196 | 234 | * @param {string} userName User name |
| @@ -413,7 +451,7 @@ router.post('/import', urlencodedParser, function (request, response) { | ||
| 413 | 451 | const format = request.body.file_type; |
| 414 | 452 | const avatarUrl = (request.body.avatar_url).replace('.png', ''); |
| 415 | 453 | const characterName = request.body.character_name; |
| 416 | 454 | const userName = request.body.user_name || 'YouUser'; |
| 417 | 455 | |
| 418 | 456 | if (!request.file) { |
| 419 | 457 | return response.sendStatus(400); |
| @@ -426,33 +464,38 @@ router.post('/import', urlencodedParser, function (request, response) { | ||
| 426 | 464 | if (format === 'json') { |
| 427 | 465 | fs.unlinkSync(pathToUpload); |
| 428 | 466 | 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); | |
| 448 | 485 | const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`; |
| 449 | 486 | const filePath = path.join(request.user.directories.chats, avatarUrl, fileName); |
| 450 | 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 | 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 | } |
| 457 | 500 | |
| 458 | 501 | if (format === 'jsonl') { |