import { callPopup, chat_metadata, eventSource, event_types, generateQuietPrompt, getCurrentChatId, getRequestHeaders, getThumbnailUrl, saveSettingsDebounced } from '../script.js'; import { saveMetadataDebounced } from './extensions.js'; import { SlashCommand } from './slash-commands/SlashCommand.js'; import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { flashHighlight, stringFormat } from './utils.js'; const BG_METADATA_KEY = 'custom_background'; const LIST_METADATA_KEY = 'chat_backgrounds'; export let background_settings = { name: '__transparent.png', url: generateUrlParameter('__transparent.png', false), }; export function loadBackgroundSettings(settings) { let backgroundSettings = settings.background; if (!backgroundSettings || !backgroundSettings.name || !backgroundSettings.url) { backgroundSettings = background_settings; } setBackground(backgroundSettings.name, backgroundSettings.url); } /** * Sets the background for the current chat and adds it to the list of custom backgrounds. * @param {{url: string, path:string}} backgroundInfo */ function forceSetBackground(backgroundInfo) { saveBackgroundMetadata(backgroundInfo.url); setCustomBackground(); const list = chat_metadata[LIST_METADATA_KEY] || []; const bg = backgroundInfo.path; list.push(bg); chat_metadata[LIST_METADATA_KEY] = list; saveMetadataDebounced(); getChatBackgroundsList(); highlightNewBackground(bg); highlightLockedBackground(); } async function onChatChanged() { if (hasCustomBackground()) { setCustomBackground(); } else { unsetCustomBackground(); } getChatBackgroundsList(); highlightLockedBackground(); } function getChatBackgroundsList() { const list = chat_metadata[LIST_METADATA_KEY]; const listEmpty = !Array.isArray(list) || list.length === 0; $('#bg_custom_content').empty(); $('#bg_chat_hint').toggle(listEmpty); if (listEmpty) { return; } for (const bg of list) { const template = getBackgroundFromTemplate(bg, true); $('#bg_custom_content').append(template); } } function getBackgroundPath(fileUrl) { return `backgrounds/${fileUrl}`; } function highlightLockedBackground() { $('.bg_example').removeClass('locked'); const lockedBackground = chat_metadata[BG_METADATA_KEY]; if (!lockedBackground) { return; } $('.bg_example').each(function () { const url = $(this).data('url'); if (url === lockedBackground) { $(this).addClass('locked'); } }); } function onLockBackgroundClick(e) { e.stopPropagation(); const chatName = getCurrentChatId(); if (!chatName) { toastr.warning('Select a chat to lock the background for it'); return ''; } const relativeBgImage = getUrlParameter(this); saveBackgroundMetadata(relativeBgImage); setCustomBackground(); highlightLockedBackground(); return ''; } function onUnlockBackgroundClick(e) { e.stopPropagation(); removeBackgroundMetadata(); unsetCustomBackground(); highlightLockedBackground(); return ''; } function hasCustomBackground() { return chat_metadata[BG_METADATA_KEY]; } function saveBackgroundMetadata(file) { chat_metadata[BG_METADATA_KEY] = file; saveMetadataDebounced(); } function removeBackgroundMetadata() { delete chat_metadata[BG_METADATA_KEY]; saveMetadataDebounced(); } function setCustomBackground() { const file = chat_metadata[BG_METADATA_KEY]; // bg already set if (document.getElementById('bg_custom').style.backgroundImage == file) { return; } $('#bg_custom').css('background-image', file); } function unsetCustomBackground() { $('#bg_custom').css('background-image', 'none'); } function onSelectBackgroundClick() { const isCustom = $(this).attr('custom') === 'true'; const relativeBgImage = getUrlParameter(this); // if clicked on upload button if (!relativeBgImage) { return; } // Automatically lock the background if it's custom or other background is locked if (hasCustomBackground() || isCustom) { saveBackgroundMetadata(relativeBgImage); setCustomBackground(); highlightLockedBackground(); } highlightLockedBackground(); const customBg = window.getComputedStyle(document.getElementById('bg_custom')).backgroundImage; // Custom background is set. Do not override the layer below if (customBg !== 'none') { return; } const bgFile = $(this).attr('bgfile'); const backgroundUrl = getBackgroundPath(bgFile); // Fetching to browser memory to reduce flicker fetch(backgroundUrl).then(() => { setBackground(bgFile, relativeBgImage); }).catch(() => { console.log('Background could not be set: ' + backgroundUrl); }); } async function onCopyToSystemBackgroundClick(e) { e.stopPropagation(); const bgNames = await getNewBackgroundName(this); if (!bgNames) { return; } const bgFile = await fetch(bgNames.oldBg); if (!bgFile.ok) { toastr.warning('Failed to copy background'); return; } const blob = await bgFile.blob(); const file = new File([blob], bgNames.newBg); const formData = new FormData(); formData.set('avatar', file); uploadBackground(formData); const list = chat_metadata[LIST_METADATA_KEY] || []; const index = list.indexOf(bgNames.oldBg); list.splice(index, 1); saveMetadataDebounced(); getChatBackgroundsList(); } /** * Gets the new background name from the user. * @param {Element} referenceElement * @returns {Promise<{oldBg: string, newBg: string}>} * */ async function getNewBackgroundName(referenceElement) { const exampleBlock = $(referenceElement).closest('.bg_example'); const isCustom = exampleBlock.attr('custom') === 'true'; const oldBg = exampleBlock.attr('bgfile'); if (!oldBg) { console.debug('no bgfile'); return; } const fileExtension = oldBg.split('.').pop(); const fileNameBase = isCustom ? oldBg.split('/').pop() : oldBg; const oldBgExtensionless = fileNameBase.replace(`.${fileExtension}`, ''); const newBgExtensionless = await callPopup('

Enter new background name:

', 'input', oldBgExtensionless); if (!newBgExtensionless) { console.debug('no new_bg_extensionless'); return; } const newBg = `${newBgExtensionless}.${fileExtension}`; if (oldBgExtensionless === newBgExtensionless) { console.debug('new_bg === old_bg'); return; } return { oldBg, newBg }; } async function onRenameBackgroundClick(e) { e.stopPropagation(); const bgNames = await getNewBackgroundName(this); if (!bgNames) { return; } const data = { old_bg: bgNames.oldBg, new_bg: bgNames.newBg }; const response = await fetch('/api/backgrounds/rename', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify(data), cache: 'no-cache', }); if (response.ok) { await getBackgrounds(); highlightNewBackground(bgNames.newBg); } else { toastr.warning('Failed to rename background'); } } async function onDeleteBackgroundClick(e) { e.stopPropagation(); const bgToDelete = $(this).closest('.bg_example'); const url = bgToDelete.data('url'); const isCustom = bgToDelete.attr('custom') === 'true'; const confirm = await callPopup('

Delete the background?

', 'confirm'); const bg = bgToDelete.attr('bgfile'); if (confirm) { // If it's not custom, it's a built-in background. Delete it from the server if (!isCustom) { delBackground(bg); } else { const list = chat_metadata[LIST_METADATA_KEY] || []; const index = list.indexOf(bg); list.splice(index, 1); } const siblingSelector = '.bg_example:not(#form_bg_download)'; const nextBg = bgToDelete.next(siblingSelector); const prevBg = bgToDelete.prev(siblingSelector); const anyBg = $(siblingSelector); if (nextBg.length > 0) { nextBg.trigger('click'); } else if (prevBg.length > 0) { prevBg.trigger('click'); } else { $(anyBg[Math.floor(Math.random() * anyBg.length)]).trigger('click'); } bgToDelete.remove(); if (url === chat_metadata[BG_METADATA_KEY]) { removeBackgroundMetadata(); unsetCustomBackground(); highlightLockedBackground(); } if (isCustom) { getChatBackgroundsList(); saveMetadataDebounced(); } } } const autoBgPrompt = 'Pause your roleplay and choose a location ONLY from the provided list that is the most suitable for the current scene. Do not output any other text:\n{0}'; async function autoBackgroundCommand() { /** @type {HTMLElement[]} */ const bgTitles = Array.from(document.querySelectorAll('#bg_menu_content .BGSampleTitle')); const options = bgTitles.map(x => ({ element: x, text: x.innerText.trim() })).filter(x => x.text.length > 0); if (options.length == 0) { toastr.warning('No backgrounds to choose from. Please upload some images to the "backgrounds" folder.'); return ''; } const list = options.map(option => `- ${option.text}`).join('\n'); const prompt = stringFormat(autoBgPrompt, list); const reply = await generateQuietPrompt(prompt, false, false); const fuse = new Fuse(options, { keys: ['text'] }); const bestMatch = fuse.search(reply, { limit: 1 }); if (bestMatch.length == 0) { toastr.warning('No match found. Please try again.'); return ''; } console.debug('Automatically choosing background:', bestMatch); bestMatch[0].item.element.click(); return ''; } export async function getBackgrounds() { const response = await fetch('/api/backgrounds/all', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ '': '', }), }); if (response.ok) { const getData = await response.json(); //background = getData; //console.log(getData.length); $('#bg_menu_content').children('div').remove(); for (const bg of getData) { const template = getBackgroundFromTemplate(bg, false); $('#bg_menu_content').append(template); } } } /** * Gets the CSS URL of the background * @param {Element} block * @returns {string} URL of the background */ function getUrlParameter(block) { return $(block).closest('.bg_example').data('url'); } function generateUrlParameter(bg, isCustom) { return isCustom ? `url("${encodeURI(bg)}")` : `url("${getBackgroundPath(bg)}")`; } /** * Instantiates a background template * @param {string} bg Path to background * @param {boolean} isCustom Whether the background is custom * @returns {JQuery} Background template */ function getBackgroundFromTemplate(bg, isCustom) { const template = $('#background_template .bg_example').clone(); const thumbPath = isCustom ? bg : getThumbnailUrl('bg', bg); const url = generateUrlParameter(bg, isCustom); const title = isCustom ? bg.split('/').pop() : bg; const friendlyTitle = title.slice(0, title.lastIndexOf('.')); template.attr('title', title); template.attr('bgfile', bg); template.attr('custom', String(isCustom)); template.data('url', url); template.css('background-image', `url('${thumbPath}')`); template.find('.BGSampleTitle').text(friendlyTitle); return template; } async function setBackground(bg, url) { $('#bg1').css('background-image', url); background_settings.name = bg; background_settings.url = url; saveSettingsDebounced(); } async function delBackground(bg) { await fetch('/api/backgrounds/delete', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ bg: bg, }), }); } function onBackgroundUploadSelected() { const form = $('#form_bg_download').get(0); if (!(form instanceof HTMLFormElement)) { console.error('form_bg_download is not a form'); return; } const formData = new FormData(form); uploadBackground(formData); form.reset(); } /** * Uploads a background to the server * @param {FormData} formData */ function uploadBackground(formData) { jQuery.ajax({ type: 'POST', url: '/api/backgrounds/upload', data: formData, beforeSend: function () { }, cache: false, contentType: false, processData: false, success: async function (bg) { setBackground(bg, generateUrlParameter(bg, false)); await getBackgrounds(); highlightNewBackground(bg); }, error: function (jqXHR, exception) { console.log(exception); console.log(jqXHR); }, }); } /** * @param {string} bg */ function highlightNewBackground(bg) { const newBg = $(`.bg_example[bgfile="${bg}"]`); const scrollOffset = newBg.offset().top - newBg.parent().offset().top; $('#Backgrounds').scrollTop(scrollOffset); flashHighlight(newBg); } function onBackgroundFilterInput() { const filterValue = String($(this).val()).toLowerCase(); $('#bg_menu_content > div').each(function () { const $bgContent = $(this); if ($bgContent.attr('title').toLowerCase().includes(filterValue)) { $bgContent.show(); } else { $bgContent.hide(); } }); } export function initBackgrounds() { eventSource.on(event_types.CHAT_CHANGED, onChatChanged); eventSource.on(event_types.FORCE_SET_BACKGROUND, forceSetBackground); $(document).on('click', '.bg_example', onSelectBackgroundClick); $(document).on('click', '.bg_example_lock', onLockBackgroundClick); $(document).on('click', '.bg_example_unlock', onUnlockBackgroundClick); $(document).on('click', '.bg_example_edit', onRenameBackgroundClick); $(document).on('click', '.bg_example_cross', onDeleteBackgroundClick); $(document).on('click', '.bg_example_copy', onCopyToSystemBackgroundClick); $('#auto_background').on('click', autoBackgroundCommand); $('#add_bg_button').on('change', onBackgroundUploadSelected); $('#bg-filter').on('input', onBackgroundFilterInput); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'lockbg', callback: onLockBackgroundClick, aliases: ['bglock'], helpString: 'Locks a background for the currently selected chat', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'unlockbg', callback: onUnlockBackgroundClick, aliases: ['bgunlock'], helpString: 'Unlocks a background for the currently selected chat', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'autobg', callback: autoBackgroundCommand, aliases: ['bgauto'], helpString: 'Automatically changes the background based on the chat context using the AI request prompt', })); }