Add bulk and drag&drop to chats import (#4652) * Add bulk and drag-drop to chats import * Perform list refresh only once

3234045d64ffbd09f474cdc968f7f835f5b41bb7

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

Signed
3 files changed, +44 -42Ignore whitespace
public/index.html+1 -1
@@ -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;">
public/script.js+37 -34
@@ -409,6 +409,7 @@ let fav_ch_checked = false;
409let scrollLock = false;409let scrollLock = false;
410export let abortStatusCheck = new AbortController();410export let abortStatusCheck = new AbortController();
411export let charDragDropHandler = null;411export let charDragDropHandler = null;
412export let chatDragDropHandler = null;
412413
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 */
414export const DEFAULT_SAVE_EDIT_TIMEOUT = debounce_timeout.relaxed;415export 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 */
8055async function importCharacterChat(formData, eventTarget) {8057async 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) {
80628064
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}
80748072
8075function updateViewMessageIds(startFromZero = false) {8073function updateViewMessageIds(startFromZero = false) {
@@ -10612,41 +10610,38 @@ jQuery(async function () {
10612 });10610 });
1061310611
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 }
1062410618
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 }
1063210622
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 }
1063710627
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 }
1064010632
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);
1064410637
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 });
1065110646
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 });
1109411089
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 });
public/scripts/group-chats.js+6 -7
@@ -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 server2066 * @param {FormData} formData Form data to send to the server
2067 * @param {EventTarget} eventTarget Element that triggered the import2067 * @param {object} [options={}] Options for the import
2068 * @param {boolean} [options.refresh] Whether to refresh the group chat list after import
2068 */2069 */
2069export async function importGroupChat(formData, eventTarget) {2070export 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}
20952094
2096export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) {2095export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) {