Merge branch 'SillyTavern:staging' into staging

This commit is contained in:
Rendal 2025-01-10 16:32:03 +01:00 committed by GitHub
commit 1528e2afed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 245 additions and 39 deletions

View File

@ -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() {
@ -5425,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');
}
// 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;
}
/**
@ -7301,7 +7309,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 });
@ -8853,24 +8861,61 @@ 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, { preserveFileName: preservedName });
if (avatarFileName !== undefined) {
avatarFileNames.push(avatarFileName);
}
} else {
toastr.warning(t`Unsupported file type: ` + file.name);
}
}
if (avatarFileNames.length > 0) {
await importCharactersTags(avatarFileNames);
selectImportedChar(avatarFileNames[avatarFileNames.length - 1]);
}
}
/**
* Imports tags for the given characters
* @param {string[]} avatarFileNames character avatar filenames whose tags are to import
*/
async function importCharactersTags(avatarFileNames) {
await getCharacters();
for (let i = 0; i < avatarFileNames.length; i++) {
if (power_user.tag_import_setting !== tag_import_setting.NONE) {
const importedCharacter = characters.find(character => character.avatar === avatarFileNames[i]);
await importTags(importedCharacter);
}
}
}
/**
* 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
* @param {string?} preserveFileName Whether to preserve original file name
* @returns {Promise<void>}
* @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<string>}
*/
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');
@ -8906,19 +8951,14 @@ async function importCharacter(file, preserveFileName = '') {
if (data.file_name !== undefined) {
$('#character_search_bar').val('').trigger('input');
let oldSelectedChar = null;
if (this_chid !== undefined) {
oldSelectedChar = characters[this_chid].avatar;
}
toastr.success(t`Character Created: ${String(data.file_name).replace('.png', '')}`);
let avatarFileName = `${data.file_name}.png`;
if (importTags) {
await importCharactersTags([avatarFileName]);
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);
selectImportedChar(data.file_name);
}
return avatarFileName;
}
}
@ -10797,8 +10837,17 @@ 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);
}
}
if (avatarFileNames.length > 0) {
await importCharactersTags(avatarFileNames);
selectImportedChar(avatarFileNames[avatarFileNames.length - 1]);
}
});

View File

@ -585,10 +585,12 @@ async function enlargeMessageImage() {
const imgHolder = document.createElement('div');
imgHolder.classList.add('img_enlarged_holder');
imgHolder.append(img);
const imgContainer = $('<div><pre><code></code></pre></div>');
const imgContainer = $('<div><pre><code class="img_enlarged_title"></code></pre></div>');
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();

View File

@ -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;

View File

@ -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', id);
newElement.find('.drag-grabber').attr('id', `${id}header`);
newElement.find('.dragTitle').text('Image Gallery');
//add a div for the gallery

View File

@ -388,7 +388,7 @@ class AllTalkTtsProvider {
}
async fetchRvcVoiceObjects() {
if (this.settings.server_version !== 'v2') {
if (this.settings.server_version == 'v2') {
console.log('Skipping RVC voices fetch for V1 server');
return [];
}

View File

@ -202,10 +202,19 @@ 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'));
return chat_metadata['lastInContextMessageId'];
}
if (!isNaN(index) && index >= 0) {
return index;
/**
* 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;
@ -467,6 +476,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('') },

View File

@ -3962,6 +3962,95 @@ $(document).ready(() => {
</div>
`,
}));
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: '#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: `
<div>
Sets a CSS variable to a specified value on a target element.
<br />
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.
<br /><br />
<b>This value will be gone after a page reload!</b>
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code>/css-var varname="--SmartThemeBodyColor" #ff0000</code></pre>
Sets the text color of the chat to red
</li>
<li>
<pre><code>/css-var to=zoomedAvatar varname="--SmartThemeBlurStrength" 0</code></pre>
Remove the blur from the zoomed avatar
</li>
</ul>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'movingui',
callback: setmovingUIPreset,

View File

@ -39,6 +39,7 @@ import {
setCharacterName,
setExtensionPrompt,
setUserName,
showMoreMessages,
stopGeneration,
substituteParams,
system_avatar,
@ -1964,6 +1965,39 @@ export function initDefaultSlashCommands() {
returns: ARGUMENT_TYPE.BOOLEAN,
helpString: 'Returns true if the current device is a mobile device, false otherwise. Equivalent to <code>{{isMobile}}</code> 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: (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,
),
],
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'chat-reload',
helpString: 'Reloads the current chat.',
callback: async () => {
await reloadCurrentChat();
return '';
},
}));
registerVariableCommands();
}

View File

@ -37,6 +37,7 @@ export const enumIcons = {
voice: '🎤',
server: '🖥️',
popup: '🗔',
image: '🖼️',
true: '✔️',
false: '❌',

View File

@ -28,7 +28,8 @@
<li><tt>&lcub;&lcub;lastUserMessage&rcub;&rcub;</tt> <span data-i18n="help_macros_lastUser">the text of the latest user chat message.</span></li>
<li><tt>&lcub;&lcub;lastCharMessage&rcub;&rcub;</tt> <span data-i18n="help_macros_lastChar">the text of the latest character chat message.</span></li>
<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>
<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>
<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>
<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>
<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>
<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>
<li><tt>&lcub;&lcub;reverse:(content)&rcub;&rcub;</tt> <span data-i18n="help_macros_reverse">reverses the content of the macro.</span></li>

View File

@ -67,6 +67,16 @@ export function escapeHtml(str) {
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
/**
* 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);

View File

@ -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
}