SillyTavern/public/scripts/backgrounds.js

487 lines
14 KiB
JavaScript
Raw Normal View History

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('<h3>Enter new background name:</h3>', '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("<h3>Delete the background?</h3>", '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() {
const options = Array.from(document.querySelectorAll('.BGSampleTitle')).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<HTMLElement>} 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);
}