From c7cbeed9bb1b73ccb011ad2ba5da373837109360 Mon Sep 17 00:00:00 2001 From: d-ber Date: Sun, 5 Jan 2025 22:36:10 +0100 Subject: [PATCH 01/17] speed up char import by postponing tag import --- public/script.js | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/public/script.js b/public/script.js index d5ed97d4d..ece64ac58 100644 --- a/public/script.js +++ b/public/script.js @@ -8851,22 +8851,43 @@ export async function processDroppedFiles(files, data = new Map()) { 'charx', ]; + const avatarFileNames = []; for (const file of files) { const extension = file.name.split('.').pop().toLowerCase(); if (allowedMimeTypes.some(x => file.type.startsWith(x)) || allowedExtensions.includes(extension)) { const preservedName = data instanceof Map && data.get(file); - await importCharacter(file, preservedName); + const avatarFileName = await importCharacter(file, preservedName); + if (avatarFileName !== undefined){ + avatarFileNames.push(avatarFileName); + } } else { toastr.warning(t`Unsupported file type: ` + file.name); } } + + await ImportMultipleCharactersTags(avatarFileNames); +} + +/** + * Imports tags for the given characters + * @param {string[]} avatarFileNames character avatar filenames whose tags are to import + */ +async function ImportMultipleCharactersTags(avatarFileNames) { + await getCharacters(); + const currentContext = getContext(); + for (let i = 0; i < avatarFileNames.length; i++) { + if (power_user.tag_import_setting !== tag_import_setting.NONE) { + const importedCharacter = currentContext.characters.find(character => character.avatar === avatarFileNames[i]); + await importTags(importedCharacter); + } + } } /** * Imports a character from a file. * @param {File} file File to import * @param {string?} preserveFileName Whether to preserve original file name - * @returns {Promise} + * @returns {Promise} */ async function importCharacter(file, preserveFileName = '') { if (is_group_generating || is_send_press) { @@ -8909,14 +8930,9 @@ async function importCharacter(file, preserveFileName = '') { oldSelectedChar = characters[this_chid].avatar; } - await getCharacters(); select_rm_info('char_import', data.file_name, oldSelectedChar); - if (power_user.tag_import_setting !== tag_import_setting.NONE) { - let currentContext = getContext(); - let avatarFileName = `${data.file_name}.png`; - let importedCharacter = currentContext.characters.find(character => character.avatar === avatarFileName); - await importTags(importedCharacter); - } + let avatarFileName = `${data.file_name}.png`; + return avatarFileName; } } @@ -10795,9 +10811,15 @@ jQuery(async function () { return; } + const avatarFileNames = []; for (const file of e.target.files) { - await importCharacter(file); + const avatarFileName = await importCharacter(file); + if (avatarFileName !== undefined){ + avatarFileNames.push(avatarFileName); + } } + + await ImportMultipleCharactersTags(avatarFileNames); }); $('#export_button').on('click', function () { From 96cd683d0eb1f772d1c585c8452a6a3bc3bf0fee Mon Sep 17 00:00:00 2001 From: d-ber Date: Mon, 6 Jan 2025 00:28:38 +0100 Subject: [PATCH 02/17] added tag import back into importCharacter --- public/script.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/public/script.js b/public/script.js index ece64ac58..587c36ba7 100644 --- a/public/script.js +++ b/public/script.js @@ -8856,7 +8856,7 @@ export async function processDroppedFiles(files, data = new Map()) { const extension = file.name.split('.').pop().toLowerCase(); if (allowedMimeTypes.some(x => file.type.startsWith(x)) || allowedExtensions.includes(extension)) { const preservedName = data instanceof Map && data.get(file); - const avatarFileName = await importCharacter(file, preservedName); + const avatarFileName = await importCharacter(file, {preserveFileName: preservedName}); if (avatarFileName !== undefined){ avatarFileNames.push(avatarFileName); } @@ -8865,14 +8865,14 @@ export async function processDroppedFiles(files, data = new Map()) { } } - await ImportMultipleCharactersTags(avatarFileNames); + await ImportCharactersTags(avatarFileNames); } /** * Imports tags for the given characters * @param {string[]} avatarFileNames character avatar filenames whose tags are to import */ -async function ImportMultipleCharactersTags(avatarFileNames) { +async function ImportCharactersTags(avatarFileNames) { await getCharacters(); const currentContext = getContext(); for (let i = 0; i < avatarFileNames.length; i++) { @@ -8886,10 +8886,12 @@ async function ImportMultipleCharactersTags(avatarFileNames) { /** * Imports a character from a file. * @param {File} file File to import - * @param {string?} preserveFileName Whether to preserve original file name + * @param {object} [options] - Options + * @param {string} [options.preserveFileName] Whether to preserve original file name + * @param {Boolean} [options.importTags=false] Whether to import tags * @returns {Promise} */ -async function importCharacter(file, preserveFileName = '') { +async function importCharacter(file, {preserveFileName = '', importTags = false} = {}) { if (is_group_generating || is_send_press) { toastr.error(t`Cannot import characters while generating. Stop the request and try again.`, t`Import aborted`); throw new Error('Cannot import character while generating'); @@ -8932,6 +8934,9 @@ async function importCharacter(file, preserveFileName = '') { select_rm_info('char_import', data.file_name, oldSelectedChar); let avatarFileName = `${data.file_name}.png`; + if (importTags) { + ImportCharactersTags([avatarFileName]) + } return avatarFileName; } } @@ -10818,8 +10823,7 @@ jQuery(async function () { avatarFileNames.push(avatarFileName); } } - - await ImportMultipleCharactersTags(avatarFileNames); + await ImportCharactersTags(avatarFileNames); }); $('#export_button').on('click', function () { From c22ed52c72d0e3cb7d94b99f310295367c244de2 Mon Sep 17 00:00:00 2001 From: d-ber Date: Mon, 6 Jan 2025 21:35:03 +0100 Subject: [PATCH 03/17] fix function name, and minor fixes --- public/script.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/public/script.js b/public/script.js index 587c36ba7..caf4d8468 100644 --- a/public/script.js +++ b/public/script.js @@ -8865,19 +8865,18 @@ export async function processDroppedFiles(files, data = new Map()) { } } - await ImportCharactersTags(avatarFileNames); + await importCharactersTags(avatarFileNames); } /** * Imports tags for the given characters * @param {string[]} avatarFileNames character avatar filenames whose tags are to import */ -async function ImportCharactersTags(avatarFileNames) { +async function importCharactersTags(avatarFileNames) { await getCharacters(); - const currentContext = getContext(); for (let i = 0; i < avatarFileNames.length; i++) { if (power_user.tag_import_setting !== tag_import_setting.NONE) { - const importedCharacter = currentContext.characters.find(character => character.avatar === avatarFileNames[i]); + const importedCharacter = characters.find(character => character.avatar === avatarFileNames[i]); await importTags(importedCharacter); } } @@ -8935,7 +8934,7 @@ async function importCharacter(file, {preserveFileName = '', importTags = false} select_rm_info('char_import', data.file_name, oldSelectedChar); let avatarFileName = `${data.file_name}.png`; if (importTags) { - ImportCharactersTags([avatarFileName]) + await importCharactersTags([avatarFileName]) } return avatarFileName; } @@ -10823,7 +10822,7 @@ jQuery(async function () { avatarFileNames.push(avatarFileName); } } - await ImportCharactersTags(avatarFileNames); + await importCharactersTags(avatarFileNames); }); $('#export_button').on('click', function () { From d4f23de003997002d33f99bec8a824177f4a0b73 Mon Sep 17 00:00:00 2001 From: d-ber Date: Mon, 6 Jan 2025 22:41:32 +0100 Subject: [PATCH 04/17] fix toasts and final char selection --- public/script.js | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/public/script.js b/public/script.js index caf4d8468..7029de053 100644 --- a/public/script.js +++ b/public/script.js @@ -7299,7 +7299,7 @@ export function select_rm_info(type, charId, previousCharId = null) { // Set a timeout so multiple flashes don't overlap clearTimeout(importFlashTimeout); importFlashTimeout = setTimeout(function () { - if (type === 'char_import' || type === 'char_create') { + if (type === 'char_import' || type === 'char_create' || type === 'char_import_no_toast') { // Find the page at which the character is located const avatarFileName = charId; const charData = getEntitiesList({ doFilter: true }); @@ -8865,7 +8865,10 @@ export async function processDroppedFiles(files, data = new Map()) { } } - await importCharactersTags(avatarFileNames); + if (avatarFileNames.length > 0){ + await importCharactersTags(avatarFileNames); + selectImportedChar(avatarFileNames[avatarFileNames.length - 1]); + } } /** @@ -8882,6 +8885,18 @@ async function importCharactersTags(avatarFileNames) { } } +/** + * Selects the given imported char + * @param {string} charId char to select + */ +function selectImportedChar(charId) { + let oldSelectedChar = null; + if (this_chid !== undefined) { + oldSelectedChar = characters[this_chid].avatar; + } + select_rm_info('char_import_no_toast', charId, oldSelectedChar); +} + /** * Imports a character from a file. * @param {File} file File to import @@ -8926,15 +8941,12 @@ async function importCharacter(file, {preserveFileName = '', importTags = false} if (data.file_name !== undefined) { $('#character_search_bar').val('').trigger('input'); - let oldSelectedChar = null; - if (this_chid !== undefined) { - oldSelectedChar = characters[this_chid].avatar; - } - - select_rm_info('char_import', data.file_name, oldSelectedChar); + toastr.success(t`Character Created: ${String(data.file_name).replace('.png', '')}`); let avatarFileName = `${data.file_name}.png`; if (importTags) { - await importCharactersTags([avatarFileName]) + await importCharactersTags([avatarFileName]); + + selectImportedChar(data.file_name); } return avatarFileName; } @@ -10822,7 +10834,11 @@ jQuery(async function () { avatarFileNames.push(avatarFileName); } } - await importCharactersTags(avatarFileNames); + + if (avatarFileNames.length > 0){ + await importCharactersTags(avatarFileNames); + selectImportedChar(avatarFileNames[avatarFileNames.length - 1]); + } }); $('#export_button').on('click', function () { From de8ecc6903724b3bed8dcee1293048dcbef7703c Mon Sep 17 00:00:00 2001 From: erew123 <35898566+erew123@users.noreply.github.com> Date: Tue, 7 Jan 2025 23:20:53 +0000 Subject: [PATCH 05/17] Correct skipping RVC on AllTalk V1 Server Silly bug that caused the extension when used with the legacy V1 AllTalk server, would cause it to skip downloading Narrator voices. Literally changed "v2" to "v1" on line 391 No other changes. --- public/scripts/extensions/tts/alltalk.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/extensions/tts/alltalk.js b/public/scripts/extensions/tts/alltalk.js index 05e3db5d8..aaad6073d 100644 --- a/public/scripts/extensions/tts/alltalk.js +++ b/public/scripts/extensions/tts/alltalk.js @@ -388,7 +388,7 @@ class AllTalkTtsProvider { } async fetchRvcVoiceObjects() { - if (this.settings.server_version !== 'v2') { + if (this.settings.server_version !== 'v1') { console.log('Skipping RVC voices fetch for V1 server'); return []; } From 5ea8309a306c997b1aefd0a44312e4dd48e2588e Mon Sep 17 00:00:00 2001 From: erew123 <35898566+erew123@users.noreply.github.com> Date: Wed, 8 Jan 2025 00:01:36 +0000 Subject: [PATCH 06/17] Change check from ! not to is == V2 --- public/scripts/extensions/tts/alltalk.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/extensions/tts/alltalk.js b/public/scripts/extensions/tts/alltalk.js index aaad6073d..593ff3eac 100644 --- a/public/scripts/extensions/tts/alltalk.js +++ b/public/scripts/extensions/tts/alltalk.js @@ -388,7 +388,7 @@ class AllTalkTtsProvider { } async fetchRvcVoiceObjects() { - if (this.settings.server_version !== 'v1') { + if (this.settings.server_version == 'v2') { console.log('Skipping RVC voices fetch for V1 server'); return []; } From a8b5f8a95f22d65c1cafaa21492aec209883c32e Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Thu, 9 Jan 2025 18:37:50 +0100 Subject: [PATCH 07/17] Add /css-var slash command - Add /css-var slash command to set CSS variables on some elements - Update gallery div id to something actually unique ("#draggable_gallery") --- public/scripts/extensions/gallery/index.js | 2 +- public/scripts/power-user.js | 89 +++++++++++++++++++ .../SlashCommandCommonEnumsProvider.js | 1 + 3 files changed, 91 insertions(+), 1 deletion(-) diff --git a/public/scripts/extensions/gallery/index.js b/public/scripts/extensions/gallery/index.js index a39634603..94b8c30c7 100644 --- a/public/scripts/extensions/gallery/index.js +++ b/public/scripts/extensions/gallery/index.js @@ -277,7 +277,7 @@ function makeMovable(id = 'gallery') { const newElement = $(template); newElement.css('background-color', 'var(--SmartThemeBlurTintColor)'); newElement.attr('forChar', id); - newElement.attr('id', `${id}`); + newElement.attr('id', `draggable_${id}`); newElement.find('.drag-grabber').attr('id', `${id}header`); newElement.find('.dragTitle').text('Image Gallery'); //add a div for the gallery diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index 1e69d3608..c25f275c7 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -3962,6 +3962,95 @@ $(document).ready(() => { `, })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'css-var', + /** @param {{to: string, varname: string }} args @param {string} value @returns {string} */ + callback: (args, value) => { + // Map enum to target selector + const targetSelector = { + chat: '#chat', + background: '#bg1', + gallery: '#draggable_gallery', + zoomedAvatar: 'div.zoomed_avatar', + }[args.to || 'chat']; + + if (!targetSelector) { + toastr.error(`Invalid target: ${args.to}`); + return; + } + + if (!args.varname) { + toastr.error('CSS variable name is required'); + return; + } + if (!args.varname.startsWith('--')) { + toastr.error('CSS variable names must start with "--"'); + return; + } + + const elements = document.querySelectorAll(targetSelector); + if (elements.length === 0) { + toastr.error(`No elements found for ${args.to ?? 'chat'} with selector "${targetSelector}"`); + return; + } + + elements.forEach(element => { + element.style.setProperty(args.varname, value); + }); + + console.info(`Set CSS variable "${args.varname}" to "${value}" on "${targetSelector}"`); + }, + namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'varname', + description: 'CSS variable name (starting with double dashes)', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + }), + SlashCommandNamedArgument.fromProps({ + name: 'to', + description: 'The target element to which the CSS variable will be applied', + typeList: [ARGUMENT_TYPE.STRING], + enumList: [ + new SlashCommandEnumValue('chat', null, enumTypes.enum, enumIcons.message), + new SlashCommandEnumValue('background', null, enumTypes.enum, enumIcons.image), + new SlashCommandEnumValue('zoomedAvatar', null, enumTypes.enum, enumIcons.character), + new SlashCommandEnumValue('gallery', null, enumTypes.enum, enumIcons.image), + ], + defaultValue: 'chat', + }), + ], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'CSS variable value', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + }), + ], + helpString: ` +
+ Sets a CSS variable to a specified value on a target element. +
+ Only setting of variable names is supported. They have to be prefixed with double dashes ("--exampleVar"). + Setting actual CSS properties is not supported. Custom CSS in the theme settings can be used for that. +

+ This value will be gone after a page reload! +
+
+ Example: +
    +
  • +
    /css-var varname="--SmartThemeBodyColor" #ff0000
    + Sets the text color of the chat to red +
  • +
  • +
    /css-var to=zoomedAvatar varname="--SmartThemeBlurStrength" 0
    + Remove the blur from the zoomed avatar +
  • +
+
+ `, + })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'movingui', callback: setmovingUIPreset, diff --git a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js index b96b83a53..9003cb754 100644 --- a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js +++ b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js @@ -37,6 +37,7 @@ export const enumIcons = { voice: '🎤', server: '🖥️', popup: '🗔', + image: '🖼️', true: '✔️', false: '❌', From ba73d278ae4668774f44736489813b14e3914e83 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Thu, 9 Jan 2025 19:16:48 +0100 Subject: [PATCH 08/17] Allow zoomed in images be closed by click outside - Add event listener to close image enlarge popup when clicked outside of image or description - Change img div to not be width:100% by default, was useless and just confusing when you could click next to the image and it zoomed in --- public/scripts/chats.js | 16 +++++++++++++--- public/style.css | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/public/scripts/chats.js b/public/scripts/chats.js index f1da93158..b60778141 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -585,10 +585,12 @@ async function enlargeMessageImage() { const imgHolder = document.createElement('div'); imgHolder.classList.add('img_enlarged_holder'); imgHolder.append(img); - const imgContainer = $('
'); + const imgContainer = $('
'); imgContainer.prepend(imgHolder); imgContainer.addClass('img_enlarged_container'); - imgContainer.find('code').addClass('txt').text(title); + + const codeTitle = imgContainer.find('.img_enlarged_title'); + codeTitle.addClass('txt').text(title); const titleEmpty = !title || title.trim().length === 0; imgContainer.find('pre').toggle(!titleEmpty); addCopyToCodeBlocks(imgContainer); @@ -598,9 +600,17 @@ async function enlargeMessageImage() { popup.dlg.style.width = 'unset'; popup.dlg.style.height = 'unset'; - img.addEventListener('click', () => { + img.addEventListener('click', event => { const shouldZoom = !img.classList.contains('zoomed'); img.classList.toggle('zoomed', shouldZoom); + event.stopPropagation(); + }); + codeTitle[0]?.addEventListener('click', event => { + event.stopPropagation(); + }); + + popup.dlg.addEventListener('click', event => { + popup.completeCancelled(); }); await popup.show(); diff --git a/public/style.css b/public/style.css index ce076f915..331696654 100644 --- a/public/style.css +++ b/public/style.css @@ -4799,7 +4799,7 @@ body:not(.sd) .mes_img_swipes { .img_enlarged { object-fit: contain; - width: 100%; + max-width: 100%; height: 100%; cursor: zoom-in } From 9be04fd69f61dc52a9024e611fb3b23cee09eaa2 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 9 Jan 2025 20:58:36 +0200 Subject: [PATCH 09/17] Fix tag structure --- public/scripts/chats.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/chats.js b/public/scripts/chats.js index b60778141..fbf7af3fc 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -585,7 +585,7 @@ async function enlargeMessageImage() { const imgHolder = document.createElement('div'); imgHolder.classList.add('img_enlarged_holder'); imgHolder.append(img); - const imgContainer = $('
'); + const imgContainer = $('
'); imgContainer.prepend(imgHolder); imgContainer.addClass('img_enlarged_container'); From 0a03793d7b59d5e9e306422c4a7bf110483a6efc Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 9 Jan 2025 21:10:12 +0200 Subject: [PATCH 10/17] Add /chat-render and /chat-reload commands --- public/script.js | 12 ++++++++---- public/scripts/slash-commands.js | 22 ++++++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/public/script.js b/public/script.js index 13b938f31..7a32c65dd 100644 --- a/public/script.js +++ b/public/script.js @@ -167,6 +167,7 @@ import { flashHighlight, isTrueBoolean, toggleDrawer, + isElementInViewport, } from './scripts/utils.js'; import { debounce_timeout } from './scripts/constants.js'; @@ -1827,10 +1828,10 @@ export async function replaceCurrentChat() { } } -export function showMoreMessages() { +export function showMoreMessages(messagesToLoad = null) { const firstDisplayedMesId = $('#chat').children('.mes').first().attr('mesid'); let messageId = Number(firstDisplayedMesId); - let count = power_user.chat_truncation || Number.MAX_SAFE_INTEGER; + let count = messagesToLoad || power_user.chat_truncation || Number.MAX_SAFE_INTEGER; // If there are no messages displayed, or the message somehow has no mesid, we default to one higher than last message id, // so the first "new" message being shown will be the last available message @@ -1840,6 +1841,7 @@ export function showMoreMessages() { console.debug('Inserting messages before', messageId, 'count', count, 'chat length', chat.length); const prevHeight = $('#chat').prop('scrollHeight'); + const isButtonInView = isElementInViewport($('#show_more_messages')[0]); while (messageId > 0 && count > 0) { let newMessageId = messageId - 1; @@ -1852,8 +1854,10 @@ export function showMoreMessages() { $('#show_more_messages').remove(); } - const newHeight = $('#chat').prop('scrollHeight'); - $('#chat').scrollTop(newHeight - prevHeight); + if (isButtonInView) { + const newHeight = $('#chat').prop('scrollHeight'); + $('#chat').scrollTop(newHeight - prevHeight); + } } export async function printMessages() { diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index f10e89d0c..ed7e6088d 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -39,6 +39,7 @@ import { setCharacterName, setExtensionPrompt, setUserName, + showMoreMessages, stopGeneration, substituteParams, system_avatar, @@ -1964,6 +1965,27 @@ export function initDefaultSlashCommands() { returns: ARGUMENT_TYPE.BOOLEAN, helpString: 'Returns true if the current device is a mobile device, false otherwise. Equivalent to {{isMobile}} macro.', })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'chat-render', + helpString: 'Renders a specified number of messages into the chat window. Displays all messages if no argument is provided.', + callback: (_, number) => { + showMoreMessages(number && !isNaN(Number(number)) ? Number(number) : Number.MAX_SAFE_INTEGER); + return ''; + }, + unnamedArgumentList: [ + new SlashCommandArgument( + 'number of messages', [ARGUMENT_TYPE.NUMBER], false, + ), + ], + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'chat-reload', + helpString: 'Reloads the current chat.', + callback: async () => { + await reloadCurrentChat(); + return ''; + }, + })); registerVariableCommands(); } From a585f3abcc26c81f24bb3e5eb0c1703ee9910052 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 9 Jan 2025 21:12:34 +0200 Subject: [PATCH 11/17] Add {{firstDisplayedMessageId}} macro --- public/scripts/macros.js | 16 ++++++++++++++++ public/scripts/templates/macros.html | 1 + 2 files changed, 17 insertions(+) diff --git a/public/scripts/macros.js b/public/scripts/macros.js index cbc38ded4..7ce1a373c 100644 --- a/public/scripts/macros.js +++ b/public/scripts/macros.js @@ -211,6 +211,21 @@ function getFirstIncludedMessageId() { return null; } +/** + * Returns the ID of the first displayed message in the chat. + * + * @returns {number|null} The ID of the first displayed message + */ +function getFirstDisplayedMessageId() { + const mesId = Number(document.querySelector('#chat .mes')?.getAttribute('mesid')); + + if (!isNaN(mesId) && mesId >= 0) { + return mesId; + } + + return null; +} + /** * Returns the last message in the chat * @@ -467,6 +482,7 @@ export function evaluateMacros(content, env, postProcessFn) { { regex: /{{lastUserMessage}}/gi, replace: () => getLastUserMessage() }, { regex: /{{lastCharMessage}}/gi, replace: () => getLastCharMessage() }, { regex: /{{firstIncludedMessageId}}/gi, replace: () => String(getFirstIncludedMessageId() ?? '') }, + { regex: /{{firstDisplayedMessageId}}/gi, replace: () => String(getFirstDisplayedMessageId() ?? '') }, { regex: /{{lastSwipeId}}/gi, replace: () => String(getLastSwipeId() ?? '') }, { regex: /{{currentSwipeId}}/gi, replace: () => String(getCurrentSwipeId() ?? '') }, { regex: /{{reverse:(.+?)}}/gi, replace: (_, str) => Array.from(str).reverse().join('') }, diff --git a/public/scripts/templates/macros.html b/public/scripts/templates/macros.html index 7490596d3..97740073f 100644 --- a/public/scripts/templates/macros.html +++ b/public/scripts/templates/macros.html @@ -29,6 +29,7 @@
  • {{lastCharMessage}}the text of the latest character chat message.
  • {{lastMessageId}}index # of the latest chat message. Useful for slash command batching.
  • {{firstIncludedMessageId}}the ID of the first message included in the context. Requires generation to be ran at least once in the current session.
  • +
  • {{firstDisplayedMessageId}}the ID of the first message loaded into the visible chat.
  • {{currentSwipeId}}the 1-based ID of the current swipe in the last chat message. Empty string if the last message is user or prompt-hidden.
  • {{lastSwipeId}}the number of swipes in the last chat message. Empty string if the last message is user or prompt-hidden.
  • {{reverse:(content)}}reverses the content of the macro.
  • From b60458863849fa64ec6e905d60bc63a8b782511c Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 9 Jan 2025 21:15:34 +0200 Subject: [PATCH 12/17] Add scroll option to /chat-render command --- public/scripts/slash-commands.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index ed7e6088d..33725bf2d 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -1968,10 +1968,22 @@ export function initDefaultSlashCommands() { SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'chat-render', helpString: 'Renders a specified number of messages into the chat window. Displays all messages if no argument is provided.', - callback: (_, number) => { + callback: (args, number) => { showMoreMessages(number && !isNaN(Number(number)) ? Number(number) : Number.MAX_SAFE_INTEGER); + if (isTrueBoolean(String(args?.scroll ?? ''))) { + $('#chat').scrollTop(0); + } return ''; }, + namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'scroll', + description: 'scroll to the top after rendering', + typeList: [ARGUMENT_TYPE.BOOLEAN], + defaultValue: 'false', + enumList: commonEnumProviders.boolean('trueFalse')(), + }), + ], unnamedArgumentList: [ new SlashCommandArgument( 'number of messages', [ARGUMENT_TYPE.NUMBER], false, From 7a2276c176bc7d99f0ab8ebb0c05d03d9d4c3984 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Thu, 9 Jan 2025 21:23:18 +0100 Subject: [PATCH 13/17] Fix {{firstIncludedMessageId}} to not rely on DOM Up until now, the {{firstIncludedMessageId}} macro relied on searching the DOM for the actual CSS class applied for the context line. Bad design, as the actual message was maybe not display, with display X messages enabled. - Use setInContextMessages() that sets the context line on generation to also update a chat metadata field - Utilize that field inside the macro - Update docs to clarify that this will only show the mesid that was relevant during last generation --- public/script.js | 10 +++++++--- public/scripts/macros.js | 8 +------- public/scripts/templates/macros.html | 2 +- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/public/script.js b/public/script.js index 7a32c65dd..2bd680cd0 100644 --- a/public/script.js +++ b/public/script.js @@ -5429,20 +5429,24 @@ async function promptItemize(itemizedPrompts, requestedMesId) { await popup.show(); } -function setInContextMessages(lastmsg, type) { +function setInContextMessages(msgInContextCount, type) { $('#chat .mes').removeClass('lastInContext'); if (type === 'swipe' || type === 'regenerate' || type === 'continue') { - lastmsg++; + msgInContextCount++; } - const lastMessageBlock = $('#chat .mes:not([is_system="true"])').eq(-lastmsg); + const lastMessageBlock = $('#chat .mes:not([is_system="true"])').eq(-msgInContextCount); lastMessageBlock.addClass('lastInContext'); if (lastMessageBlock.length === 0) { const firstMessageId = getFirstDisplayedMessageId(); $(`#chat .mes[mesid="${firstMessageId}"`).addClass('lastInContext'); } + + const lastMessageId = Math.max(0, chat.length - msgInContextCount); + chat_metadata['lastInContextMessageId'] = lastMessageId; + saveMetadataDebounced(); } /** diff --git a/public/scripts/macros.js b/public/scripts/macros.js index 7ce1a373c..3464b7a64 100644 --- a/public/scripts/macros.js +++ b/public/scripts/macros.js @@ -202,13 +202,7 @@ export function getLastMessageId({ exclude_swipe_in_propress = true, filter = nu * @returns {number|null} The ID of the first message in the context */ function getFirstIncludedMessageId() { - const index = Number(document.querySelector('.lastInContext')?.getAttribute('mesid')); - - if (!isNaN(index) && index >= 0) { - return index; - } - - return null; + return chat_metadata['lastInContextMessageId']; } /** diff --git a/public/scripts/templates/macros.html b/public/scripts/templates/macros.html index 97740073f..50ca3e5fe 100644 --- a/public/scripts/templates/macros.html +++ b/public/scripts/templates/macros.html @@ -28,7 +28,7 @@
  • {{lastUserMessage}}the text of the latest user chat message.
  • {{lastCharMessage}}the text of the latest character chat message.
  • {{lastMessageId}}index # of the latest chat message. Useful for slash command batching.
  • -
  • {{firstIncludedMessageId}}the ID of the first message included in the context. Requires generation to be ran at least once in the current session.
  • +
  • {{firstIncludedMessageId}}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.
  • {{firstDisplayedMessageId}}the ID of the first message loaded into the visible chat.
  • {{currentSwipeId}}the 1-based ID of the current swipe in the last chat message. Empty string if the last message is user or prompt-hidden.
  • {{lastSwipeId}}the number of swipes in the last chat message. Empty string if the last message is user or prompt-hidden.
  • From f869b26664516c6a67068c69d3a765c3c5c817b2 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 9 Jan 2025 22:32:15 +0200 Subject: [PATCH 14/17] Ensure unique selectors for loaded extension files --- public/scripts/extensions.js | 12 +++++++----- public/scripts/utils.js | 10 ++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index 307a50721..0c3dbfa2e 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -4,7 +4,7 @@ import { eventSource, event_types, saveSettings, saveSettingsDebounced, getReque import { showLoader } from './loader.js'; import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; import { renderTemplate, renderTemplateAsync } from './templates.js'; -import { delay, isSubsetOf, setValueByPath } from './utils.js'; +import { delay, isSubsetOf, sanitizeSelector, setValueByPath } from './utils.js'; import { getContext } from './st-context.js'; import { isAdmin } from './user.js'; import { t } from './i18n.js'; @@ -509,10 +509,11 @@ function addExtensionStyle(name, manifest) { return new Promise((resolve, reject) => { const url = `/scripts/extensions/${name}/${manifest.css}`; + const id = sanitizeSelector(`${name}-css`); - if ($(`link[id="${name}"]`).length === 0) { + if ($(`link[id="${id}"]`).length === 0) { const link = document.createElement('link'); - link.id = name; + link.id = id; link.rel = 'stylesheet'; link.type = 'text/css'; link.href = url; @@ -540,11 +541,12 @@ function addExtensionScript(name, manifest) { return new Promise((resolve, reject) => { const url = `/scripts/extensions/${name}/${manifest.js}`; + const id = sanitizeSelector(`${name}-js`); let ready = false; - if ($(`script[id="${name}"]`).length === 0) { + if ($(`script[id="${id}"]`).length === 0) { const script = document.createElement('script'); - script.id = name; + script.id = id; script.type = 'module'; script.src = url; script.async = true; diff --git a/public/scripts/utils.js b/public/scripts/utils.js index eb944b586..60b49797a 100644 --- a/public/scripts/utils.js +++ b/public/scripts/utils.js @@ -67,6 +67,16 @@ export function escapeHtml(str) { return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } +/** + * Make string safe for use as a CSS selector. + * @param {string} str String to sanitize + * @param {string} replacement Replacement for invalid characters + * @returns {string} Sanitized string + */ +export function sanitizeSelector(str, replacement = '_') { + return String(str).replace(/[^a-z0-9_-]/ig, replacement); +} + export function isValidUrl(value) { try { new URL(value); From efeedc12742154ad3d92189b0fde6e7b3c46f7ff Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 9 Jan 2025 22:39:40 +0200 Subject: [PATCH 15/17] Revert gallery selector changes --- public/scripts/extensions/gallery/index.js | 2 +- public/scripts/power-user.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/scripts/extensions/gallery/index.js b/public/scripts/extensions/gallery/index.js index 94b8c30c7..c34ea56e3 100644 --- a/public/scripts/extensions/gallery/index.js +++ b/public/scripts/extensions/gallery/index.js @@ -277,7 +277,7 @@ function makeMovable(id = 'gallery') { const newElement = $(template); newElement.css('background-color', 'var(--SmartThemeBlurTintColor)'); newElement.attr('forChar', id); - newElement.attr('id', `draggable_${id}`); + newElement.attr('id', id); newElement.find('.drag-grabber').attr('id', `${id}header`); newElement.find('.dragTitle').text('Image Gallery'); //add a div for the gallery diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index c25f275c7..b3b7f41d5 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -3970,7 +3970,7 @@ $(document).ready(() => { const targetSelector = { chat: '#chat', background: '#bg1', - gallery: '#draggable_gallery', + gallery: '#gallery', zoomedAvatar: 'div.zoomed_avatar', }[args.to || 'chat']; From 2ab59f5a7a157a98ab7bcc83b319b6e740f84f3e Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 9 Jan 2025 22:57:43 +0200 Subject: [PATCH 16/17] chore: reformat new code --- public/script.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/public/script.js b/public/script.js index 7029de053..bbae531d7 100644 --- a/public/script.js +++ b/public/script.js @@ -8856,8 +8856,8 @@ export async function processDroppedFiles(files, data = new Map()) { const extension = file.name.split('.').pop().toLowerCase(); if (allowedMimeTypes.some(x => file.type.startsWith(x)) || allowedExtensions.includes(extension)) { const preservedName = data instanceof Map && data.get(file); - const avatarFileName = await importCharacter(file, {preserveFileName: preservedName}); - if (avatarFileName !== undefined){ + const avatarFileName = await importCharacter(file, { preserveFileName: preservedName }); + if (avatarFileName !== undefined) { avatarFileNames.push(avatarFileName); } } else { @@ -8865,7 +8865,7 @@ export async function processDroppedFiles(files, data = new Map()) { } } - if (avatarFileNames.length > 0){ + if (avatarFileNames.length > 0) { await importCharactersTags(avatarFileNames); selectImportedChar(avatarFileNames[avatarFileNames.length - 1]); } @@ -8905,7 +8905,7 @@ function selectImportedChar(charId) { * @param {Boolean} [options.importTags=false] Whether to import tags * @returns {Promise} */ -async function importCharacter(file, {preserveFileName = '', importTags = false} = {}) { +async function importCharacter(file, { preserveFileName = '', importTags = false } = {}) { if (is_group_generating || is_send_press) { toastr.error(t`Cannot import characters while generating. Stop the request and try again.`, t`Import aborted`); throw new Error('Cannot import character while generating'); @@ -10830,12 +10830,12 @@ jQuery(async function () { const avatarFileNames = []; for (const file of e.target.files) { const avatarFileName = await importCharacter(file); - if (avatarFileName !== undefined){ + if (avatarFileName !== undefined) { avatarFileNames.push(avatarFileName); } } - if (avatarFileNames.length > 0){ + if (avatarFileNames.length > 0) { await importCharactersTags(avatarFileNames); selectImportedChar(avatarFileNames[avatarFileNames.length - 1]); } From e33c3771f511965a3dd9109977515ca46b9fe804 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Fri, 10 Jan 2025 01:27:02 +0100 Subject: [PATCH 17/17] No metadata save --- public/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/script.js b/public/script.js index 2bd680cd0..03dfd6f4c 100644 --- a/public/script.js +++ b/public/script.js @@ -5444,9 +5444,9 @@ function setInContextMessages(msgInContextCount, type) { $(`#chat .mes[mesid="${firstMessageId}"`).addClass('lastInContext'); } + // Update last id to chat. No metadata save on purpose, gets hopefully saved via another call const lastMessageId = Math.max(0, chat.length - msgInContextCount); chat_metadata['lastInContextMessageId'] = lastMessageId; - saveMetadataDebounced(); } /**