| import { t } from './i18n.js'; |
| import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js'; |
| import { getFileExtension, sortMoments, timestampToMoment } from './utils.js'; |
| import { displayPastChats, getRequestHeaders, importCharacterChat } from '/script.js'; |
| import { importGroupChat } from './group-chats.js'; |
|
|
| class BackupsBrowser { |
| |
| #buttonElement; |
| |
| #buttonChevronIcon; |
| |
| #backupsListElement; |
| |
| #loadingAbortController; |
| |
| #isOpen = false; |
|
|
| get isOpen() { |
| return this.#isOpen; |
| } |
|
|
| |
| |
| |
| |
| |
| async viewBackup(name) { |
| const response = await fetch('/api/backups/chat/download', { |
| method: 'POST', |
| headers: getRequestHeaders(), |
| body: JSON.stringify({ name: name }), |
| }); |
|
|
| if (!response.ok) { |
| toastr.error(t`Failed to download backup, try again later.`); |
| console.error('Failed to download chat backup:', response.statusText); |
| return; |
| } |
|
|
| try { |
| |
| const parsedLines = []; |
| const fileText = await response.text(); |
| fileText.split('\n').forEach(line => { |
| try { |
| |
| const lineData = JSON.parse(line); |
| if (lineData?.mes) { |
| parsedLines.push(lineData); |
| } |
| } catch (error) { |
| console.error('Failed to parse chat backup line:', error); |
| } |
| }); |
| const textArea = document.createElement('textarea'); |
| textArea.classList.add('text_pole', 'monospace', 'textarea_compact', 'margin0', 'height100p'); |
| textArea.readOnly = true; |
| textArea.value = parsedLines.map(l => `${l.name} [${timestampToMoment(l.send_date).format('lll')}]\n${l.mes}`).join('\n\n\n'); |
| await callGenericPopup(textArea, POPUP_TYPE.TEXT, '', { allowVerticalScrolling: true, large: true, wide: true }); |
| } catch (error) { |
| console.error('Failed to parse chat backup content:', error); |
| toastr.error(t`Failed to parse backup content.`); |
| return; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| async restoreBackup(name) { |
| const response = await fetch('/api/backups/chat/download', { |
| method: 'POST', |
| headers: getRequestHeaders(), |
| body: JSON.stringify({ name: name }), |
| }); |
|
|
| if (!response.ok) { |
| toastr.error(t`Failed to download backup, try again later.`); |
| console.error('Failed to download chat backup:', response.statusText); |
| return; |
| } |
|
|
| const blob = await response.blob(); |
| const file = new File([blob], name, { type: 'application/octet-stream' }); |
|
|
| const extension = getFileExtension(file); |
|
|
| if (extension !== 'jsonl') { |
| toastr.warning(t`Only .jsonl files are supported for chat imports.`); |
| return; |
| } |
|
|
| const context = SillyTavern.getContext(); |
|
|
| const formData = new FormData(); |
| formData.set('file_type', extension); |
| formData.set('avatar', file); |
| formData.set('avatar_url', context.characters[context.characterId]?.avatar || ''); |
| formData.set('user_name', context.name1); |
| formData.set('character_name', context.name2); |
|
|
| const importFn = context.groupId ? importGroupChat : importCharacterChat; |
| const result = await importFn(formData, { refresh: false }); |
|
|
| if (result.length === 0) { |
| toastr.error(t`Failed to import chat backup, try again later.`); |
| return; |
| } |
|
|
| toastr.success(`Chat imported: ${result.join(', ')}`); |
| await displayPastChats(result); |
| } |
|
|
| |
| |
| |
| |
| |
| async deleteBackup(name) { |
| const confirm = await Popup.show.confirm(t`Are you sure?`); |
| if (!confirm) { |
| return false; |
| } |
|
|
| const response = await fetch('/api/backups/chat/delete', { |
| method: 'POST', |
| headers: getRequestHeaders(), |
| body: JSON.stringify({ name: name }), |
| }); |
|
|
| if (!response.ok) { |
| toastr.error(t`Failed to delete backup, try again later.`); |
| console.error('Failed to delete chat backup:', response.statusText); |
| return false; |
| } |
|
|
| toastr.success(t`Backup deleted successfully.`); |
| return true; |
| } |
|
|
| |
| |
| |
| |
| |
| async loadBackupsIntoList(signal) { |
| if (!this.#backupsListElement) { |
| return; |
| } |
|
|
| this.#backupsListElement.innerHTML = ''; |
|
|
| const response = await fetch('/api/backups/chat/get', { |
| method: 'POST', |
| headers: getRequestHeaders(), |
| signal, |
| }); |
|
|
| if (!response.ok) { |
| console.error('Failed to load chat backups list:', response.statusText); |
| return; |
| } |
|
|
| |
| const backupsList = await response.json(); |
|
|
| for (const backup of backupsList.sort((a, b) => sortMoments(timestampToMoment(a.last_mes), timestampToMoment(b.last_mes)))) { |
| const listItem = document.createElement('div'); |
| listItem.classList.add('chatBackupsListItem'); |
|
|
| const backupName = document.createElement('div'); |
| backupName.textContent = backup.file_name; |
| backupName.classList.add('chatBackupsListItemName'); |
|
|
| const backupInfo = document.createElement('div'); |
| backupInfo.classList.add('chatBackupsListItemInfo'); |
| backupInfo.textContent = `${timestampToMoment(backup.last_mes).format('lll')} (${backup.file_size}, ${backup.chat_items} 💬)`; |
|
|
| const actionsList = document.createElement('div'); |
| actionsList.classList.add('chatBackupsListItemActions'); |
|
|
| const viewButton = document.createElement('div'); |
| viewButton.classList.add('right_menu_button', 'fa-solid', 'fa-eye'); |
| viewButton.title = t`View backup`; |
| viewButton.addEventListener('click', async () => { |
| await this.viewBackup(backup.file_name); |
| }); |
|
|
| const restoreButton = document.createElement('div'); |
| restoreButton.classList.add('right_menu_button', 'fa-solid', 'fa-rotate-left'); |
| restoreButton.title = t`Restore backup`; |
| restoreButton.addEventListener('click', async () => { |
| await this.restoreBackup(backup.file_name); |
| }); |
|
|
| const deleteButton = document.createElement('div'); |
| deleteButton.classList.add('right_menu_button', 'fa-solid', 'fa-trash'); |
| deleteButton.title = t`Delete backup`; |
| deleteButton.addEventListener('click',async () => { |
| const isDeleted = await this.deleteBackup(backup.file_name); |
| if (isDeleted) { |
| listItem.remove(); |
| } |
| }); |
|
|
| actionsList.appendChild(viewButton); |
| actionsList.appendChild(restoreButton); |
| actionsList.appendChild(deleteButton); |
|
|
| listItem.appendChild(backupName); |
| listItem.appendChild(backupInfo); |
| listItem.appendChild(actionsList); |
|
|
| this.#backupsListElement.appendChild(listItem); |
| } |
| } |
|
|
| closeBackups() { |
| if (!this.#isOpen) { |
| return; |
| } |
|
|
| this.#isOpen = false; |
| if (this.#buttonChevronIcon) { |
| this.#buttonChevronIcon.classList.remove('fa-chevron-up'); |
| this.#buttonChevronIcon.classList.add('fa-chevron-down'); |
| } |
| if (this.#backupsListElement) { |
| this.#backupsListElement.classList.remove('open'); |
| this.#backupsListElement.innerHTML = ''; |
| } |
| if (this.#loadingAbortController) { |
| this.#loadingAbortController.abort(); |
| this.#loadingAbortController = null; |
| } |
| } |
|
|
| openBackups() { |
| if (this.#isOpen) { |
| return; |
| } |
|
|
| this.#isOpen = true; |
| if (this.#buttonChevronIcon) { |
| this.#buttonChevronIcon.classList.remove('fa-chevron-down'); |
| this.#buttonChevronIcon.classList.add('fa-chevron-up'); |
| } |
| if (this.#backupsListElement) { |
| this.#backupsListElement.classList.add('open'); |
| } |
| if (this.#loadingAbortController) { |
| this.#loadingAbortController.abort(); |
| this.#loadingAbortController = null; |
| } |
|
|
| this.#loadingAbortController = new AbortController(); |
| this.loadBackupsIntoList(this.#loadingAbortController.signal); |
| } |
|
|
| renderButton() { |
| if (this.#buttonElement) { |
| return; |
| } |
|
|
| const sibling = document.getElementById('select_chat_search'); |
| if (!sibling) { |
| console.error('Could not find sibling element for BackupsBrowser button'); |
| return; |
| } |
|
|
| const button = document.createElement('button'); |
| button.classList.add('menu_button', 'menu_button_icon'); |
|
|
| const buttonIcon = document.createElement('i'); |
| buttonIcon.classList.add('fa-solid', 'fa-box-open'); |
|
|
| const buttonText = document.createElement('span'); |
| buttonText.textContent = t`Backups`; |
| buttonText.title = t`Browse chat backups`; |
|
|
| const chevronIcon = document.createElement('i'); |
| chevronIcon.classList.add('fa-solid', 'fa-chevron-down', 'fa-sm'); |
|
|
| button.appendChild(buttonIcon); |
| button.appendChild(buttonText); |
| button.appendChild(chevronIcon); |
|
|
| button.addEventListener('click', () => { |
| if (this.#isOpen) { |
| this.closeBackups(); |
| } else { |
| this.openBackups(); |
| } |
| }); |
|
|
| sibling.parentNode.insertBefore(button, sibling); |
|
|
| this.#buttonElement = button; |
| this.#buttonChevronIcon = chevronIcon; |
| } |
|
|
| renderBackupsList() { |
| if (this.#backupsListElement) { |
| return; |
| } |
|
|
| const sibling = document.getElementById('select_chat_div'); |
| if (!sibling) { |
| console.error('Could not find sibling element for BackupsBrowser list'); |
| return; |
| } |
|
|
| const list = document.createElement('div'); |
| list.classList.add('chatBackupsList'); |
|
|
| sibling.parentNode.insertBefore(list, sibling); |
| this.#backupsListElement = list; |
| } |
| } |
|
|
| const backupsBrowser = new BackupsBrowser(); |
|
|
| export function addChatBackupsBrowser() { |
| backupsBrowser.renderButton(); |
| backupsBrowser.renderBackupsList(); |
|
|
| |
| if (backupsBrowser.isOpen) { |
| backupsBrowser.closeBackups(); |
| backupsBrowser.openBackups(); |
| } |
| } |
|
|