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 | <div name="selectChatPopupHeader" class="flex-container alignitemscenter justifySpaceBetween flexGap10"> | 6248 | <div name="selectChatPopupHeader" class="flex-container alignitemscenter justifySpaceBetween flexGap10"> |
| 6249 | <div id="select_chat_import"> <!-- import chat popup header --> | 6249 | <div id="select_chat_import"> <!-- import chat popup header --> |
| 6250 | <form id="form_import_chat" action="javascript:void(null);" method="post" enctype="multipart/form-data" style="display: none;"> | 6250 | <form id="form_import_chat" action="javascript:void(null);" method="post" enctype="multipart/form-data" style="display: none;"> |
| 6251 | <input type="file" id="chat_import_file" accept=".json, .jsonl" name="avatar"> | 6251 | <input type="file" id="chat_import_file" accept=".json, .jsonl" multiple name="avatar"> |
| 6252 | <input id="chat_import_file_type" name="file_type" class="text_pole" value="" autocomplete="off" style="display: none;"> | 6252 | <input id="chat_import_file_type" name="file_type" class="text_pole" value="" autocomplete="off" style="display: none;"> |
| 6253 | <input id="chat_import_avatar_url" name="avatar_url" class="text_pole" value="" autocomplete="off" style="display: none;"> | 6253 | <input id="chat_import_avatar_url" name="avatar_url" class="text_pole" value="" autocomplete="off" style="display: none;"> |
| 6254 | <input id="chat_import_character_name" name="character_name" class="text_pole" value="" autocomplete="off" style="display: none;"> | 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 | let scrollLock = false; | 409 | let scrollLock = false; |
| 410 | export let abortStatusCheck = new AbortController(); | 410 | export let abortStatusCheck = new AbortController(); |
| 411 | export let charDragDropHandler = null; | 411 | export let charDragDropHandler = null; |
| 412 | export let chatDragDropHandler = null; | ||
| 412 | 413 | ||
| 413 | /** @type {debounce_timeout} The debounce timeout used for chat/settings save. debounce_timeout.long: 1.000 ms */ | 414 | /** @type {debounce_timeout} The debounce timeout used for chat/settings save. debounce_timeout.long: 1.000 ms */ |
| 414 | export const DEFAULT_SAVE_EDIT_TIMEOUT = debounce_timeout.relaxed; | 415 | export const DEFAULT_SAVE_EDIT_TIMEOUT = debounce_timeout.relaxed; |
| @@ -8050,9 +8051,10 @@ export async function saveChatConditional() { | |||
| 8050 | /** | 8051 | /** |
| 8051 | * Saves the chat to the server. | 8052 | * Saves the chat to the server. |
| 8052 | * @param {FormData} formData Form data to send to the server. | 8053 | * @param {FormData} formData Form data to send to the server. |
| 8053 | * @param {EventTarget} eventTarget Event target to trigger the event on. | 8054 | * @param {object} [options={}] Options for the import |
| 8055 | * @param {boolean} [options.refresh] Whether to refresh the group chat list after import | ||
| 8054 | */ | 8056 | */ |
| 8055 | async function importCharacterChat(formData, eventTarget) { | 8057 | async function importCharacterChat(formData, { refresh = true } = {}) { |
| 8056 | const fetchResult = await fetch('/api/chats/import', { | 8058 | const fetchResult = await fetch('/api/chats/import', { |
| 8057 | method: 'POST', | 8059 | method: 'POST', |
| 8058 | body: formData, | 8060 | body: formData, |
| @@ -8062,14 +8064,10 @@ async function importCharacterChat(formData, eventTarget) { | |||
| 8062 | 8064 | ||
| 8063 | if (fetchResult.ok) { | 8065 | if (fetchResult.ok) { |
| 8064 | const data = await fetchResult.json(); | 8066 | const data = await fetchResult.json(); |
| 8065 | if (data.res) { | 8067 | if (data.res && refresh) { |
| 8066 | await displayPastChats(); | 8068 | await displayPastChats(); |
| 8067 | } | 8069 | } |
| 8068 | } | 8070 | } |
| 8069 | |||
| 8070 | if (eventTarget instanceof HTMLInputElement) { | ||
| 8071 | eventTarget.value = ''; | ||
| 8072 | } | ||
| 8073 | } | 8071 | } |
| 8074 | 8072 | ||
| 8075 | function updateViewMessageIds(startFromZero = false) { | 8073 | function updateViewMessageIds(startFromZero = false) { |
| @@ -10612,41 +10610,38 @@ jQuery(async function () { | |||
| 10612 | }); | 10610 | }); |
| 10613 | 10611 | ||
| 10614 | $('#chat_import_file').on('change', async function (e) { | 10612 | $('#chat_import_file').on('change', async function (e) { |
| 10615 | const targetElement = /** @type {HTMLInputElement} */ (e.target); | 10613 | const targetElement = 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 | return; | 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 | toastr.warning('Only SillyTavern\'s own format is supported for group chat imports. Sorry!'); | 10624 | toastr.warning(t`Only JSON and JSONL files are supported for chat imports.`); |
| 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 | formData.append('user_name', name1); | 10634 | formData.set('file_type', format); |
| 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 | await importGroupChat(formData, e.originalEvent.target); | 10639 | await importFn(formData, { 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 | $('#rm_button_group_chats').on('click', function () { | 10647 | $('#rm_button_group_chats').on('click', function () { |
| @@ -11092,6 +11087,14 @@ jQuery(async function () { | |||
| 11092 | await processDroppedFiles(files); | 11087 | await processDroppedFiles(files); |
| 11093 | }, { noAnimation: true }); | 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 | $('#charListGridToggle').on('click', async () => { | 11098 | $('#charListGridToggle').on('click', async () => { |
| 11096 | doCharListDisplaySwitch(); | 11099 | doCharListDisplaySwitch(); |
| 11097 | }); | 11100 | }); |
| @@ -2064,9 +2064,10 @@ export async function deleteGroupChat(groupId, chatId, { jumpToNewChat = true } | |||
| 2064 | /** | 2064 | /** |
| 2065 | * Imports a group chat from a file and adds it to the group. | 2065 | * Imports a group chat from a file and adds it to the group. |
| 2066 | * @param {FormData} formData Form data to send to the server | 2066 | * @param {FormData} formData Form data to send to the server |
| 2067 | * @param {EventTarget} eventTarget Element that triggered the import | 2067 | * @param {object} [options={}] Options for the import |
| 2068 | * @param {boolean} [options.refresh] Whether to refresh the group chat list after import | ||
| 2068 | */ | 2069 | */ |
| 2069 | export async function importGroupChat(formData, eventTarget) { | 2070 | export async function importGroupChat(formData, { refresh = true } = {}) { |
| 2070 | const fetchResult = await fetch('/api/chats/group/import', { | 2071 | const fetchResult = await fetch('/api/chats/group/import', { |
| 2071 | method: 'POST', | 2072 | method: 'POST', |
| 2072 | headers: getRequestHeaders({ omitContentType: true }), | 2073 | headers: getRequestHeaders({ omitContentType: true }), |
| @@ -2083,14 +2084,12 @@ export async function importGroupChat(formData, eventTarget) { | |||
| 2083 | if (group) { | 2084 | if (group) { |
| 2084 | group.chats.push(chatId); | 2085 | group.chats.push(chatId); |
| 2085 | await editGroup(selected_group, true, true); | 2086 | await editGroup(selected_group, true, true); |
| 2086 | await displayPastChats(); | 2087 | if (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 | export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) { | 2095 | export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) { |