Merge branch 'SillyTavern:staging' into staging
Signed| @@ -167,6 +167,7 @@ import { | |||
| 167 | flashHighlight, | 167 | flashHighlight, |
| 168 | isTrueBoolean, | 168 | isTrueBoolean, |
| 169 | toggleDrawer, | 169 | toggleDrawer, |
| 170 | isElementInViewport, | ||
| 170 | } from './scripts/utils.js'; | 171 | } from './scripts/utils.js'; |
| 171 | import { debounce_timeout } from './scripts/constants.js'; | 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 | export function showMoreMessages() { | 1831 | export function showMoreMessages(messagesToLoad = null) { |
| 1831 | const firstDisplayedMesId = $('#chat').children('.mes').first().attr('mesid'); | 1832 | const firstDisplayedMesId = $('#chat').children('.mes').first().attr('mesid'); |
| 1832 | let messageId = Number(firstDisplayedMesId); | 1833 | let messageId = Number(firstDisplayedMesId); |
| 1833 | let count = power_user.chat_truncation || Number.MAX_SAFE_INTEGER; | 1834 | let count = messagesToLoad || power_user.chat_truncation || Number.MAX_SAFE_INTEGER; |
| 1834 | 1835 | ||
| 1835 | // If there are no messages displayed, or the message somehow has no mesid, we default to one higher than last message id, | 1836 | // If there are no messages displayed, or the message somehow has no mesid, we default to one higher than last message id, |
| 1836 | // so the first "new" message being shown will be the last available message | 1837 | // so the first "new" message being shown will be the last available message |
| @@ -1840,6 +1841,7 @@ export function showMoreMessages() { | |||
| 1840 | 1841 | ||
| 1841 | console.debug('Inserting messages before', messageId, 'count', count, 'chat length', chat.length); | 1842 | console.debug('Inserting messages before', messageId, 'count', count, 'chat length', chat.length); |
| 1842 | const prevHeight = $('#chat').prop('scrollHeight'); | 1843 | const prevHeight = $('#chat').prop('scrollHeight'); |
| 1844 | const isButtonInView = isElementInViewport($('#show_more_messages')[0]); | ||
| 1843 | 1845 | ||
| 1844 | while (messageId > 0 && count > 0) { | 1846 | while (messageId > 0 && count > 0) { |
| 1845 | let newMessageId = messageId - 1; | 1847 | let newMessageId = messageId - 1; |
| @@ -1852,8 +1854,10 @@ export function showMoreMessages() { | |||
| 1852 | $('#show_more_messages').remove(); | 1854 | $('#show_more_messages').remove(); |
| 1853 | } | 1855 | } |
| 1854 | 1856 | ||
| 1855 | const newHeight = $('#chat').prop('scrollHeight'); | 1857 | if (isButtonInView) { |
| 1856 | $('#chat').scrollTop(newHeight - prevHeight); | 1858 | const newHeight = $('#chat').prop('scrollHeight'); |
| 1859 | $('#chat').scrollTop(newHeight - prevHeight); | ||
| 1860 | } | ||
| 1857 | } | 1861 | } |
| 1858 | 1862 | ||
| 1859 | export async function printMessages() { | 1863 | export async function printMessages() { |
| @@ -5425,20 +5429,24 @@ async function promptItemize(itemizedPrompts, requestedMesId) { | |||
| 5425 | await popup.show(); | 5429 | await popup.show(); |
| 5426 | } | 5430 | } |
| 5427 | 5431 | ||
| 5428 | function setInContextMessages(lastmsg, type) { | 5432 | function setInContextMessages(msgInContextCount, type) { |
| 5429 | $('#chat .mes').removeClass('lastInContext'); | 5433 | $('#chat .mes').removeClass('lastInContext'); |
| 5430 | 5434 | ||
| 5431 | if (type === 'swipe' || type === 'regenerate' || type === 'continue') { | 5435 | if (type === 'swipe' || type === 'regenerate' || type === 'continue') { |
| 5432 | lastmsg++; | 5436 | msgInContextCount++; |
| 5433 | } | 5437 | } |
| 5434 | 5438 | ||
| 5435 | const lastMessageBlock = $('#chat .mes:not([is_system="true"])').eq(-lastmsg); | 5439 | const lastMessageBlock = $('#chat .mes:not([is_system="true"])').eq(-msgInContextCount); |
| 5436 | lastMessageBlock.addClass('lastInContext'); | 5440 | lastMessageBlock.addClass('lastInContext'); |
| 5437 | 5441 | ||
| 5438 | if (lastMessageBlock.length === 0) { | 5442 | if (lastMessageBlock.length === 0) { |
| 5439 | const firstMessageId = getFirstDisplayedMessageId(); | 5443 | const firstMessageId = getFirstDisplayedMessageId(); |
| 5440 | $(`#chat .mes[mesid="${firstMessageId}"`).addClass('lastInContext'); | 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 | // Set a timeout so multiple flashes don't overlap | 7309 | // Set a timeout so multiple flashes don't overlap |
| 7302 | clearTimeout(importFlashTimeout); | 7310 | clearTimeout(importFlashTimeout); |
| 7303 | importFlashTimeout = setTimeout(function () { | 7311 | importFlashTimeout = setTimeout(function () { |
| 7304 | if (type === 'char_import' || type === 'char_create') { | 7312 | if (type === 'char_import' || type === 'char_create' || type === 'char_import_no_toast') { |
| 7305 | // Find the page at which the character is located | 7313 | // Find the page at which the character is located |
| 7306 | const avatarFileName = charId; | 7314 | const avatarFileName = charId; |
| 7307 | const charData = getEntitiesList({ doFilter: true }); | 7315 | const charData = getEntitiesList({ doFilter: true }); |
| @@ -8853,24 +8861,61 @@ export async function processDroppedFiles(files, data = new Map()) { | |||
| 8853 | 'charx', | 8861 | 'charx', |
| 8854 | ]; | 8862 | ]; |
| 8855 | 8863 | ||
| 8864 | const avatarFileNames = []; | ||
| 8856 | for (const file of files) { | 8865 | for (const file of files) { |
| 8857 | const extension = file.name.split('.').pop().toLowerCase(); | 8866 | const extension = file.name.split('.').pop().toLowerCase(); |
| 8858 | if (allowedMimeTypes.some(x => file.type.startsWith(x)) || allowedExtensions.includes(extension)) { | 8867 | if (allowedMimeTypes.some(x => file.type.startsWith(x)) || allowedExtensions.includes(extension)) { |
| 8859 | const preservedName = data instanceof Map && data.get(file); | 8868 | const preservedName = data instanceof Map && data.get(file); |
| 8860 | await importCharacter(file, preservedName); | 8869 | const avatarFileName = await importCharacter(file, { preserveFileName: preservedName }); |
| 8870 | if (avatarFileName !== undefined) { | ||
| 8871 | avatarFileNames.push(avatarFileName); | ||
| 8872 | } | ||
| 8861 | } else { | 8873 | } else { |
| 8862 | toastr.warning(t`Unsupported file type: ` + file.name); | 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 | * Imports a character from a file. | 8911 | * Imports a character from a file. |
| 8869 | * @param {File} file File to import | 8912 | * @param {File} file File to import |
| 8870 | * @param {string?} preserveFileName Whether to preserve original file name | 8913 | * @param {object} [options] - Options |
| 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 | async function importCharacter(file, preserveFileName = '') { | 8918 | async function importCharacter(file, { preserveFileName = '', importTags = false } = {}) { |
| 8874 | if (is_group_generating || is_send_press) { | 8919 | if (is_group_generating || is_send_press) { |
| 8875 | toastr.error(t`Cannot import characters while generating. Stop the request and try again.`, t`Import aborted`); | 8920 | toastr.error(t`Cannot import characters while generating. Stop the request and try again.`, t`Import aborted`); |
| 8876 | throw new Error('Cannot import character while generating'); | 8921 | throw new Error('Cannot import character while generating'); |
| @@ -8906,19 +8951,14 @@ async function importCharacter(file, preserveFileName = '') { | |||
| 8906 | if (data.file_name !== undefined) { | 8951 | if (data.file_name !== undefined) { |
| 8907 | $('#character_search_bar').val('').trigger('input'); | 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 | return; | 10837 | return; |
| 10798 | } | 10838 | } |
| 10799 | 10839 | ||
| 10840 | const avatarFileNames = []; | ||
| 10800 | for (const file of e.target.files) { | 10841 | for (const file of e.target.files) { |
| 10801 | await importCharacter(file); | 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 | const imgHolder = document.createElement('div'); | 585 | const imgHolder = document.createElement('div'); |
| 586 | imgHolder.classList.add('img_enlarged_holder'); | 586 | imgHolder.classList.add('img_enlarged_holder'); |
| 587 | imgHolder.append(img); | 587 | imgHolder.append(img); |
| 588 | const imgContainer = $('<div><pre><code></code></pre></div>'); | 588 | const imgContainer = $('<div><pre><code class="img_enlarged_title"></code></pre></div>'); |
| 589 | imgContainer.prepend(imgHolder); | 589 | imgContainer.prepend(imgHolder); |
| 590 | imgContainer.addClass('img_enlarged_container'); | 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 | const titleEmpty = !title || title.trim().length === 0; | 594 | const titleEmpty = !title || title.trim().length === 0; |
| 593 | imgContainer.find('pre').toggle(!titleEmpty); | 595 | imgContainer.find('pre').toggle(!titleEmpty); |
| 594 | addCopyToCodeBlocks(imgContainer); | 596 | addCopyToCodeBlocks(imgContainer); |
| @@ -598,9 +600,17 @@ async function enlargeMessageImage() { | |||
| 598 | popup.dlg.style.width = 'unset'; | 600 | popup.dlg.style.width = 'unset'; |
| 599 | popup.dlg.style.height = 'unset'; | 601 | popup.dlg.style.height = 'unset'; |
| 600 | 602 | ||
| 601 | img.addEventListener('click', () => { | 603 | img.addEventListener('click', event => { |
| 602 | const shouldZoom = !img.classList.contains('zoomed'); | 604 | const shouldZoom = !img.classList.contains('zoomed'); |
| 603 | img.classList.toggle('zoomed', shouldZoom); | 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 | await popup.show(); | 616 | await popup.show(); |
| @@ -4,7 +4,7 @@ import { eventSource, event_types, saveSettings, saveSettingsDebounced, getReque | |||
| 4 | import { showLoader } from './loader.js'; | 4 | import { showLoader } from './loader.js'; |
| 5 | import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; | 5 | import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; |
| 6 | import { renderTemplate, renderTemplateAsync } from './templates.js'; | 6 | import { renderTemplate, renderTemplateAsync } from './templates.js'; |
| 7 | import { delay, isSubsetOf, setValueByPath } from './utils.js'; | 7 | import { delay, isSubsetOf, sanitizeSelector, setValueByPath } from './utils.js'; |
| 8 | import { getContext } from './st-context.js'; | 8 | import { getContext } from './st-context.js'; |
| 9 | import { isAdmin } from './user.js'; | 9 | import { isAdmin } from './user.js'; |
| 10 | import { t } from './i18n.js'; | 10 | import { t } from './i18n.js'; |
| @@ -509,10 +509,11 @@ function addExtensionStyle(name, manifest) { | |||
| 509 | 509 | ||
| 510 | return new Promise((resolve, reject) => { | 510 | return new Promise((resolve, reject) => { |
| 511 | const url = `/scripts/extensions/${name}/${manifest.css}`; | 511 | const url = `/scripts/extensions/${name}/${manifest.css}`; |
| 512 | const id = sanitizeSelector(`${name}-css`); | ||
| 512 | 513 | ||
| 513 | if ($(`link[id="${name}"]`).length === 0) { | 514 | if ($(`link[id="${id}"]`).length === 0) { |
| 514 | const link = document.createElement('link'); | 515 | const link = document.createElement('link'); |
| 515 | link.id = name; | 516 | link.id = id; |
| 516 | link.rel = 'stylesheet'; | 517 | link.rel = 'stylesheet'; |
| 517 | link.type = 'text/css'; | 518 | link.type = 'text/css'; |
| 518 | link.href = url; | 519 | link.href = url; |
| @@ -540,11 +541,12 @@ function addExtensionScript(name, manifest) { | |||
| 540 | 541 | ||
| 541 | return new Promise((resolve, reject) => { | 542 | return new Promise((resolve, reject) => { |
| 542 | const url = `/scripts/extensions/${name}/${manifest.js}`; | 543 | const url = `/scripts/extensions/${name}/${manifest.js}`; |
| 544 | const id = sanitizeSelector(`${name}-js`); | ||
| 543 | let ready = false; | 545 | let ready = false; |
| 544 | 546 | ||
| 545 | if ($(`script[id="${name}"]`).length === 0) { | 547 | if ($(`script[id="${id}"]`).length === 0) { |
| 546 | const script = document.createElement('script'); | 548 | const script = document.createElement('script'); |
| 547 | script.id = name; | 549 | script.id = id; |
| 548 | script.type = 'module'; | 550 | script.type = 'module'; |
| 549 | script.src = url; | 551 | script.src = url; |
| 550 | script.async = true; | 552 | script.async = true; |
| @@ -277,7 +277,7 @@ function makeMovable(id = 'gallery') { | |||
| 277 | const newElement = $(template); | 277 | const newElement = $(template); |
| 278 | newElement.css('background-color', 'var(--SmartThemeBlurTintColor)'); | 278 | newElement.css('background-color', 'var(--SmartThemeBlurTintColor)'); |
| 279 | newElement.attr('forChar', id); | 279 | newElement.attr('forChar', id); |
| 280 | newElement.attr('id', `${id}`); | 280 | newElement.attr('id', id); |
| 281 | newElement.find('.drag-grabber').attr('id', `${id}header`); | 281 | newElement.find('.drag-grabber').attr('id', `${id}header`); |
| 282 | newElement.find('.dragTitle').text('Image Gallery'); | 282 | newElement.find('.dragTitle').text('Image Gallery'); |
| 283 | //add a div for the gallery | 283 | //add a div for the gallery |
| @@ -388,7 +388,7 @@ class AllTalkTtsProvider { | |||
| 388 | } | 388 | } |
| 389 | 389 | ||
| 390 | async fetchRvcVoiceObjects() { | 390 | async fetchRvcVoiceObjects() { |
| 391 | if (this.settings.server_version !== 'v2') { | 391 | if (this.settings.server_version == 'v2') { |
| 392 | console.log('Skipping RVC voices fetch for V1 server'); | 392 | console.log('Skipping RVC voices fetch for V1 server'); |
| 393 | return []; | 393 | return []; |
| 394 | } | 394 | } |
| @@ -202,10 +202,19 @@ export function getLastMessageId({ exclude_swipe_in_propress = true, filter = nu | |||
| 202 | * @returns {number|null} The ID of the first message in the context | 202 | * @returns {number|null} The ID of the first message in the context |
| 203 | */ | 203 | */ |
| 204 | function getFirstIncludedMessageId() { | 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 | if (!isNaN(index) && index >= 0) { | 216 | if (!isNaN(mesId) && mesId >= 0) { |
| 208 | return index; | 217 | return mesId; |
| 209 | } | 218 | } |
| 210 | 219 | ||
| 211 | return null; | 220 | return null; |
| @@ -467,6 +476,7 @@ export function evaluateMacros(content, env, postProcessFn) { | |||
| 467 | { regex: /{{lastUserMessage}}/gi, replace: () => getLastUserMessage() }, | 476 | { regex: /{{lastUserMessage}}/gi, replace: () => getLastUserMessage() }, |
| 468 | { regex: /{{lastCharMessage}}/gi, replace: () => getLastCharMessage() }, | 477 | { regex: /{{lastCharMessage}}/gi, replace: () => getLastCharMessage() }, |
| 469 | { regex: /{{firstIncludedMessageId}}/gi, replace: () => String(getFirstIncludedMessageId() ?? '') }, | 478 | { regex: /{{firstIncludedMessageId}}/gi, replace: () => String(getFirstIncludedMessageId() ?? '') }, |
| 479 | { regex: /{{firstDisplayedMessageId}}/gi, replace: () => String(getFirstDisplayedMessageId() ?? '') }, | ||
| 470 | { regex: /{{lastSwipeId}}/gi, replace: () => String(getLastSwipeId() ?? '') }, | 480 | { regex: /{{lastSwipeId}}/gi, replace: () => String(getLastSwipeId() ?? '') }, |
| 471 | { regex: /{{currentSwipeId}}/gi, replace: () => String(getCurrentSwipeId() ?? '') }, | 481 | { regex: /{{currentSwipeId}}/gi, replace: () => String(getCurrentSwipeId() ?? '') }, |
| 472 | { regex: /{{reverse:(.+?)}}/gi, replace: (_, str) => Array.from(str).reverse().join('') }, | 482 | { regex: /{{reverse:(.+?)}}/gi, replace: (_, str) => Array.from(str).reverse().join('') }, |
| @@ -3963,6 +3963,95 @@ $(document).ready(() => { | |||
| 3963 | `, | 3963 | `, |
| 3964 | })); | 3964 | })); |
| 3965 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 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 | name: 'movingui', | 4055 | name: 'movingui', |
| 3967 | callback: setmovingUIPreset, | 4056 | callback: setmovingUIPreset, |
| 3968 | unnamedArgumentList: [ | 4057 | unnamedArgumentList: [ |
| @@ -39,6 +39,7 @@ import { | |||
| 39 | setCharacterName, | 39 | setCharacterName, |
| 40 | setExtensionPrompt, | 40 | setExtensionPrompt, |
| 41 | setUserName, | 41 | setUserName, |
| 42 | showMoreMessages, | ||
| 42 | stopGeneration, | 43 | stopGeneration, |
| 43 | substituteParams, | 44 | substituteParams, |
| 44 | system_avatar, | 45 | system_avatar, |
| @@ -1964,6 +1965,39 @@ export function initDefaultSlashCommands() { | |||
| 1964 | returns: ARGUMENT_TYPE.BOOLEAN, | 1965 | returns: ARGUMENT_TYPE.BOOLEAN, |
| 1965 | helpString: 'Returns true if the current device is a mobile device, false otherwise. Equivalent to <code>{{isMobile}}</code> macro.', | 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 | registerVariableCommands(); | 2002 | registerVariableCommands(); |
| 1969 | } | 2003 | } |
| @@ -37,6 +37,7 @@ export const enumIcons = { | |||
| 37 | voice: 'π€', | 37 | voice: 'π€', |
| 38 | server: 'π₯οΈ', | 38 | server: 'π₯οΈ', |
| 39 | popup: 'π', | 39 | popup: 'π', |
| 40 | image: 'πΌοΈ', | ||
| 40 | 41 | ||
| 41 | true: 'βοΈ', | 42 | true: 'βοΈ', |
| 42 | false: 'β', | 43 | false: 'β', |
| @@ -28,7 +28,8 @@ | |||
| 28 | <li><tt>{{lastUserMessage}}</tt> β <span data-i18n="help_macros_lastUser">the text of the latest user chat message.</span></li> | 28 | <li><tt>{{lastUserMessage}}</tt> β <span data-i18n="help_macros_lastUser">the text of the latest user chat message.</span></li> |
| 29 | <li><tt>{{lastCharMessage}}</tt> β <span data-i18n="help_macros_lastChar">the text of the latest character chat message.</span></li> | 29 | <li><tt>{{lastCharMessage}}</tt> β <span data-i18n="help_macros_lastChar">the text of the latest character chat message.</span></li> |
| 30 | <li><tt>{{lastMessageId}}</tt> β <span data-i18n="help_macros_21">index # of the latest chat message. Useful for slash command batching.</span></li> | 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 | <li><tt>{{firstIncludedMessageId}}</tt> β <span data-i18n="help_macros_22">the ID of the first message included in the context. Requires generation to be ran at least once in the current session.</span></li> | 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 run 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 | <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 | <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 | <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 | <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 | <li><tt>{{reverse:(content)}}</tt> β <span data-i18n="help_macros_reverse">reverses the content of the macro.</span></li> | 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 | return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); | 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 | export function isValidUrl(value) { | 80 | export function isValidUrl(value) { |
| 71 | try { | 81 | try { |
| 72 | new URL(value); | 82 | new URL(value); |
| @@ -4799,7 +4799,7 @@ body:not(.sd) .mes_img_swipes { | |||
| 4799 | 4799 | ||
| 4800 | .img_enlarged { | 4800 | .img_enlarged { |
| 4801 | object-fit: contain; | 4801 | object-fit: contain; |
| 4802 | width: 100%; | 4802 | max-width: 100%; |
| 4803 | height: 100%; | 4803 | height: 100%; |
| 4804 | cursor: zoom-in | 4804 | cursor: zoom-in |
| 4805 | } | 4805 | } |