|
import { Fuse } from '../lib.js'; |
|
|
|
import { callPopup, chat_metadata, eventSource, event_types, generateQuietPrompt, getCurrentChatId, getRequestHeaders, getThumbnailUrl, saveSettingsDebounced } from '../script.js'; |
|
import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js'; |
|
import { SlashCommand } from './slash-commands/SlashCommand.js'; |
|
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
|
import { flashHighlight, stringFormat } from './utils.js'; |
|
import { t } from './i18n.js'; |
|
import { Popup } from './popup.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), |
|
fitting: 'classic', |
|
}; |
|
|
|
export function loadBackgroundSettings(settings) { |
|
let backgroundSettings = settings.background; |
|
if (!backgroundSettings || !backgroundSettings.name || !backgroundSettings.url) { |
|
backgroundSettings = background_settings; |
|
} |
|
if (!backgroundSettings.fitting) { |
|
backgroundSettings.fitting = 'classic'; |
|
} |
|
setBackground(backgroundSettings.name, backgroundSettings.url); |
|
setFittingClass(backgroundSettings.fitting); |
|
$('#background_fitting').val(backgroundSettings.fitting); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
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/${encodeURIComponent(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) ?? background_settings.url; |
|
|
|
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]; |
|
|
|
|
|
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 (!relativeBgImage) { |
|
return; |
|
} |
|
|
|
|
|
if (hasCustomBackground() || isCustom) { |
|
saveBackgroundMetadata(relativeBgImage); |
|
setCustomBackground(); |
|
highlightLockedBackground(); |
|
} |
|
highlightLockedBackground(); |
|
|
|
const customBg = window.getComputedStyle(document.getElementById('bg_custom')).backgroundImage; |
|
|
|
|
|
if (customBg !== 'none') { |
|
return; |
|
} |
|
|
|
const bgFile = $(this).attr('bgfile'); |
|
const backgroundUrl = getBackgroundPath(bgFile); |
|
|
|
|
|
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); |
|
|
|
await uploadBackground(formData); |
|
|
|
const list = chat_metadata[LIST_METADATA_KEY] || []; |
|
const index = list.indexOf(bgNames.oldBg); |
|
list.splice(index, 1); |
|
saveMetadataDebounced(); |
|
getChatBackgroundsList(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
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>' + t`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('/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 Popup.show.confirm(t`Delete the background?`, null); |
|
const bg = bgToDelete.attr('bgfile'); |
|
|
|
if (confirm) { |
|
|
|
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 = 'Ignore previous instructions 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 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) { |
|
for (const option of options) { |
|
if (String(reply).toLowerCase().includes(option.text.toLowerCase())) { |
|
console.debug('Fallback choosing background:', option); |
|
option.element.click(); |
|
return ''; |
|
} |
|
} |
|
|
|
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(); |
|
|
|
|
|
$('#bg_menu_content').children('div').remove(); |
|
for (const bg of getData) { |
|
const template = getBackgroundFromTemplate(bg, false); |
|
$('#bg_menu_content').append(template); |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function getUrlParameter(block) { |
|
return $(block).closest('.bg_example').data('url'); |
|
} |
|
|
|
function generateUrlParameter(bg, isCustom) { |
|
return isCustom ? `url("${encodeURI(bg)}")` : `url("${getBackgroundPath(bg)}")`; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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, |
|
}), |
|
}); |
|
} |
|
|
|
async 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); |
|
await convertFileIfVideo(formData); |
|
await uploadBackground(formData); |
|
form.reset(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
async function convertFileIfVideo(formData) { |
|
const file = formData.get('avatar'); |
|
if (!(file instanceof File)) { |
|
return; |
|
} |
|
if (!file.type.startsWith('video/')) { |
|
return; |
|
} |
|
if (typeof globalThis.convertVideoToAnimatedWebp !== 'function') { |
|
toastr.warning(t`Click here to install the Video Background Loader extension`, t`Video background uploads require a downloadable add-on`, { |
|
timeOut: 0, |
|
extendedTimeOut: 0, |
|
onclick: () => openThirdPartyExtensionMenu('https://github.com/SillyTavern/Extension-VideoBackgroundLoader'), |
|
}); |
|
return; |
|
} |
|
|
|
let toastMessage = jQuery(); |
|
try { |
|
toastMessage = toastr.info(t`Preparing video for upload. This may take several minutes.`, t`Please wait`, { timeOut: 0, extendedTimeOut: 0 }); |
|
const sourceBuffer = await file.arrayBuffer(); |
|
const convertedBuffer = await globalThis.convertVideoToAnimatedWebp({ buffer: new Uint8Array(sourceBuffer), name: file.name }); |
|
const convertedFileName = file.name.replace(/\.[^/.]+$/, '.webp'); |
|
const convertedFile = new File([convertedBuffer], convertedFileName, { type: 'image/webp' }); |
|
formData.set('avatar', convertedFile); |
|
toastMessage.remove(); |
|
} catch (error) { |
|
formData.delete('avatar'); |
|
toastMessage.remove(); |
|
console.error('Error converting video to animated webp:', error); |
|
toastr.error(t`Error converting video to animated webp`); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
async function uploadBackground(formData) { |
|
try { |
|
if (!formData.has('avatar')) { |
|
console.log('No file provided. Background upload cancelled.'); |
|
return; |
|
} |
|
|
|
const headers = getRequestHeaders(); |
|
delete headers['Content-Type']; |
|
|
|
const response = await fetch('/api/backgrounds/upload', { |
|
method: 'POST', |
|
headers: headers, |
|
body: formData, |
|
cache: 'no-cache', |
|
}); |
|
|
|
if (!response.ok) { |
|
throw new Error('Failed to upload background'); |
|
} |
|
|
|
const bg = await response.text(); |
|
setBackground(bg, generateUrlParameter(bg, false)); |
|
await getBackgrounds(); |
|
highlightNewBackground(bg); |
|
} catch (error) { |
|
console.error('Error uploading background:', error); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
function highlightNewBackground(bg) { |
|
const newBg = $(`.bg_example[bgfile="${bg}"]`); |
|
const scrollOffset = newBg.offset().top - newBg.parent().offset().top; |
|
$('#Backgrounds').scrollTop(scrollOffset); |
|
flashHighlight(newBg); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function setFittingClass(fitting) { |
|
const backgrounds = $('#bg1, #bg_custom'); |
|
for (const option of ['cover', 'contain', 'stretch', 'center']) { |
|
backgrounds.toggleClass(option, option === fitting); |
|
} |
|
background_settings.fitting = fitting; |
|
} |
|
|
|
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(new CustomEvent('click')), |
|
aliases: ['bglock'], |
|
helpString: 'Locks a background for the currently selected chat', |
|
})); |
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'unlockbg', |
|
callback: () => onUnlockBackgroundClick(new CustomEvent('click')), |
|
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', |
|
})); |
|
|
|
$('#background_fitting').on('input', function () { |
|
background_settings.fitting = String($(this).val()); |
|
setFittingClass(background_settings.fitting); |
|
saveSettingsDebounced(); |
|
}); |
|
} |
|
|