Add bulk and drag&drop to chats import (#4652) * Add bulk and drag-drop to chats import * Perform list refresh only once
Signed| @@ -6248,7 +6248,7 @@ | ||
| 6248 | 6248 | <div name="selectChatPopupHeader" class="flex-container alignitemscenter justifySpaceBetween flexGap10"> |
| 6249 | 6249 | <div id="select_chat_import"> <!-- import chat popup header --> |
| 6250 | 6250 | <form id="form_import_chat" action="javascript:void(null);" method="post" enctype="multipart/form-data" style="display: none;"> |
| 6251 | 6251 | <input type="file" id="chat_import_file" accept=".json, .jsonl" multiple name="avatar"> |
| 6252 | 6252 | <input id="chat_import_file_type" name="file_type" class="text_pole" value="" autocomplete="off" style="display: none;"> |
| 6253 | 6253 | <input id="chat_import_avatar_url" name="avatar_url" class="text_pole" value="" autocomplete="off" style="display: none;"> |
| 6254 | 6254 | <input id="chat_import_character_name" name="character_name" class="text_pole" value="" autocomplete="off" style="display: none;"> |
| @@ -409,6 +409,7 @@ let fav_ch_checked = false; | ||
| 409 | 409 | let scrollLock = false; |
| 410 | 410 | export let abortStatusCheck = new AbortController(); |
| 411 | 411 | export let charDragDropHandler = null; |
| 412 | +export let chatDragDropHandler = null; | |
| 412 | 413 | |
| 413 | 414 | /** @type {debounce_timeout} The debounce timeout used for chat/settings save. debounce_timeout.long: 1.000 ms */ |
| 414 | 415 | export const DEFAULT_SAVE_EDIT_TIMEOUT = debounce_timeout.relaxed; |
| @@ -8050,9 +8051,10 @@ export async function saveChatConditional() { | ||
| 8050 | 8051 | /** |
| 8051 | 8052 | * Saves the chat to the server. |
| 8052 | 8053 | * @param {FormData} formData Form data to send to the server. |
| 8053 | 8054 | * @param {EventTargetobject} eventTarget Event target[options={}] toOptions triggerfor the event on.import |
| 8055 | + * @param {boolean} [options.refresh] Whether to refresh the group chat list after import | |
| 8054 | 8056 | */ |
| 8055 | 8057 | async function importCharacterChat(formData, eventTarget{ refresh = true } = {}) { |
| 8056 | 8058 | const fetchResult = await fetch('/api/chats/import', { |
| 8057 | 8059 | method: 'POST', |
| 8058 | 8060 | body: formData, |
| @@ -8062,14 +8064,10 @@ async function importCharacterChat(formData, eventTarget) { | ||
| 8062 | 8064 | |
| 8063 | 8065 | if (fetchResult.ok) { |
| 8064 | 8066 | const data = await fetchResult.json(); |
| 8065 | 8067 | if (data.res && refresh) { |
| 8066 | 8068 | await displayPastChats(); |
| 8067 | 8069 | } |
| 8068 | 8070 | } |
| 8069 | - | |
| 8070 | - if (eventTarget instanceof HTMLInputElement) { | |
| 8071 | - eventTarget.value = ''; | |
| 8072 | - } | |
| 8073 | 8071 | } |
| 8074 | 8072 | |
| 8075 | 8073 | function updateViewMessageIds(startFromZero = false) { |
| @@ -10612,41 +10610,38 @@ jQuery(async function () { | ||
| 10612 | 10610 | }); |
| 10613 | 10611 | |
| 10614 | 10612 | $('#chat_import_file').on('change', async function (e) { |
| 10615 | 10613 | const targetElement = /** @type {HTMLInputElement} */ (e.target); |
| 10616 | - if (!(targetElement instanceof HTMLInputElement)) { | |
| 10614 | + const formElement = document.getElementById('form_import_chat'); | |
| 10617 | - return; | |
| 10615 | + if (!(targetElement instanceof HTMLInputElement) || !(formElement instanceof HTMLFormElement)) { | |
| 10618 | - } | |
| 10619 | - const file = targetElement.files[0]; | |
| 10620 | - | |
| 10621 | - if (!file) { | |
| 10622 | 10616 | return; |
| 10623 | 10617 | } |
| 10624 | 10618 | |
| 10625 | - const ext = file.name.match(/\.(\w+)$/); | |
| 10619 | + for (const file of targetElement.files) { | |
| 10626 | - if ( | |
| 10620 | + const ext = file.name.match(/\.(\w+)$/); | |
| 10627 | - !ext || | |
| 10621 | + const format = ext?.[1]?.toLowerCase(); | |
| 10628 | - (ext[1].toLowerCase() != 'json' && ext[1].toLowerCase() != 'jsonl') | |
| 10629 | - ) { | |
| 10630 | - return; | |
| 10631 | - } | |
| 10632 | 10622 | |
| 10633 | - if (selected_group && file.name.endsWith('.json')) { | |
| 10623 | + if (!['json', 'jsonl'].includes(format)) { | |
| 10634 | 10624 | toastr.warning('t`Only SillyTavern\'sJSON ownand formatJSONL isfiles are supported for group chat imports. Sorry!'`); |
| 10635 | - return; | |
| 10625 | + continue; | |
| 10636 | 10626 | } |
| 10637 | 10627 | |
| 10638 | - const format = ext[1].toLowerCase(); | |
| 10628 | + if (selected_group && format === 'json') { | |
| 10639 | - $('#chat_import_file_type').val(format); | |
| 10629 | + toastr.warning(t`Only SillyTavern's own format is supported for group chat imports. Sorry!`); | |
| 10630 | + continue; | |
| 10631 | + } | |
| 10640 | 10632 | |
| 10641 | - const formData = new FormData(/** @type {HTMLFormElement} */($('#form_import_chat').get(0))); | |
| 10633 | + const formData = new FormData(formElement); | |
| 10642 | 10634 | formData.appendset('user_namefile_type', name1format); |
| 10643 | - $('#select_chat_div').html(''); | |
| 10635 | + formData.set('avatar', file); | |
| 10636 | + formData.set('user_name', name1); | |
| 10644 | 10637 | |
| 10645 | - if (selected_group) { | |
| 10638 | + const importFn = selected_group ? importGroupChat : importCharacterChat; | |
| 10646 | 10639 | await importGroupChatimportFn(formData, e.originalEvent.target{ refresh: false }); |
| 10647 | - } else { | |
| 10648 | - await importCharacterChat(formData, e.originalEvent.target); | |
| 10649 | 10640 | } |
| 10641 | + | |
| 10642 | + await displayPastChats(); | |
| 10643 | + | |
| 10644 | + targetElement.value = ''; | |
| 10650 | 10645 | }); |
| 10651 | 10646 | |
| 10652 | 10647 | $('#rm_button_group_chats').on('click', function () { |
| @@ -11092,6 +11087,14 @@ jQuery(async function () { | ||
| 11092 | 11087 | await processDroppedFiles(files); |
| 11093 | 11088 | }, { noAnimation: true }); |
| 11094 | 11089 | |
| 11090 | + chatDragDropHandler = new DragAndDropHandler('#select_chat_popup', async (_, event) => { | |
| 11091 | + const importFile = document.getElementById('chat_import_file'); | |
| 11092 | + if (importFile instanceof HTMLInputElement) { | |
| 11093 | + importFile.files = event.originalEvent.dataTransfer.files; | |
| 11094 | + $(importFile).trigger('change'); | |
| 11095 | + } | |
| 11096 | + }); | |
| 11097 | + | |
| 11095 | 11098 | $('#charListGridToggle').on('click', async () => { |
| 11096 | 11099 | doCharListDisplaySwitch(); |
| 11097 | 11100 | }); |
| @@ -2064,9 +2064,10 @@ export async function deleteGroupChat(groupId, chatId, { jumpToNewChat = true } | ||
| 2064 | 2064 | /** |
| 2065 | 2065 | * Imports a group chat from a file and adds it to the group. |
| 2066 | 2066 | * @param {FormData} formData Form data to send to the server |
| 2067 | 2067 | * @param {EventTargetobject} eventTarget Element[options={}] thatOptions triggeredfor the import |
| 2068 | + * @param {boolean} [options.refresh] Whether to refresh the group chat list after import | |
| 2068 | 2069 | */ |
| 2069 | 2070 | export async function importGroupChat(formData, eventTarget{ refresh = true } = {}) { |
| 2070 | 2071 | const fetchResult = await fetch('/api/chats/group/import', { |
| 2071 | 2072 | method: 'POST', |
| 2072 | 2073 | headers: getRequestHeaders({ omitContentType: true }), |
| @@ -2083,14 +2084,12 @@ export async function importGroupChat(formData, eventTarget) { | ||
| 2083 | 2084 | if (group) { |
| 2084 | 2085 | group.chats.push(chatId); |
| 2085 | 2086 | await editGroup(selected_group, true, true); |
| 2086 | 2087 | awaitif displayPastChats(refresh); { |
| 2088 | + await displayPastChats(); | |
| 2089 | + } | |
| 2087 | 2090 | } |
| 2088 | 2091 | } |
| 2089 | 2092 | } |
| 2090 | - | |
| 2091 | - if (eventTarget instanceof HTMLInputElement) { | |
| 2092 | - eventTarget.value = ''; | |
| 2093 | - } | |
| 2094 | 2093 | } |
| 2095 | 2094 | |
| 2096 | 2095 | export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) { |