mirror of
				https://github.com/SillyTavern/SillyTavern.git
				synced 2025-06-05 21:59:27 +02:00 
			
		
		
		
	* set isForced to true on input
* make floating auto-complete follow horizontal scrolling
* add callable closure vars
* changes to /let and /var for callable closures
* fix error message
* fix scope for closure arguments
* if should return the pipe result from closures
* use /run to call closures and no arguments on immediate closures
* throw exception from QRs window-function if no match
* when to show autocomplete vs info only
* autocomplete positioning
* autocomplete styling
* add theming to autocomplete (theme, dark, light)
* improve autocomplete show/hide logic and editor selection
* use blur tint color instead of chat tint color and use blur setting
* cleanup and docs
* use scope macros for QR args
* add enter to select autocomplete
* fix no executor found
* cleanup and comment
* fix alias list in help string
* fallback to empty string piped value if null or undefined
* fix typo
* blur textarea on ctrl+enter execute (and refocus after)
* stop executeSlashCommand if parser throws
* move /let and /var callbacks into functions
* switch textarea to monospace when value starts with slash
* add double pipe a pipe breaker
* fix /? slash
* remove some logging
* add "/:name" as shorthand for "/run name" after all
* move shit around
* fix error message
* use testRunShorthandEnd
* use parseQuotedValue and parseValue to determine name for "/:"
QR labels and set names can include spaces
* add some adjustments to make autocomplete work properly
some hint in there about "/:" would still be nice
* add autocomplete style  selector
* only strip quotes from subcommand if they are at both ends
* fix JSDoc
* escaping
* allow open quotes on dry run
* throwing shit at the wall for /: autocomplete
* escapes only for symbols
* clean up autocomplete
* improve performance
* fix scope macros
* remove unescaping of pipes
* fix macros in scope copy
* fix "/? slash"
* don't run parser for getNameAt if text has not changed
* fix options filter
* re-enable blur listener
* restore selection on non-replace select
* fix for escaping first character of value
* add support for {{pipe}} and {{var::}} closures
* add index support to var macro
* add scoped var macro to macro help
* more escape fixes
* reduce autocomplete render debounce
* cleanup
* restore old escape handling and parser flag for strict escaping
* fix "no match" autocomplete message
* add dummy commands for comments and parser flag
* fix type annotations
* somewhat safer macro replacements
* fix autocomplete select on blank / "no match"
* fix cutting off handled part in substitution
* add parser flag REPLACE_GETVAR
Replaces all {{getvar::}} and {{getglobalvar::}} macros with {{var::}}.
Inserts a series of command executors before the command with the macros that:
- save {{pipe}} to a var
- call /getvar or /getglobalvar to get the variable used in the macro
- call /let to save the retrieved variable
- return the saved {{pipe}} value
This helps to avoid double-substitutions when the var values contain text that could be interpreted as macros.
* remove old parser
* fix send on enter when no match
* deal with pipes in quoted values (loose escaping)
* add default parser flags to user settings
* allow quoted values in unnamed argument
* set parser flag without explicit state to "on"
* add click hint on parser error toast
* dirty more detailed cmd defs
* remove name from unnamed arg
* move autocomplete into class and floating with details
* replace jQuery's trigger('input') on #send_textarea with native events because jQuery does not dispatch the native event
* fix ctrl+space
* fix arrow navigation
* add comments
* fix pointer block
* add static fromProps
* fix up dummy commands
* migrate all commands to addCommandObject
* remove commented comment command
* fix alias in details
* add range as argument type
* switch to addCommandObject
* switch to addCommandObject
* fix height
* fix floating details position on left
* re-enable blur event
* use auto width for full details on floating autocomplete
* auto-size floating full details
* fix typo
* re-enable blur listener
* don't prevent enter when selected item is fully typed out
* add autocomplete details tooltips
* add language to slash command examples
* move makeItem into option and command and fix click select
* use autocomplete parts in /? slash
* fix alias formatting
* add language to slash command examples
* fix details position on initial input history
* small screen styles
* replace registerSlashCommand with detailed declarations
* put name on first line
* add missing returns
* fix missing comma
* fix alias display in autocomplete list
* remove args from help string
* move parser settings to its own section
* jsdoc
* hljs stscript lang
* add hljs to autocomplete help examples
* add missing import
* apply autocomplete colors to stscript codeblocks (hljs)
* add fromProps
* cache autocomplete elements
* towards generic autocomplete
* remove unused imports
* fix blanks
* add return types
* re-enable blur
* fix blank check
* Caption messages by id
* add aborting command execution
* fix return type
* fix chat input font reset
* add slash command progress indicator
* add missing return
* mark registerSlashCommand deprecated
* why??
* separate abort logic for commands
* remove parsing of quoted values from unnamed arg
* add adjustable autocomplete width
* revert stop button pulse
* add progress and pause/abort to QR editor
* add resize event on autocomplete width change
* add key= argument to all get vars
* refactoring
* introduce NamedArgumentAsignment
* add TODOs
* refactoring
* record start and end of named arg assignment
* refactoring
* prevent duplicate calls to show
* refactoring
* remove macro ac
* add secondary autocomplete and enum descriptions
* add syntax highlighting to QR editor
* add enum descriptions to /while
* add /let key=... to scope variable names
* add unnamed argument assignment class and unnamed argument splitting
* fix QR editor style
* remove dash before autocomplete help text
* add autocomplete for unnamed enums
* fix remaining dom after holding backslash
* fix for unnamed enums
* fix autocomplete for /parser-flag
* add parser-flag enum help
* fix type annotations
* fix autocomplete result for /:
* add colored autocomplete type icons
* collapse second line autocomplete help if empty
* mark optional named args in autocomplete
* fix when what
* remove duplicate debug buttons
* dispatch input on autocomplete select
* prevent grow from editor syntax layer
* add auto-adjust qr editor caret color
* remove text-shadow from autocomplete
* join value strings in /let and /var
* add /abort syntax highlight
* fix attempting secondary result when there is none
* rename settings headers and split autocomplete / stscript
* add parser flag tooltips
* add tooltips to chat width stops
* fix typo
* return clone of help item
* fix enum string
* don't make optional notice for autocomplete arguments smaller
* avoid scrollbar in chat input
* add rudimentary macro autocomplete
* strip macro from helptext
* finally remove closure delimiters around root
* cleanup
* fix index stuff for removed closure delimiters
* fix type hint
* add child commands to progress indicator
* include sub-separator in macro autocomplete
* remove all mentions of interruptsGeneration and purge
* remove unused imports
* fix syntax highlight with newline at end of input
* cleanup select pointer events
* coalesce onProgress call
* add regex to STscript syntax highlighting
* fix closure end
* fix autocomplete type icon alignment
* adjustments for small screens
* fix removing wrong element
* add missing "at=" arg to /sys, /comment, /sendas
* add font scale setting for autocomplete
* add target=_blank for parser flag links
* fix for searching enums
* remove REGEXP_MODE from hljs
just causes trouble
* fix autocomplete in closures
* fix typo
* fix type hint
* Get rid of scroll bar on load
* Add type hint for /send name argument. Fix 'at' types
* Add 'negative' arg hint to /sd command
* reenable blur event
* Allow /summarize to process any text
* Compact layout of script toggles
* Expand CSS by default
* fix double ranger indicator and adjust to narrow container
* make custom css input fill available vertical space
* reduce scroll lag
* use default cursor on scrollbar
* Clean-up module loading in index.html
* fix tab indent with hljs
---------
Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
		
	
		
			
				
	
	
		
			501 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			501 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
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();
 | 
						|
}
 | 
						|
 | 
						|
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();
 | 
						|
    }
 | 
						|
    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('<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('/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('<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() {
 | 
						|
    /** @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('/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<HTMLElement>} 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',
 | 
						|
    }));
 | 
						|
 | 
						|
}
 |