diff --git a/public/index.html b/public/index.html index 554b031ff..67b142a52 100644 --- a/public/index.html +++ b/public/index.html @@ -5451,9 +5451,10 @@
-
- Creator's Notes +
+ Creator's Notes Character details are hidden. +
diff --git a/public/script.js b/public/script.js index c91bff5d7..b8a47cf17 100644 --- a/public/script.js +++ b/public/script.js @@ -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); diff --git a/public/scripts/chats.js b/public/scripts/chats.js index 212c09653..0b361f92a 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -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>/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); +} diff --git a/public/scripts/templates/globalStylesPopup.html b/public/scripts/templates/globalStylesPopup.html new file mode 100644 index 000000000..06a9e4ce1 --- /dev/null +++ b/public/scripts/templates/globalStylesPopup.html @@ -0,0 +1,27 @@ +
+

+ Creator's Notes contain CSS style tags. Do you want to apply them just to Creator's Notes or to the entire application? +

+

+ CAUTION: Malformed styles may cause issues. +

+
+ + + To change the preference later, use the + + + + button in the Creator's Notes block. + + + + + + Note: + + + Class names will be automatically prefixed with 'custom-'. + + +
diff --git a/public/scripts/templates/globalStylesPreference.html b/public/scripts/templates/globalStylesPreference.html new file mode 100644 index 000000000..ff766a199 --- /dev/null +++ b/public/scripts/templates/globalStylesPreference.html @@ -0,0 +1,16 @@ +
+

+ Choose how to apply CSS style tags if they are defined in Creator's Notes of this character: +

+

+ CAUTION: Malformed styles may cause issues. +

+ + +