Merge branch 'SillyTavern:staging' into staging
Signed| @@ -167,6 +167,7 @@ import { | ||
| 167 | 167 | flashHighlight, |
| 168 | 168 | isTrueBoolean, |
| 169 | 169 | toggleDrawer, |
| 170 | + isElementInViewport, | |
| 170 | 171 | } from './scripts/utils.js'; |
| 171 | 172 | import { debounce_timeout } from './scripts/constants.js'; |
| 172 | 173 | |
| @@ -1827,10 +1828,10 @@ export async function replaceCurrentChat() { | ||
| 1827 | 1828 | } |
| 1828 | 1829 | } |
| 1829 | 1830 | |
| 1830 | 1831 | export function showMoreMessages(messagesToLoad = null) { |
| 1831 | 1832 | const firstDisplayedMesId = $('#chat').children('.mes').first().attr('mesid'); |
| 1832 | 1833 | let messageId = Number(firstDisplayedMesId); |
| 1833 | 1834 | let count = messagesToLoad || power_user.chat_truncation || Number.MAX_SAFE_INTEGER; |
| 1834 | 1835 | |
| 1835 | 1836 | // If there are no messages displayed, or the message somehow has no mesid, we default to one higher than last message id, |
| 1836 | 1837 | // so the first "new" message being shown will be the last available message |
| @@ -1840,6 +1841,7 @@ export function showMoreMessages() { | ||
| 1840 | 1841 | |
| 1841 | 1842 | console.debug('Inserting messages before', messageId, 'count', count, 'chat length', chat.length); |
| 1842 | 1843 | const prevHeight = $('#chat').prop('scrollHeight'); |
| 1844 | + const isButtonInView = isElementInViewport($('#show_more_messages')[0]); | |
| 1843 | 1845 | |
| 1844 | 1846 | while (messageId > 0 && count > 0) { |
| 1845 | 1847 | let newMessageId = messageId - 1; |
| @@ -1852,8 +1854,10 @@ export function showMoreMessages() { | ||
| 1852 | 1854 | $('#show_more_messages').remove(); |
| 1853 | 1855 | } |
| 1854 | 1856 | |
| 1855 | - const newHeight = $('#chat').prop('scrollHeight'); | |
| 1857 | + if (isButtonInView) { | |
| 1856 | 1858 | const newHeight = $('#chat').scrollTopprop(newHeight - prevHeight'scrollHeight'); |
| 1859 | + $('#chat').scrollTop(newHeight - prevHeight); | |
| 1860 | + } | |
| 1857 | 1861 | } |
| 1858 | 1862 | |
| 1859 | 1863 | export async function printMessages() { |
| @@ -5425,20 +5429,24 @@ async function promptItemize(itemizedPrompts, requestedMesId) { | ||
| 5425 | 5429 | await popup.show(); |
| 5426 | 5430 | } |
| 5427 | 5431 | |
| 5428 | 5432 | function setInContextMessages(lastmsgmsgInContextCount, type) { |
| 5429 | 5433 | $('#chat .mes').removeClass('lastInContext'); |
| 5430 | 5434 | |
| 5431 | 5435 | if (type === 'swipe' || type === 'regenerate' || type === 'continue') { |
| 5432 | 5436 | lastmsgmsgInContextCount++; |
| 5433 | 5437 | } |
| 5434 | 5438 | |
| 5435 | 5439 | const lastMessageBlock = $('#chat .mes:not([is_system="true"])').eq(-lastmsgmsgInContextCount); |
| 5436 | 5440 | lastMessageBlock.addClass('lastInContext'); |
| 5437 | 5441 | |
| 5438 | 5442 | if (lastMessageBlock.length === 0) { |
| 5439 | 5443 | const firstMessageId = getFirstDisplayedMessageId(); |
| 5440 | 5444 | $(`#chat .mes[mesid="${firstMessageId}"`).addClass('lastInContext'); |
| 5441 | 5445 | } |
| 5446 | + | |
| 5447 | + // Update last id to chat. No metadata save on purpose, gets hopefully saved via another call | |
| 5448 | + const lastMessageId = Math.max(0, chat.length - msgInContextCount); | |
| 5449 | + chat_metadata['lastInContextMessageId'] = lastMessageId; | |
| 5442 | 5450 | } |
| 5443 | 5451 | |
| 5444 | 5452 | /** |
| @@ -7301,7 +7309,7 @@ export function select_rm_info(type, charId, previousCharId = null) { | ||
| 7301 | 7309 | // Set a timeout so multiple flashes don't overlap |
| 7302 | 7310 | clearTimeout(importFlashTimeout); |
| 7303 | 7311 | importFlashTimeout = setTimeout(function () { |
| 7304 | 7312 | if (type === 'char_import' || type === 'char_create' || type === 'char_import_no_toast') { |
| 7305 | 7313 | // Find the page at which the character is located |
| 7306 | 7314 | const avatarFileName = charId; |
| 7307 | 7315 | const charData = getEntitiesList({ doFilter: true }); |
| @@ -8853,24 +8861,61 @@ export async function processDroppedFiles(files, data = new Map()) { | ||
| 8853 | 8861 | 'charx', |
| 8854 | 8862 | ]; |
| 8855 | 8863 | |
| 8864 | + const avatarFileNames = []; | |
| 8856 | 8865 | for (const file of files) { |
| 8857 | 8866 | const extension = file.name.split('.').pop().toLowerCase(); |
| 8858 | 8867 | if (allowedMimeTypes.some(x => file.type.startsWith(x)) || allowedExtensions.includes(extension)) { |
| 8859 | 8868 | const preservedName = data instanceof Map && data.get(file); |
| 8860 | 8869 | const avatarFileName = await importCharacter(file, { preserveFileName: preservedName }); |
| 8870 | + if (avatarFileName !== undefined) { | |
| 8871 | + avatarFileNames.push(avatarFileName); | |
| 8872 | + } | |
| 8861 | 8873 | } else { |
| 8862 | 8874 | toastr.warning(t`Unsupported file type: ` + file.name); |
| 8863 | 8875 | } |
| 8864 | 8876 | } |
| 8877 | + | |
| 8878 | + if (avatarFileNames.length > 0) { | |
| 8879 | + await importCharactersTags(avatarFileNames); | |
| 8880 | + selectImportedChar(avatarFileNames[avatarFileNames.length - 1]); | |
| 8881 | + } | |
| 8882 | +} | |
| 8883 | + | |
| 8884 | +/** | |
| 8885 | + * Imports tags for the given characters | |
| 8886 | + * @param {string[]} avatarFileNames character avatar filenames whose tags are to import | |
| 8887 | + */ | |
| 8888 | +async function importCharactersTags(avatarFileNames) { | |
| 8889 | + await getCharacters(); | |
| 8890 | + for (let i = 0; i < avatarFileNames.length; i++) { | |
| 8891 | + if (power_user.tag_import_setting !== tag_import_setting.NONE) { | |
| 8892 | + const importedCharacter = characters.find(character => character.avatar === avatarFileNames[i]); | |
| 8893 | + await importTags(importedCharacter); | |
| 8894 | + } | |
| 8895 | + } | |
| 8896 | +} | |
| 8897 | + | |
| 8898 | +/** | |
| 8899 | + * Selects the given imported char | |
| 8900 | + * @param {string} charId char to select | |
| 8901 | + */ | |
| 8902 | +function selectImportedChar(charId) { | |
| 8903 | + let oldSelectedChar = null; | |
| 8904 | + if (this_chid !== undefined) { | |
| 8905 | + oldSelectedChar = characters[this_chid].avatar; | |
| 8906 | + } | |
| 8907 | + select_rm_info('char_import_no_toast', charId, oldSelectedChar); | |
| 8865 | 8908 | } |
| 8866 | 8909 | |
| 8867 | 8910 | /** |
| 8868 | 8911 | * Imports a character from a file. |
| 8869 | 8912 | * @param {File} file File to import |
| 8870 | 8913 | * @param {string?object} preserveFileName Whether to preserve original[options] file- nameOptions |
| 8871 | - * @returns {Promise<void>} | |
| 8914 | + * @param {string} [options.preserveFileName] Whether to preserve original file name | |
| 8915 | + * @param {Boolean} [options.importTags=false] Whether to import tags | |
| 8916 | + * @returns {Promise<string>} | |
| 8872 | 8917 | */ |
| 8873 | 8918 | async function importCharacter(file, { preserveFileName = '', importTags = false } = {}) { |
| 8874 | 8919 | if (is_group_generating || is_send_press) { |
| 8875 | 8920 | toastr.error(t`Cannot import characters while generating. Stop the request and try again.`, t`Import aborted`); |
| 8876 | 8921 | throw new Error('Cannot import character while generating'); |
| @@ -8906,19 +8951,14 @@ async function importCharacter(file, preserveFileName = '') { | ||
| 8906 | 8951 | if (data.file_name !== undefined) { |
| 8907 | 8952 | $('#character_search_bar').val('').trigger('input'); |
| 8908 | 8953 | |
| 8909 | - let oldSelectedChar = null; | |
| 8954 | + toastr.success(t`Character Created: ${String(data.file_name).replace('.png', '')}`); | |
| 8910 | - if (this_chid !== undefined) { | |
| 8955 | + let avatarFileName = `${data.file_name}.png`; | |
| 8911 | - oldSelectedChar = characters[this_chid].avatar; | |
| 8956 | + if (importTags) { | |
| 8912 | - } | |
| 8957 | + await importCharactersTags([avatarFileName]); | |
| 8913 | 8958 | |
| 8914 | - await getCharacters(); | |
| 8959 | + selectImportedChar(data.file_name); | |
| 8915 | - select_rm_info('char_import', data.file_name, oldSelectedChar); | |
| 8916 | - if (power_user.tag_import_setting !== tag_import_setting.NONE) { | |
| 8917 | - let currentContext = getContext(); | |
| 8918 | - let avatarFileName = `${data.file_name}.png`; | |
| 8919 | - let importedCharacter = currentContext.characters.find(character => character.avatar === avatarFileName); | |
| 8920 | - await importTags(importedCharacter); | |
| 8921 | 8960 | } |
| 8961 | + return avatarFileName; | |
| 8922 | 8962 | } |
| 8923 | 8963 | } |
| 8924 | 8964 | |
| @@ -10797,8 +10837,17 @@ jQuery(async function () { | ||
| 10797 | 10837 | return; |
| 10798 | 10838 | } |
| 10799 | 10839 | |
| 10840 | + const avatarFileNames = []; | |
| 10800 | 10841 | for (const file of e.target.files) { |
| 10801 | 10842 | const avatarFileName = await importCharacter(file); |
| 10843 | + if (avatarFileName !== undefined) { | |
| 10844 | + avatarFileNames.push(avatarFileName); | |
| 10845 | + } | |
| 10846 | + } | |
| 10847 | + | |
| 10848 | + if (avatarFileNames.length > 0) { | |
| 10849 | + await importCharactersTags(avatarFileNames); | |
| 10850 | + selectImportedChar(avatarFileNames[avatarFileNames.length - 1]); | |
| 10802 | 10851 | } |
| 10803 | 10852 | }); |
| 10804 | 10853 | |
| @@ -585,10 +585,12 @@ async function enlargeMessageImage() { | ||
| 585 | 585 | const imgHolder = document.createElement('div'); |
| 586 | 586 | imgHolder.classList.add('img_enlarged_holder'); |
| 587 | 587 | imgHolder.append(img); |
| 588 | 588 | const imgContainer = $('<div><pre><code class="img_enlarged_title"></code></pre></div>'); |
| 589 | 589 | imgContainer.prepend(imgHolder); |
| 590 | 590 | imgContainer.addClass('img_enlarged_container'); |
| 591 | - imgContainer.find('code').addClass('txt').text(title); | |
| 591 | + | |
| 592 | + const codeTitle = imgContainer.find('.img_enlarged_title'); | |
| 593 | + codeTitle.addClass('txt').text(title); | |
| 592 | 594 | const titleEmpty = !title || title.trim().length === 0; |
| 593 | 595 | imgContainer.find('pre').toggle(!titleEmpty); |
| 594 | 596 | addCopyToCodeBlocks(imgContainer); |
| @@ -598,9 +600,17 @@ async function enlargeMessageImage() { | ||
| 598 | 600 | popup.dlg.style.width = 'unset'; |
| 599 | 601 | popup.dlg.style.height = 'unset'; |
| 600 | 602 | |
| 601 | 603 | img.addEventListener('click', ()event => { |
| 602 | 604 | const shouldZoom = !img.classList.contains('zoomed'); |
| 603 | 605 | img.classList.toggle('zoomed', shouldZoom); |
| 606 | + event.stopPropagation(); | |
| 607 | + }); | |
| 608 | + codeTitle[0]?.addEventListener('click', event => { | |
| 609 | + event.stopPropagation(); | |
| 610 | + }); | |
| 611 | + | |
| 612 | + popup.dlg.addEventListener('click', event => { | |
| 613 | + popup.completeCancelled(); | |
| 604 | 614 | }); |
| 605 | 615 | |
| 606 | 616 | await popup.show(); |
| @@ -4,7 +4,7 @@ import { eventSource, event_types, saveSettings, saveSettingsDebounced, getReque | ||
| 4 | 4 | import { showLoader } from './loader.js'; |
| 5 | 5 | import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; |
| 6 | 6 | import { renderTemplate, renderTemplateAsync } from './templates.js'; |
| 7 | 7 | import { delay, isSubsetOf, sanitizeSelector, setValueByPath } from './utils.js'; |
| 8 | 8 | import { getContext } from './st-context.js'; |
| 9 | 9 | import { isAdmin } from './user.js'; |
| 10 | 10 | import { t } from './i18n.js'; |
| @@ -509,10 +509,11 @@ function addExtensionStyle(name, manifest) { | ||
| 509 | 509 | |
| 510 | 510 | return new Promise((resolve, reject) => { |
| 511 | 511 | const url = `/scripts/extensions/${name}/${manifest.css}`; |
| 512 | + const id = sanitizeSelector(`${name}-css`); | |
| 512 | 513 | |
| 513 | 514 | if ($(`link[id="${nameid}"]`).length === 0) { |
| 514 | 515 | const link = document.createElement('link'); |
| 515 | 516 | link.id = nameid; |
| 516 | 517 | link.rel = 'stylesheet'; |
| 517 | 518 | link.type = 'text/css'; |
| 518 | 519 | link.href = url; |
| @@ -540,11 +541,12 @@ function addExtensionScript(name, manifest) { | ||
| 540 | 541 | |
| 541 | 542 | return new Promise((resolve, reject) => { |
| 542 | 543 | const url = `/scripts/extensions/${name}/${manifest.js}`; |
| 544 | + const id = sanitizeSelector(`${name}-js`); | |
| 543 | 545 | let ready = false; |
| 544 | 546 | |
| 545 | 547 | if ($(`script[id="${nameid}"]`).length === 0) { |
| 546 | 548 | const script = document.createElement('script'); |
| 547 | 549 | script.id = nameid; |
| 548 | 550 | script.type = 'module'; |
| 549 | 551 | script.src = url; |
| 550 | 552 | script.async = true; |
| @@ -277,7 +277,7 @@ function makeMovable(id = 'gallery') { | ||
| 277 | 277 | const newElement = $(template); |
| 278 | 278 | newElement.css('background-color', 'var(--SmartThemeBlurTintColor)'); |
| 279 | 279 | newElement.attr('forChar', id); |
| 280 | 280 | newElement.attr('id', `${id}`); |
| 281 | 281 | newElement.find('.drag-grabber').attr('id', `${id}header`); |
| 282 | 282 | newElement.find('.dragTitle').text('Image Gallery'); |
| 283 | 283 | //add a div for the gallery |
| @@ -388,7 +388,7 @@ class AllTalkTtsProvider { | ||
| 388 | 388 | } |
| 389 | 389 | |
| 390 | 390 | async fetchRvcVoiceObjects() { |
| 391 | 391 | if (this.settings.server_version !== 'v2') { |
| 392 | 392 | console.log('Skipping RVC voices fetch for V1 server'); |
| 393 | 393 | return []; |
| 394 | 394 | } |
| @@ -202,10 +202,19 @@ export function getLastMessageId({ exclude_swipe_in_propress = true, filter = nu | ||
| 202 | 202 | * @returns {number|null} The ID of the first message in the context |
| 203 | 203 | */ |
| 204 | 204 | function getFirstIncludedMessageId() { |
| 205 | - const index = Number(document.querySelector('.lastInContext')?.getAttribute('mesid')); | |
| 205 | + return chat_metadata['lastInContextMessageId']; | |
| 206 | +} | |
| 207 | + | |
| 208 | +/** | |
| 209 | + * Returns the ID of the first displayed message in the chat. | |
| 210 | + * | |
| 211 | + * @returns {number|null} The ID of the first displayed message | |
| 212 | + */ | |
| 213 | +function getFirstDisplayedMessageId() { | |
| 214 | + const mesId = Number(document.querySelector('#chat .mes')?.getAttribute('mesid')); | |
| 206 | 215 | |
| 207 | 216 | if (!isNaN(indexmesId) && indexmesId >= 0) { |
| 208 | 217 | return indexmesId; |
| 209 | 218 | } |
| 210 | 219 | |
| 211 | 220 | return null; |
| @@ -467,6 +476,7 @@ export function evaluateMacros(content, env, postProcessFn) { | ||
| 467 | 476 | { regex: /{{lastUserMessage}}/gi, replace: () => getLastUserMessage() }, |
| 468 | 477 | { regex: /{{lastCharMessage}}/gi, replace: () => getLastCharMessage() }, |
| 469 | 478 | { regex: /{{firstIncludedMessageId}}/gi, replace: () => String(getFirstIncludedMessageId() ?? '') }, |
| 479 | + { regex: /{{firstDisplayedMessageId}}/gi, replace: () => String(getFirstDisplayedMessageId() ?? '') }, | |
| 470 | 480 | { regex: /{{lastSwipeId}}/gi, replace: () => String(getLastSwipeId() ?? '') }, |
| 471 | 481 | { regex: /{{currentSwipeId}}/gi, replace: () => String(getCurrentSwipeId() ?? '') }, |
| 472 | 482 | { regex: /{{reverse:(.+?)}}/gi, replace: (_, str) => Array.from(str).reverse().join('') }, |
| @@ -3963,6 +3963,95 @@ $(document).ready(() => { | ||
| 3963 | 3963 | `, |
| 3964 | 3964 | })); |
| 3965 | 3965 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 3966 | + name: 'css-var', | |
| 3967 | + /** @param {{to: string, varname: string }} args @param {string} value @returns {string} */ | |
| 3968 | + callback: (args, value) => { | |
| 3969 | + // Map enum to target selector | |
| 3970 | + const targetSelector = { | |
| 3971 | + chat: '#chat', | |
| 3972 | + background: '#bg1', | |
| 3973 | + gallery: '#gallery', | |
| 3974 | + zoomedAvatar: 'div.zoomed_avatar', | |
| 3975 | + }[args.to || 'chat']; | |
| 3976 | + | |
| 3977 | + if (!targetSelector) { | |
| 3978 | + toastr.error(`Invalid target: ${args.to}`); | |
| 3979 | + return; | |
| 3980 | + } | |
| 3981 | + | |
| 3982 | + if (!args.varname) { | |
| 3983 | + toastr.error('CSS variable name is required'); | |
| 3984 | + return; | |
| 3985 | + } | |
| 3986 | + if (!args.varname.startsWith('--')) { | |
| 3987 | + toastr.error('CSS variable names must start with "--"'); | |
| 3988 | + return; | |
| 3989 | + } | |
| 3990 | + | |
| 3991 | + const elements = document.querySelectorAll(targetSelector); | |
| 3992 | + if (elements.length === 0) { | |
| 3993 | + toastr.error(`No elements found for ${args.to ?? 'chat'} with selector "${targetSelector}"`); | |
| 3994 | + return; | |
| 3995 | + } | |
| 3996 | + | |
| 3997 | + elements.forEach(element => { | |
| 3998 | + element.style.setProperty(args.varname, value); | |
| 3999 | + }); | |
| 4000 | + | |
| 4001 | + console.info(`Set CSS variable "${args.varname}" to "${value}" on "${targetSelector}"`); | |
| 4002 | + }, | |
| 4003 | + namedArgumentList: [ | |
| 4004 | + SlashCommandNamedArgument.fromProps({ | |
| 4005 | + name: 'varname', | |
| 4006 | + description: 'CSS variable name (starting with double dashes)', | |
| 4007 | + typeList: [ARGUMENT_TYPE.STRING], | |
| 4008 | + isRequired: true, | |
| 4009 | + }), | |
| 4010 | + SlashCommandNamedArgument.fromProps({ | |
| 4011 | + name: 'to', | |
| 4012 | + description: 'The target element to which the CSS variable will be applied', | |
| 4013 | + typeList: [ARGUMENT_TYPE.STRING], | |
| 4014 | + enumList: [ | |
| 4015 | + new SlashCommandEnumValue('chat', null, enumTypes.enum, enumIcons.message), | |
| 4016 | + new SlashCommandEnumValue('background', null, enumTypes.enum, enumIcons.image), | |
| 4017 | + new SlashCommandEnumValue('zoomedAvatar', null, enumTypes.enum, enumIcons.character), | |
| 4018 | + new SlashCommandEnumValue('gallery', null, enumTypes.enum, enumIcons.image), | |
| 4019 | + ], | |
| 4020 | + defaultValue: 'chat', | |
| 4021 | + }), | |
| 4022 | + ], | |
| 4023 | + unnamedArgumentList: [ | |
| 4024 | + SlashCommandArgument.fromProps({ | |
| 4025 | + description: 'CSS variable value', | |
| 4026 | + typeList: [ARGUMENT_TYPE.STRING], | |
| 4027 | + isRequired: true, | |
| 4028 | + }), | |
| 4029 | + ], | |
| 4030 | + helpString: ` | |
| 4031 | + <div> | |
| 4032 | + Sets a CSS variable to a specified value on a target element. | |
| 4033 | + <br /> | |
| 4034 | + Only setting of variable names is supported. They have to be prefixed with double dashes ("--exampleVar"). | |
| 4035 | + Setting actual CSS properties is not supported. Custom CSS in the theme settings can be used for that. | |
| 4036 | + <br /><br /> | |
| 4037 | + <b>This value will be gone after a page reload!</b> | |
| 4038 | + </div> | |
| 4039 | + <div> | |
| 4040 | + <strong>Example:</strong> | |
| 4041 | + <ul> | |
| 4042 | + <li> | |
| 4043 | + <pre><code>/css-var varname="--SmartThemeBodyColor" #ff0000</code></pre> | |
| 4044 | + Sets the text color of the chat to red | |
| 4045 | + </li> | |
| 4046 | + <li> | |
| 4047 | + <pre><code>/css-var to=zoomedAvatar varname="--SmartThemeBlurStrength" 0</code></pre> | |
| 4048 | + Remove the blur from the zoomed avatar | |
| 4049 | + </li> | |
| 4050 | + </ul> | |
| 4051 | + </div> | |
| 4052 | + `, | |
| 4053 | + })); | |
| 4054 | + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | |
| 3966 | 4055 | name: 'movingui', |
| 3967 | 4056 | callback: setmovingUIPreset, |
| 3968 | 4057 | unnamedArgumentList: [ |
| @@ -39,6 +39,7 @@ import { | ||
| 39 | 39 | setCharacterName, |
| 40 | 40 | setExtensionPrompt, |
| 41 | 41 | setUserName, |
| 42 | + showMoreMessages, | |
| 42 | 43 | stopGeneration, |
| 43 | 44 | substituteParams, |
| 44 | 45 | system_avatar, |
| @@ -1964,6 +1965,39 @@ export function initDefaultSlashCommands() { | ||
| 1964 | 1965 | returns: ARGUMENT_TYPE.BOOLEAN, |
| 1965 | 1966 | helpString: 'Returns true if the current device is a mobile device, false otherwise. Equivalent to <code>{{isMobile}}</code> macro.', |
| 1966 | 1967 | })); |
| 1968 | + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | |
| 1969 | + name: 'chat-render', | |
| 1970 | + helpString: 'Renders a specified number of messages into the chat window. Displays all messages if no argument is provided.', | |
| 1971 | + callback: (args, number) => { | |
| 1972 | + showMoreMessages(number && !isNaN(Number(number)) ? Number(number) : Number.MAX_SAFE_INTEGER); | |
| 1973 | + if (isTrueBoolean(String(args?.scroll ?? ''))) { | |
| 1974 | + $('#chat').scrollTop(0); | |
| 1975 | + } | |
| 1976 | + return ''; | |
| 1977 | + }, | |
| 1978 | + namedArgumentList: [ | |
| 1979 | + SlashCommandNamedArgument.fromProps({ | |
| 1980 | + name: 'scroll', | |
| 1981 | + description: 'scroll to the top after rendering', | |
| 1982 | + typeList: [ARGUMENT_TYPE.BOOLEAN], | |
| 1983 | + defaultValue: 'false', | |
| 1984 | + enumList: commonEnumProviders.boolean('trueFalse')(), | |
| 1985 | + }), | |
| 1986 | + ], | |
| 1987 | + unnamedArgumentList: [ | |
| 1988 | + new SlashCommandArgument( | |
| 1989 | + 'number of messages', [ARGUMENT_TYPE.NUMBER], false, | |
| 1990 | + ), | |
| 1991 | + ], | |
| 1992 | + })); | |
| 1993 | + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | |
| 1994 | + name: 'chat-reload', | |
| 1995 | + helpString: 'Reloads the current chat.', | |
| 1996 | + callback: async () => { | |
| 1997 | + await reloadCurrentChat(); | |
| 1998 | + return ''; | |
| 1999 | + }, | |
| 2000 | + })); | |
| 1967 | 2001 | |
| 1968 | 2002 | registerVariableCommands(); |
| 1969 | 2003 | } |
| @@ -37,6 +37,7 @@ export const enumIcons = { | ||
| 37 | 37 | voice: 'π€', |
| 38 | 38 | server: 'π₯οΈ', |
| 39 | 39 | popup: 'π', |
| 40 | + image: 'πΌοΈ', | |
| 40 | 41 | |
| 41 | 42 | true: 'βοΈ', |
| 42 | 43 | false: 'β', |
| @@ -28,7 +28,8 @@ | ||
| 28 | 28 | <li><tt>{{lastUserMessage}}</tt> β <span data-i18n="help_macros_lastUser">the text of the latest user chat message.</span></li> |
| 29 | 29 | <li><tt>{{lastCharMessage}}</tt> β <span data-i18n="help_macros_lastChar">the text of the latest character chat message.</span></li> |
| 30 | 30 | <li><tt>{{lastMessageId}}</tt> β <span data-i18n="help_macros_21">index # of the latest chat message. Useful for slash command batching.</span></li> |
| 31 | 31 | <li><tt>{{firstIncludedMessageId}}</tt> β <span data-i18n="help_macros_22">the ID of the first message included in the context. Requires generation to be ranrun at least once in the current session. Will only be updated on generation.</span></li> |
| 32 | + <li><tt>{{firstDisplayedMessageId}}</tt> β <span data-i18n="help_macros_firstDisplayedMessageId">the ID of the first message loaded into the visible chat.</span></li> | |
| 32 | 33 | <li><tt>{{currentSwipeId}}</tt> β <span data-i18n="help_macros_23">the 1-based ID of the current swipe in the last chat message. Empty string if the last message is user or prompt-hidden.</span></li> |
| 33 | 34 | <li><tt>{{lastSwipeId}}</tt> β <span data-i18n="help_macros_24">the number of swipes in the last chat message. Empty string if the last message is user or prompt-hidden.</span></li> |
| 34 | 35 | <li><tt>{{reverse:(content)}}</tt> β <span data-i18n="help_macros_reverse">reverses the content of the macro.</span></li> |
| @@ -67,6 +67,16 @@ export function escapeHtml(str) { | ||
| 67 | 67 | return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); |
| 68 | 68 | } |
| 69 | 69 | |
| 70 | +/** | |
| 71 | + * Make string safe for use as a CSS selector. | |
| 72 | + * @param {string} str String to sanitize | |
| 73 | + * @param {string} replacement Replacement for invalid characters | |
| 74 | + * @returns {string} Sanitized string | |
| 75 | + */ | |
| 76 | +export function sanitizeSelector(str, replacement = '_') { | |
| 77 | + return String(str).replace(/[^a-z0-9_-]/ig, replacement); | |
| 78 | +} | |
| 79 | + | |
| 70 | 80 | export function isValidUrl(value) { |
| 71 | 81 | try { |
| 72 | 82 | new URL(value); |
| @@ -4799,7 +4799,7 @@ body:not(.sd) .mes_img_swipes { | ||
| 4799 | 4799 | |
| 4800 | 4800 | .img_enlarged { |
| 4801 | 4801 | object-fit: contain; |
| 4802 | 4802 | max-width: 100%; |
| 4803 | 4803 | height: 100%; |
| 4804 | 4804 | cursor: zoom-in |
| 4805 | 4805 | } |