Merge branch 'SillyTavern:staging' into staging

1528e2afede858495426d8e97acb9c0aa7804dae

Rendal <23568313+sakhavhyand@users.noreply.github.com>

Signed
12 files changed, +243 -37Showing whitespace changes
public/script.js+71 -22
@@ -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';
171import { debounce_timeout } from './scripts/constants.js';172import { debounce_timeout } from './scripts/constants.js';
172173
@@ -1827,10 +1828,10 @@ export async function replaceCurrentChat() {
1827 }1828 }
1828}1829}
18291830
1830export function showMoreMessages() {1831export 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;
18341835
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 message1837 // so the first "new" message being shown will be the last available message
@@ -1840,6 +1841,7 @@ export function showMoreMessages() {
18401841
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]);
18431845
1844 while (messageId > 0 && count > 0) {1846 while (messageId > 0 && count > 0) {
1845 let newMessageId = messageId - 1;1847 let newMessageId = messageId - 1;
@@ -1852,9 +1854,11 @@ export function showMoreMessages() {
1852 $('#show_more_messages').remove();1854 $('#show_more_messages').remove();
1853 }1855 }
18541856
1857 if (isButtonInView) {
1855 const newHeight = $('#chat').prop('scrollHeight');1858 const newHeight = $('#chat').prop('scrollHeight');
1856 $('#chat').scrollTop(newHeight - prevHeight);1859 $('#chat').scrollTop(newHeight - prevHeight);
1857 }1860 }
1861}
18581862
1859export async function printMessages() {1863export async function printMessages() {
1860 let startIndex = 0;1864 let startIndex = 0;
@@ -5425,20 +5429,24 @@ async function promptItemize(itemizedPrompts, requestedMesId) {
5425 await popup.show();5429 await popup.show();
5426}5430}
54275431
5428function setInContextMessages(lastmsg, type) {5432function setInContextMessages(msgInContextCount, type) {
5429 $('#chat .mes').removeClass('lastInContext');5433 $('#chat .mes').removeClass('lastInContext');
54305434
5431 if (type === 'swipe' || type === 'regenerate' || type === 'continue') {5435 if (type === 'swipe' || type === 'regenerate' || type === 'continue') {
5432 lastmsg++;5436 msgInContextCount++;
5433 }5437 }
54345438
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');
54375441
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}
54435451
5444/**5452/**
@@ -7301,7 +7309,7 @@ export function select_rm_info(type, charId, previousCharId = null) {
7301 // Set a timeout so multiple flashes don't overlap7309 // 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 located7313 // 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 ];
88558863
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 */
8888async 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 */
8902function 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}
88668909
8867/**8910/**
8868 * Imports a character from a file.8911 * Imports a character from a file.
8869 * @param {File} file File to import8912 * @param {File} file File to import
8870 * @param {string?} preserveFileName Whether to preserve original file name8913 * @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 */
8873async function importCharacter(file, preserveFileName = '') {8918async 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');
89088953
8909 let oldSelectedChar = null;8954 toastr.success(t`Character Created: ${String(data.file_name).replace('.png', '')}`);
8910 if (this_chid !== undefined) {
8911 oldSelectedChar = characters[this_chid].avatar;
8912 }
8913
8914 await getCharacters();
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`;8955 let avatarFileName = `${data.file_name}.png`;
8919 let importedCharacter = currentContext.characters.find(character => character.avatar === avatarFileName);8956 if (importTags) {
8920 await importTags(importedCharacter);8957 await importCharactersTags([avatarFileName]);
8958
8959 selectImportedChar(data.file_name);
8921 }8960 }
8961 return avatarFileName;
8922 }8962 }
8923}8963}
89248964
@@ -10797,8 +10837,17 @@ jQuery(async function () {
10797 return;10837 return;
10798 }10838 }
1079910839
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 });
1080410853
public/scripts/chats.js+13 -3
@@ -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';
600602
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 });
605615
606 await popup.show();616 await popup.show();
public/scripts/extensions.js+7 -5
@@ -4,7 +4,7 @@ import { eventSource, event_types, saveSettings, saveSettingsDebounced, getReque
4import { showLoader } from './loader.js';4import { showLoader } from './loader.js';
5import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';5import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
6import { renderTemplate, renderTemplateAsync } from './templates.js';6import { renderTemplate, renderTemplateAsync } from './templates.js';
7import { delay, isSubsetOf, setValueByPath } from './utils.js';7import { delay, isSubsetOf, sanitizeSelector, setValueByPath } from './utils.js';
8import { getContext } from './st-context.js';8import { getContext } from './st-context.js';
9import { isAdmin } from './user.js';9import { isAdmin } from './user.js';
10import { t } from './i18n.js';10import { t } from './i18n.js';
@@ -509,10 +509,11 @@ function addExtensionStyle(name, manifest) {
509509
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`);
512513
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) {
540541
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;
544546
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;
public/scripts/extensions/gallery/index.js+1 -1
@@ -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 gallery283 //add a div for the gallery
public/scripts/extensions/tts/alltalk.js+1 -1
@@ -388,7 +388,7 @@ class AllTalkTtsProvider {
388 }388 }
389389
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 }
public/scripts/macros.js+13 -3
@@ -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 context202 * @returns {number|null} The ID of the first message in the context
203 */203 */
204function getFirstIncludedMessageId() {204function 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 */
213function getFirstDisplayedMessageId() {
214 const mesId = Number(document.querySelector('#chat .mes')?.getAttribute('mesid'));
206215
207 if (!isNaN(index) && index >= 0) {216 if (!isNaN(mesId) && mesId >= 0) {
208 return index;217 return mesId;
209 }218 }
210219
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('') },
public/scripts/power-user.js+89 -0
@@ -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: [
public/scripts/slash-commands.js+34 -0
@@ -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 }));
19672001
1968 registerVariableCommands();2002 registerVariableCommands();
1969}2003}
public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js+1 -0
@@ -37,6 +37,7 @@ export const enumIcons = {
37 voice: '🎀',37 voice: '🎀',
38 server: 'πŸ–₯️',38 server: 'πŸ–₯️',
39 popup: 'πŸ—”',39 popup: 'πŸ—”',
40 image: 'πŸ–ΌοΈ',
4041
41 true: 'βœ”οΈ',42 true: 'βœ”οΈ',
42 false: '❌',43 false: '❌',
public/scripts/templates/macros.html+2 -1
@@ -28,7 +28,8 @@
28 <li><tt>&lcub;&lcub;lastUserMessage&rcub;&rcub;</tt> – <span data-i18n="help_macros_lastUser">the text of the latest user chat message.</span></li>28 <li><tt>&lcub;&lcub;lastUserMessage&rcub;&rcub;</tt> – <span data-i18n="help_macros_lastUser">the text of the latest user chat message.</span></li>
29 <li><tt>&lcub;&lcub;lastCharMessage&rcub;&rcub;</tt> – <span data-i18n="help_macros_lastChar">the text of the latest character chat message.</span></li>29 <li><tt>&lcub;&lcub;lastCharMessage&rcub;&rcub;</tt> – <span data-i18n="help_macros_lastChar">the text of the latest character chat message.</span></li>
30 <li><tt>&lcub;&lcub;lastMessageId&rcub;&rcub;</tt> – <span data-i18n="help_macros_21">index # of the latest chat message. Useful for slash command batching.</span></li>30 <li><tt>&lcub;&lcub;lastMessageId&rcub;&rcub;</tt> – <span data-i18n="help_macros_21">index # of the latest chat message. Useful for slash command batching.</span></li>
31 <li><tt>&lcub;&lcub;firstIncludedMessageId&rcub;&rcub;</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>&lcub;&lcub;firstIncludedMessageId&rcub;&rcub;</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>&lcub;&lcub;firstDisplayedMessageId&rcub;&rcub;</tt> – <span data-i18n="help_macros_firstDisplayedMessageId">the ID of the first message loaded into the visible chat.</span></li>
32 <li><tt>&lcub;&lcub;currentSwipeId&rcub;&rcub;</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>&lcub;&lcub;currentSwipeId&rcub;&rcub;</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>&lcub;&lcub;lastSwipeId&rcub;&rcub;</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>&lcub;&lcub;lastSwipeId&rcub;&rcub;</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>&lcub;&lcub;reverse:(content)&rcub;&rcub;</tt> – <span data-i18n="help_macros_reverse">reverses the content of the macro.</span></li>35 <li><tt>&lcub;&lcub;reverse:(content)&rcub;&rcub;</tt> – <span data-i18n="help_macros_reverse">reverses the content of the macro.</span></li>
public/scripts/utils.js+10 -0
@@ -67,6 +67,16 @@ export function escapeHtml(str) {
67 return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');67 return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
68}68}
6969
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 */
76export function sanitizeSelector(str, replacement = '_') {
77 return String(str).replace(/[^a-z0-9_-]/ig, replacement);
78}
79
70export function isValidUrl(value) {80export function isValidUrl(value) {
71 try {81 try {
72 new URL(value);82 new URL(value);
public/style.css+1 -1
@@ -4799,7 +4799,7 @@ body:not(.sd) .mes_img_swipes {
47994799
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-in4804 cursor: zoom-in
4805}4805}