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 {
167167 flashHighlight,
168168 isTrueBoolean,
169169 toggleDrawer,
170+ isElementInViewport,
170171} from './scripts/utils.js';
171172import { debounce_timeout } from './scripts/constants.js';
172173
@@ -1827,10 +1828,10 @@ export async function replaceCurrentChat() {
18271828 }
18281829}
18291830
18301831export function showMoreMessages(messagesToLoad = null) {
18311832 const firstDisplayedMesId = $('#chat').children('.mes').first().attr('mesid');
18321833 let messageId = Number(firstDisplayedMesId);
18331834 let count = messagesToLoad || power_user.chat_truncation || Number.MAX_SAFE_INTEGER;
18341835
18351836 // If there are no messages displayed, or the message somehow has no mesid, we default to one higher than last message id,
18361837 // so the first "new" message being shown will be the last available message
@@ -1840,6 +1841,7 @@ export function showMoreMessages() {
18401841
18411842 console.debug('Inserting messages before', messageId, 'count', count, 'chat length', chat.length);
18421843 const prevHeight = $('#chat').prop('scrollHeight');
1844+ const isButtonInView = isElementInViewport($('#show_more_messages')[0]);
18431845
18441846 while (messageId > 0 && count > 0) {
18451847 let newMessageId = messageId - 1;
@@ -1852,9 +1854,11 @@ export function showMoreMessages() {
18521854 $('#show_more_messages').remove();
18531855 }
18541856
1857+ if (isButtonInView) {
18551858 const newHeight = $('#chat').prop('scrollHeight');
18561859 $('#chat').scrollTop(newHeight - prevHeight);
18571860 }
1861+}
18581862
18591863export async function printMessages() {
18601864 let startIndex = 0;
@@ -5425,20 +5429,24 @@ async function promptItemize(itemizedPrompts, requestedMesId) {
54255429 await popup.show();
54265430}
54275431
54285432function setInContextMessages(lastmsgmsgInContextCount, type) {
54295433 $('#chat .mes').removeClass('lastInContext');
54305434
54315435 if (type === 'swipe' || type === 'regenerate' || type === 'continue') {
54325436 lastmsgmsgInContextCount++;
54335437 }
54345438
54355439 const lastMessageBlock = $('#chat .mes:not([is_system="true"])').eq(-lastmsgmsgInContextCount);
54365440 lastMessageBlock.addClass('lastInContext');
54375441
54385442 if (lastMessageBlock.length === 0) {
54395443 const firstMessageId = getFirstDisplayedMessageId();
54405444 $(`#chat .mes[mesid="${firstMessageId}"`).addClass('lastInContext');
54415445 }
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;
54425450}
54435451
54445452/**
@@ -7301,7 +7309,7 @@ export function select_rm_info(type, charId, previousCharId = null) {
73017309 // Set a timeout so multiple flashes don't overlap
73027310 clearTimeout(importFlashTimeout);
73037311 importFlashTimeout = setTimeout(function () {
73047312 if (type === 'char_import' || type === 'char_create' || type === 'char_import_no_toast') {
73057313 // Find the page at which the character is located
73067314 const avatarFileName = charId;
73077315 const charData = getEntitiesList({ doFilter: true });
@@ -8853,24 +8861,61 @@ export async function processDroppedFiles(files, data = new Map()) {
88538861 'charx',
88548862 ];
88558863
8864+ const avatarFileNames = [];
88568865 for (const file of files) {
88578866 const extension = file.name.split('.').pop().toLowerCase();
88588867 if (allowedMimeTypes.some(x => file.type.startsWith(x)) || allowedExtensions.includes(extension)) {
88598868 const preservedName = data instanceof Map && data.get(file);
88608869 const avatarFileName = await importCharacter(file, { preserveFileName: preservedName });
8870+ if (avatarFileName !== undefined) {
8871+ avatarFileNames.push(avatarFileName);
8872+ }
88618873 } else {
88628874 toastr.warning(t`Unsupported file type: ` + file.name);
88638875 }
88648876 }
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);
88658908}
88668909
88678910/**
88688911 * Imports a character from a file.
88698912 * @param {File} file File to import
88708913 * @param {string?object} preserveFileName Whether to preserve original[options] file- nameOptions
8871- * @returns {Promise<void>}
8914+ * @param {string} [options.preserveFileName] Whether to preserve original file name
8915+ * @param {Boolean} [options.importTags=false] Whether to import tags
8916+ * @returns {Promise<string>}
88728917 */
88738918async function importCharacter(file, { preserveFileName = '', importTags = false } = {}) {
88748919 if (is_group_generating || is_send_press) {
88758920 toastr.error(t`Cannot import characters while generating. Stop the request and try again.`, t`Import aborted`);
88768921 throw new Error('Cannot import character while generating');
@@ -8906,19 +8951,14 @@ async function importCharacter(file, preserveFileName = '') {
89068951 if (data.file_name !== undefined) {
89078952 $('#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();
89188955 let avatarFileName = `${data.file_name}.png`;
8919- let importedCharacter = currentContext.characters.find(character => character.avatar === avatarFileName);
8956+ if (importTags) {
89208957 await importTagsimportCharactersTags(importedCharacter[avatarFileName]);
8958+
8959+ selectImportedChar(data.file_name);
89218960 }
8961+ return avatarFileName;
89228962 }
89238963}
89248964
@@ -10797,8 +10837,17 @@ jQuery(async function () {
1079710837 return;
1079810838 }
1079910839
10840+ const avatarFileNames = [];
1080010841 for (const file of e.target.files) {
1080110842 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]);
1080210851 }
1080310852 });
1080410853
public/scripts/chats.js+13 -3
@@ -585,10 +585,12 @@ async function enlargeMessageImage() {
585585 const imgHolder = document.createElement('div');
586586 imgHolder.classList.add('img_enlarged_holder');
587587 imgHolder.append(img);
588588 const imgContainer = $('<div><pre><code class="img_enlarged_title"></code></pre></div>');
589589 imgContainer.prepend(imgHolder);
590590 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);
592594 const titleEmpty = !title || title.trim().length === 0;
593595 imgContainer.find('pre').toggle(!titleEmpty);
594596 addCopyToCodeBlocks(imgContainer);
@@ -598,9 +600,17 @@ async function enlargeMessageImage() {
598600 popup.dlg.style.width = 'unset';
599601 popup.dlg.style.height = 'unset';
600602
601603 img.addEventListener('click', ()event => {
602604 const shouldZoom = !img.classList.contains('zoomed');
603605 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();
604614 });
605615
606616 await popup.show();
public/scripts/extensions.js+7 -5
@@ -4,7 +4,7 @@ import { eventSource, event_types, saveSettings, saveSettingsDebounced, getReque
44import { showLoader } from './loader.js';
55import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
66import { renderTemplate, renderTemplateAsync } from './templates.js';
77import { delay, isSubsetOf, sanitizeSelector, setValueByPath } from './utils.js';
88import { getContext } from './st-context.js';
99import { isAdmin } from './user.js';
1010import { t } from './i18n.js';
@@ -509,10 +509,11 @@ function addExtensionStyle(name, manifest) {
509509
510510 return new Promise((resolve, reject) => {
511511 const url = `/scripts/extensions/${name}/${manifest.css}`;
512+ const id = sanitizeSelector(`${name}-css`);
512513
513514 if ($(`link[id="${nameid}"]`).length === 0) {
514515 const link = document.createElement('link');
515516 link.id = nameid;
516517 link.rel = 'stylesheet';
517518 link.type = 'text/css';
518519 link.href = url;
@@ -540,11 +541,12 @@ function addExtensionScript(name, manifest) {
540541
541542 return new Promise((resolve, reject) => {
542543 const url = `/scripts/extensions/${name}/${manifest.js}`;
544+ const id = sanitizeSelector(`${name}-js`);
543545 let ready = false;
544546
545547 if ($(`script[id="${nameid}"]`).length === 0) {
546548 const script = document.createElement('script');
547549 script.id = nameid;
548550 script.type = 'module';
549551 script.src = url;
550552 script.async = true;
public/scripts/extensions/gallery/index.js+1 -1
@@ -277,7 +277,7 @@ function makeMovable(id = 'gallery') {
277277 const newElement = $(template);
278278 newElement.css('background-color', 'var(--SmartThemeBlurTintColor)');
279279 newElement.attr('forChar', id);
280280 newElement.attr('id', `${id}`);
281281 newElement.find('.drag-grabber').attr('id', `${id}header`);
282282 newElement.find('.dragTitle').text('Image Gallery');
283283 //add a div for the gallery
public/scripts/extensions/tts/alltalk.js+1 -1
@@ -388,7 +388,7 @@ class AllTalkTtsProvider {
388388 }
389389
390390 async fetchRvcVoiceObjects() {
391391 if (this.settings.server_version !== 'v2') {
392392 console.log('Skipping RVC voices fetch for V1 server');
393393 return [];
394394 }
public/scripts/macros.js+13 -3
@@ -202,10 +202,19 @@ export function getLastMessageId({ exclude_swipe_in_propress = true, filter = nu
202202 * @returns {number|null} The ID of the first message in the context
203203 */
204204function 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'));
206215
207216 if (!isNaN(indexmesId) && indexmesId >= 0) {
208217 return indexmesId;
209218 }
210219
211220 return null;
@@ -467,6 +476,7 @@ export function evaluateMacros(content, env, postProcessFn) {
467476 { regex: /{{lastUserMessage}}/gi, replace: () => getLastUserMessage() },
468477 { regex: /{{lastCharMessage}}/gi, replace: () => getLastCharMessage() },
469478 { regex: /{{firstIncludedMessageId}}/gi, replace: () => String(getFirstIncludedMessageId() ?? '') },
479+ { regex: /{{firstDisplayedMessageId}}/gi, replace: () => String(getFirstDisplayedMessageId() ?? '') },
470480 { regex: /{{lastSwipeId}}/gi, replace: () => String(getLastSwipeId() ?? '') },
471481 { regex: /{{currentSwipeId}}/gi, replace: () => String(getCurrentSwipeId() ?? '') },
472482 { regex: /{{reverse:(.+?)}}/gi, replace: (_, str) => Array.from(str).reverse().join('') },
public/scripts/power-user.js+89 -0
@@ -3963,6 +3963,95 @@ $(document).ready(() => {
39633963 `,
39643964 }));
39653965 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({
39664055 name: 'movingui',
39674056 callback: setmovingUIPreset,
39684057 unnamedArgumentList: [
public/scripts/slash-commands.js+34 -0
@@ -39,6 +39,7 @@ import {
3939 setCharacterName,
4040 setExtensionPrompt,
4141 setUserName,
42+ showMoreMessages,
4243 stopGeneration,
4344 substituteParams,
4445 system_avatar,
@@ -1964,6 +1965,39 @@ export function initDefaultSlashCommands() {
19641965 returns: ARGUMENT_TYPE.BOOLEAN,
19651966 helpString: 'Returns true if the current device is a mobile device, false otherwise. Equivalent to <code>{{isMobile}}</code> macro.',
19661967 }));
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
19682002 registerVariableCommands();
19692003}
public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js+1 -0
@@ -37,6 +37,7 @@ export const enumIcons = {
3737 voice: '🎀',
3838 server: 'πŸ–₯️',
3939 popup: 'πŸ—”',
40+ image: 'πŸ–ΌοΈ',
4041
4142 true: 'βœ”οΈ',
4243 false: '❌',
public/scripts/templates/macros.html+2 -1
@@ -28,7 +28,8 @@
2828 <li><tt>&lcub;&lcub;lastUserMessage&rcub;&rcub;</tt> – <span data-i18n="help_macros_lastUser">the text of the latest user chat message.</span></li>
2929 <li><tt>&lcub;&lcub;lastCharMessage&rcub;&rcub;</tt> – <span data-i18n="help_macros_lastChar">the text of the latest character chat message.</span></li>
3030 <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>
3131 <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 ranrun 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>
3233 <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>
3334 <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>
3435 <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) {
6767 return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
6868}
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+ */
76+export function sanitizeSelector(str, replacement = '_') {
77+ return String(str).replace(/[^a-z0-9_-]/ig, replacement);
78+}
79+
7080export function isValidUrl(value) {
7181 try {
7282 new URL(value);
public/style.css+1 -1
@@ -4799,7 +4799,7 @@ body:not(.sd) .mes_img_swipes {
47994799
48004800.img_enlarged {
48014801 object-fit: contain;
48024802 max-width: 100%;
48034803 height: 100%;
48044804 cursor: zoom-in
48054805}