mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Implement creator's note style tag preferences (#3979)
* Implement creator's note style tag preferences * Decouple external media preference from style preference * Allow explicitly empty prefixes in decodeStyleTags * Fix Copilot comments * Refactor global styles management into StylesPreference class * Refactor openAttachmentManager to return an object instead of an array * Unify header structure * Re-render characters panel on setting initial preference * Add note about classname prefixing * Rename event handler
This commit is contained in:
@@ -5451,9 +5451,10 @@
|
||||
</div>
|
||||
<hr>
|
||||
<div id="spoiler_free_desc" class="flex-container flexFlowColumn flex1 flexNoGap">
|
||||
<div id="creators_notes_div" class="title_restorable">
|
||||
<span data-i18n="Creator's Notes">Creator's Notes</span>
|
||||
<div id="creators_notes_div" class="title_restorable flexGap5">
|
||||
<span class="flex1" data-i18n="Creator's Notes">Creator's Notes</span>
|
||||
<small id="creators_note_desc_hidden" data-i18n="Character details are hidden.">Character details are hidden.</small>
|
||||
<div id="creators_note_styles_button" class="margin0 menu_button fa-solid fa-palette fa-fw" title="Allow / Forbid the use of global styles for this character." data-i18n="[title]Allow / Forbid the use of global styles for this character."></div>
|
||||
<div id="spoiler_free_desc_button" class="margin0 menu_button fa-solid fa-eye fa-fw" title="Show / Hide Description and First Message" data-i18n="[title]Show / Hide Description and First Message"></div>
|
||||
</div>
|
||||
<div id="creator_notes_spoiler" class="flex1"></div>
|
||||
|
@@ -251,7 +251,7 @@ import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_set
|
||||
import { hideLoader, showLoader } from './scripts/loader.js';
|
||||
import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js';
|
||||
import { loadFeatherlessModels, loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels, initTextGenModels, loadTabbyModels, loadGenericModels } from './scripts/textgen-models.js';
|
||||
import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId, preserveNeutralChat, restoreNeutralChat } from './scripts/chats.js';
|
||||
import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId, preserveNeutralChat, restoreNeutralChat, formatCreatorNotes, initChatUtilities } from './scripts/chats.js';
|
||||
import { getPresetManager, initPresetManager } from './scripts/preset-manager.js';
|
||||
import { evaluateMacros, getLastMessageId, initMacros } from './scripts/macros.js';
|
||||
import { currentUser, setUserControls } from './scripts/user.js';
|
||||
@@ -544,7 +544,7 @@ console.debug('Character context menu initialized', characterContextMenu);
|
||||
// Markdown converter
|
||||
export let mesForShowdownParse; //intended to be used as a context to compare showdown strings against
|
||||
/** @type {import('showdown').Converter} */
|
||||
let converter;
|
||||
export let converter;
|
||||
|
||||
// array for prompt token calculations
|
||||
console.debug('initializing Prompt Itemization Array on Startup');
|
||||
@@ -978,6 +978,7 @@ async function firstLoadInit() {
|
||||
await getClientVersion();
|
||||
await readSecretState();
|
||||
await initLocales();
|
||||
initChatUtilities();
|
||||
initDefaultSlashCommands();
|
||||
initTextGenModels();
|
||||
initOpenAI();
|
||||
@@ -2232,7 +2233,7 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san
|
||||
};
|
||||
mes = encodeStyleTags(mes);
|
||||
mes = DOMPurify.sanitize(mes, config);
|
||||
mes = decodeStyleTags(mes);
|
||||
mes = decodeStyleTags(mes, { prefix: '.mes_text ' });
|
||||
|
||||
return mes;
|
||||
}
|
||||
@@ -8248,7 +8249,7 @@ export function select_selected_character(chid, { switchMenu = true } = {}) {
|
||||
$('#description_textarea').val(characters[chid].description);
|
||||
$('#character_world').val(characters[chid].data?.extensions?.world || '');
|
||||
$('#creator_notes_textarea').val(characters[chid].data?.creator_notes || characters[chid].creatorcomment);
|
||||
$('#creator_notes_spoiler').html(DOMPurify.sanitize(converter.makeHtml(substituteParams(characters[chid].data?.creator_notes) || characters[chid].creatorcomment), { MESSAGE_SANITIZE: true }));
|
||||
$('#creator_notes_spoiler').html(formatCreatorNotes(characters[chid].data?.creator_notes || characters[chid].creatorcomment, characters[chid].avatar));
|
||||
$('#character_version_textarea').val(characters[chid].data?.character_version || '');
|
||||
$('#system_prompt_textarea').val(characters[chid].data?.system_prompt || '');
|
||||
$('#post_history_instructions_textarea').val(characters[chid].data?.post_history_instructions || '');
|
||||
@@ -8329,7 +8330,7 @@ function select_rm_create({ switchMenu = true } = {}) {
|
||||
$('#description_textarea').val(create_save.description);
|
||||
$('#character_world').val(create_save.world);
|
||||
$('#creator_notes_textarea').val(create_save.creator_notes);
|
||||
$('#creator_notes_spoiler').html(DOMPurify.sanitize(converter.makeHtml(create_save.creator_notes), { MESSAGE_SANITIZE: true }));
|
||||
$('#creator_notes_spoiler').html(formatCreatorNotes(create_save.creator_notes, ''));
|
||||
$('#post_history_instructions_textarea').val(create_save.post_history_instructions);
|
||||
$('#system_prompt_textarea').val(create_save.system_prompt);
|
||||
$('#tags_textarea').val(create_save.tags);
|
||||
|
@@ -1,6 +1,6 @@
|
||||
// Move chat functions here from script.js (eventually)
|
||||
|
||||
import { Popper, css } from '../lib.js';
|
||||
import { Popper, css, DOMPurify } from '../lib.js';
|
||||
import {
|
||||
addCopyToCodeBlocks,
|
||||
appendMediaToMessage,
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
neutralCharacterName,
|
||||
updateChatMetadata,
|
||||
system_message_types,
|
||||
converter,
|
||||
substituteParams,
|
||||
getSystemMessageByType,
|
||||
printMessages,
|
||||
clearChat,
|
||||
@@ -475,10 +477,12 @@ export function encodeStyleTags(text) {
|
||||
/**
|
||||
* Sanitizes custom style tags in the message text to prevent DOM pollution.
|
||||
* @param {string} text Message text
|
||||
* @param {object} options Options object
|
||||
* @param {string} options.prefix Prefix the selectors with this value
|
||||
* @returns {string} Sanitized message text
|
||||
* @copyright https://github.com/kwaroran/risuAI
|
||||
*/
|
||||
export function decodeStyleTags(text) {
|
||||
export function decodeStyleTags(text, { prefix } = { prefix: '.mes_text ' }) {
|
||||
const styleDecodeRegex = /<custom-style>(.+?)<\/custom-style>/gms;
|
||||
const mediaAllowed = isExternalMediaAllowed();
|
||||
|
||||
@@ -494,7 +498,7 @@ export function decodeStyleTags(text) {
|
||||
return v;
|
||||
}).join(' ');
|
||||
|
||||
rule.selectors[i] = '.mes_text ' + selectors;
|
||||
rule.selectors[i] = prefix + selectors;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -532,6 +536,200 @@ export function decodeStyleTags(text) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to manage style preferences for characters.
|
||||
*/
|
||||
class StylesPreference {
|
||||
/**
|
||||
* Creates a new StylesPreference instance.
|
||||
* @param {string|null} avatarId - The avatar ID of the character
|
||||
*/
|
||||
constructor(avatarId) {
|
||||
this.avatarId = avatarId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the account storage key for the style preference.
|
||||
*/
|
||||
get key() {
|
||||
return `AllowGlobalStyles-${this.avatarId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a preference exists for this character.
|
||||
* @returns {boolean} True if preference exists, false otherwise
|
||||
*/
|
||||
exists() {
|
||||
return this.avatarId
|
||||
? accountStorage.getItem(this.key) !== null
|
||||
: true; // No character == assume preference is set
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current style preference.
|
||||
* @returns {boolean} True if global styles are allowed, false otherwise
|
||||
*/
|
||||
get() {
|
||||
return this.avatarId
|
||||
? accountStorage.getItem(this.key) === 'true'
|
||||
: false; // Always disabled when creating a new character
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the global styles preference.
|
||||
* @param {boolean} allowed - Whether global styles are allowed
|
||||
*/
|
||||
set(allowed) {
|
||||
if (this.avatarId) {
|
||||
accountStorage.setItem(this.key, String(allowed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats creator notes in the message text.
|
||||
* @param {string} text Raw Markdown text
|
||||
* @param {string} avatarId Avatar ID
|
||||
* @returns {string} Formatted HTML text
|
||||
*/
|
||||
export function formatCreatorNotes(text, avatarId) {
|
||||
const preference = new StylesPreference(avatarId);
|
||||
const sanitizeStyles = !preference.get();
|
||||
const decodeStyleParam = { prefix: sanitizeStyles ? '#creator_notes_spoiler ' : '' };
|
||||
/** @type {import('dompurify').Config & { MESSAGE_SANITIZE: boolean }} */
|
||||
const config = {
|
||||
RETURN_DOM: false,
|
||||
RETURN_DOM_FRAGMENT: false,
|
||||
RETURN_TRUSTED_TYPE: false,
|
||||
MESSAGE_SANITIZE: true,
|
||||
ADD_TAGS: ['custom-style'],
|
||||
};
|
||||
|
||||
let html = converter.makeHtml(substituteParams(text));
|
||||
html = encodeStyleTags(html);
|
||||
html = DOMPurify.sanitize(html, config);
|
||||
html = decodeStyleTags(html, decodeStyleParam);
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
async function openGlobalStylesPreferenceDialog() {
|
||||
if (selected_group) {
|
||||
toastr.info(t`To change the global styles preference, please select a character individually.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const entityId = getCurrentEntityId();
|
||||
const preference = new StylesPreference(entityId);
|
||||
const currentValue = preference.get();
|
||||
|
||||
const template = $(await renderTemplateAsync('globalStylesPreference'));
|
||||
|
||||
const allowedRadio = template.find('#global_styles_allowed');
|
||||
const forbiddenRadio = template.find('#global_styles_forbidden');
|
||||
|
||||
allowedRadio.on('change', () => {
|
||||
preference.set(true);
|
||||
allowedRadio.prop('checked', true);
|
||||
forbiddenRadio.prop('checked', false);
|
||||
});
|
||||
|
||||
forbiddenRadio.on('change', () => {
|
||||
preference.set(false);
|
||||
allowedRadio.prop('checked', false);
|
||||
forbiddenRadio.prop('checked', true);
|
||||
});
|
||||
|
||||
const currentPreferenceRadio = currentValue ? allowedRadio : forbiddenRadio;
|
||||
template.find(currentPreferenceRadio).prop('checked', true);
|
||||
|
||||
await callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: false, large: false });
|
||||
|
||||
// Re-render the notes if the preference changed
|
||||
const newValue = preference.get();
|
||||
if (newValue !== currentValue) {
|
||||
$('#rm_button_selected_ch').trigger('click');
|
||||
setGlobalStylesButtonClass(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkForCreatorNotesStyles() {
|
||||
// Don't do anything if in group chat or not in a chat
|
||||
if (selected_group || this_chid === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const notes = characters[this_chid].data?.creator_notes || characters[this_chid].creatorcomment;
|
||||
const avatarId = characters[this_chid].avatar;
|
||||
const styleContents = getStyleContentsFromMarkdown(notes);
|
||||
|
||||
if (!styleContents) {
|
||||
setGlobalStylesButtonClass(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const preference = new StylesPreference(avatarId);
|
||||
const hasPreference = preference.exists();
|
||||
if (!hasPreference) {
|
||||
const template = $(await renderTemplateAsync('globalStylesPopup'));
|
||||
template.find('textarea').val(styleContents);
|
||||
const confirmResult = await callGenericPopup(template, POPUP_TYPE.CONFIRM, '', {
|
||||
wide: false,
|
||||
large: false,
|
||||
okButton: t`Just to Creator's Notes`,
|
||||
cancelButton: t`Apply to the entire app`,
|
||||
});
|
||||
|
||||
switch (confirmResult) {
|
||||
case POPUP_RESULT.AFFIRMATIVE:
|
||||
preference.set(false);
|
||||
break;
|
||||
case POPUP_RESULT.NEGATIVE:
|
||||
preference.set(true);
|
||||
break;
|
||||
case POPUP_RESULT.CANCELLED:
|
||||
preference.set(false);
|
||||
break;
|
||||
}
|
||||
|
||||
$('#rm_button_selected_ch').trigger('click');
|
||||
}
|
||||
|
||||
const currentPreference = preference.get();
|
||||
setGlobalStylesButtonClass(currentPreference);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the class of the global styles button based on the state.
|
||||
* @param {boolean|null} state State of the button
|
||||
*/
|
||||
function setGlobalStylesButtonClass(state) {
|
||||
const button = $('#creators_note_styles_button');
|
||||
button.toggleClass('empty', state === null);
|
||||
button.toggleClass('allowed', state === true);
|
||||
button.toggleClass('forbidden', state === false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the contents of all style elements from the Markdown text.
|
||||
* @param {string} text Markdown text
|
||||
* @returns {string} The joined contents of all style elements
|
||||
*/
|
||||
function getStyleContentsFromMarkdown(text) {
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const div = document.createElement('div');
|
||||
const html = converter.makeHtml(substituteParams(text));
|
||||
div.innerHTML = html;
|
||||
const styleElements = Array.from(div.querySelectorAll('style'));
|
||||
return styleElements
|
||||
.filter(s => s.textContent.trim().length > 0)
|
||||
.map(s => s.textContent.trim())
|
||||
.join('\n\n');
|
||||
}
|
||||
|
||||
async function openExternalMediaOverridesDialog() {
|
||||
const entityId = getCurrentEntityId();
|
||||
|
||||
@@ -1037,12 +1235,12 @@ async function openAttachmentManager() {
|
||||
popper.update();
|
||||
});
|
||||
|
||||
return [popper, bodyListener];
|
||||
return { popper, bodyListener };
|
||||
}).filter(Boolean);
|
||||
|
||||
return () => {
|
||||
modalButtonData.forEach(p => {
|
||||
const [popper, bodyListener] = p;
|
||||
const { popper,bodyListener } = p;
|
||||
popper.destroy();
|
||||
document.body.removeEventListener('click', bodyListener);
|
||||
});
|
||||
@@ -1466,7 +1664,7 @@ export function registerFileConverter(mimeType, converter) {
|
||||
converters[mimeType] = converter;
|
||||
}
|
||||
|
||||
jQuery(function () {
|
||||
export function initChatUtilities() {
|
||||
$(document).on('click', '.mes_hide', async function () {
|
||||
const messageBlock = $(this).closest('.mes');
|
||||
const messageId = Number(messageBlock.attr('mesid'));
|
||||
@@ -1645,6 +1843,10 @@ jQuery(function () {
|
||||
reloadCurrentChat();
|
||||
});
|
||||
|
||||
$('#creators_note_styles_button').on('click', function () {
|
||||
openGlobalStylesPreferenceDialog();
|
||||
});
|
||||
|
||||
$(document).on('click', '.mes_img', expandMessageImage);
|
||||
$(document).on('click', '.mes_img_enlarge', expandAndZoomMessageImage);
|
||||
$(document).on('click', '.mes_img_delete', deleteMessageImage);
|
||||
@@ -1679,4 +1881,6 @@ jQuery(function () {
|
||||
fileInput.files = dataTransfer.files;
|
||||
await onFileAttach(fileInput.files[0]);
|
||||
});
|
||||
});
|
||||
|
||||
eventSource.on(event_types.CHAT_CHANGED, checkForCreatorNotesStyles);
|
||||
}
|
||||
|
27
public/scripts/templates/globalStylesPopup.html
Normal file
27
public/scripts/templates/globalStylesPopup.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<h3 data-i18n="Creator's Notes contain CSS style tags. Do you want to apply them just to Creator's Notes or to the entire application?" class="margin0">
|
||||
Creator's Notes contain CSS style tags. Do you want to apply them just to Creator's Notes or to the entire application?
|
||||
</h3>
|
||||
<h4 data-i18n="CAUTION: Malformed styles may cause issues." class="neutral_warning">
|
||||
CAUTION: Malformed styles may cause issues.
|
||||
</h4>
|
||||
<hr>
|
||||
<small>
|
||||
<span data-i18n="To change the preference later, use the">
|
||||
To change the preference later, use the
|
||||
</span>
|
||||
<code class="fa-solid fa-palette"></code>
|
||||
<span data-i18n="button in the Creator's Notes block.">
|
||||
button in the Creator's Notes block.
|
||||
</span>
|
||||
</small>
|
||||
<textarea class="text_pole textarea_compact monospace" rows="8" readonly></textarea>
|
||||
<small class="justifyLeft">
|
||||
<b data-i18n="Note:">
|
||||
Note:
|
||||
</b>
|
||||
<span data-i18n="Class names will be automatically prefixed with 'custom-'.">
|
||||
Class names will be automatically prefixed with 'custom-'.
|
||||
</span>
|
||||
</small>
|
||||
</div>
|
16
public/scripts/templates/globalStylesPreference.html
Normal file
16
public/scripts/templates/globalStylesPreference.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<h3 data-i18n="Choose how to apply CSS style tags if they are defined in Creator's Notes of this character:" class="margin0">
|
||||
Choose how to apply CSS style tags if they are defined in Creator's Notes of this character:
|
||||
</h3>
|
||||
<h4 data-i18n="CAUTION: Malformed styles may cause issues." class="neutral_warning">
|
||||
CAUTION: Malformed styles may cause issues.
|
||||
</h4>
|
||||
<label class="checkbox_label" for="global_styles_forbidden">
|
||||
<input type="radio" id="global_styles_forbidden" name="global_styles_preference" />
|
||||
<span data-i18n="Just to Creator's Notes">Just to Creator's Notes</span>
|
||||
</label>
|
||||
<label class="checkbox_label" for="global_styles_allowed">
|
||||
<input type="radio" id="global_styles_allowed" name="global_styles_preference" />
|
||||
<span data-i18n="Apply to the entire app">Apply to the entire app</span>
|
||||
</label>
|
||||
</div>
|
Reference in New Issue
Block a user