import { callPopup, chat_metadata, eventSource, event_types, generateQuietPrompt, getCurrentChatId, getRequestHeaders, getThumbnailUrl } from "../script.js"; import { saveMetadataDebounced } from "./extensions.js"; import { registerSlashCommand } from "./slash-commands.js"; import { stringFormat } from "./utils.js"; const BG_METADATA_KEY = 'custom_background'; const LIST_METADATA_KEY = 'chat_backgrounds'; /** * 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(); } function onUnlockBackgroundClick(e) { e.stopPropagation(); removeBackgroundMetadata(); unsetCustomBackground(); highlightLockedBackground(); } 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(); } else { 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(() => { $("#bg1").css("background-image", relativeBgImage); setBackground(bgFile); }).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('/renamebackground', { 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(); } export async function getBackgrounds() { const response = await fetch("/getbackgrounds", { method: "POST", headers: getRequestHeaders(), body: JSON.stringify({ "": "", }), }); if (response.ok === true) { 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 URL of the background * @param {Element} block * @returns {string} URL of the background */ function getUrlParameter(block) { return $(block).closest(".bg_example").data("url"); } /** * 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 = isCustom ? `url("${encodeURI(bg)}")` : `url("${getBackgroundPath(bg)}")`; 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) { jQuery.ajax({ type: "POST", // url: "/setbackground", // data: JSON.stringify({ bg: bg, }), beforeSend: function () { }, cache: false, dataType: "json", contentType: "application/json", //processData: false, success: function (html) { }, error: function (jqXHR, exception) { console.log(exception); console.log(jqXHR); }, }); } async function delBackground(bg) { const response = await fetch("/delbackground", { 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: "/downloadbackground", data: formData, beforeSend: function () { }, cache: false, contentType: false, processData: false, success: async function (bg) { setBackground(bg); $("#bg1").css("background-image", `url("${getBackgroundPath(bg)}"`); 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); newBg.addClass('flash animated'); setTimeout(() => newBg.removeClass('flash animated'), 2000); } 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); registerSlashCommand('lockbg', onLockBackgroundClick, ['bglock'], "– locks a background for the currently selected chat", true, true); registerSlashCommand('unlockbg', onUnlockBackgroundClick, ['bgunlock'], '– unlocks a background for the currently selected chat', true, true); registerSlashCommand('autobg', autoBackgroundCommand, ['bgauto'], '– automatically changes the background based on the chat context using the AI request prompt', true, true); }