Add clickable buttons in Welcome chat message. Add bool `uses_system_ui` on system messages to override sanitizer for buttons when set Modify uponSanitizeAttribute DOMPurify hook to allow unmangled class names on attributes in some cases Add event listener for .drawer-opener to open a navbar drawer
| @@ -293,10 +293,17 @@ DOMPurify.addHook('afterSanitizeAttributes', function (node) { | ||
| 293 | 293 | } |
| 294 | 294 | }); |
| 295 | 295 | |
| 296 | 296 | DOMPurify.addHook('uponSanitizeAttribute', (_node, data, config) => { |
| 297 | 297 | if (!config['MESSAGE_SANITIZE']) { |
| 298 | 298 | return; |
| 299 | 299 | } |
| 300 | + | |
| 301 | + /* Retain the classes on UI elements of messages that interact with the main UI */ | |
| 302 | + const permittedNodeTypes = ['BUTTON', 'DIV']; | |
| 303 | + if (config['MESSAGE_ALLOW_SYSTEM_UI'] && node.classList.contains('menu_button') && permittedNodeTypes.includes(node.nodeName)) { | |
| 304 | + return; | |
| 305 | + } | |
| 306 | + | |
| 300 | 307 | switch (data.attrName) { |
| 301 | 308 | case 'class': { |
| 302 | 309 | if (data.attrValue) { |
| @@ -650,6 +657,7 @@ async function getSystemMessages() { | ||
| 650 | 657 | force_avatar: system_avatar, |
| 651 | 658 | is_user: false, |
| 652 | 659 | is_system: true, |
| 660 | + uses_system_ui: true, | |
| 653 | 661 | mes: await renderTemplateAsync('welcome', { displayVersion } ), |
| 654 | 662 | }, |
| 655 | 663 | group: { |
| @@ -1916,9 +1924,10 @@ export async function sendTextareaMessage() { | ||
| 1916 | 1924 | * @param {boolean} isSystem If the message was sent by the system |
| 1917 | 1925 | * @param {boolean} isUser If the message was sent by the user |
| 1918 | 1926 | * @param {number} messageId Message index in chat array |
| 1927 | + * @param {object} [sanitizerOverrides] DOMPurify sanitizer option overrides | |
| 1919 | 1928 | * @returns {string} HTML string |
| 1920 | 1929 | */ |
| 1921 | 1930 | export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, sanitizerOverrides = {}) { |
| 1922 | 1931 | if (!mes) { |
| 1923 | 1932 | return ''; |
| 1924 | 1933 | } |
| @@ -2029,7 +2038,7 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId) { | ||
| 2029 | 2038 | } |
| 2030 | 2039 | |
| 2031 | 2040 | /** @type {any} */ |
| 2032 | 2041 | const config = { MESSAGE_SANITIZE: true, ADD_TAGS: ['custom-style'], ...sanitizerOverrides }; |
| 2033 | 2042 | mes = encodeStyleTags(mes); |
| 2034 | 2043 | mes = DOMPurify.sanitize(mes, config); |
| 2035 | 2044 | mes = decodeStyleTags(mes); |
| @@ -2234,6 +2243,18 @@ export function addCopyToCodeBlocks(messageElement) { | ||
| 2234 | 2243 | } |
| 2235 | 2244 | |
| 2236 | 2245 | |
| 2246 | +/** | |
| 2247 | + * Adds a single message to the chat. | |
| 2248 | + * @param {object} mes Message object | |
| 2249 | + * @param {object} [options] Options | |
| 2250 | + * @param {string} [options.type='normal'] Message type | |
| 2251 | + * @param {number} [options.insertAfter=null] Message ID to insert the new message after | |
| 2252 | + * @param {boolean} [options.scroll=true] Whether to scroll to the new message | |
| 2253 | + * @param {number} [options.insertBefore=null] Message ID to insert the new message before | |
| 2254 | + * @param {number} [options.forceId=null] Force the message ID | |
| 2255 | + * @param {boolean} [options.showSwipes=true] Whether to show swipe buttons | |
| 2256 | + * @returns {void} | |
| 2257 | + */ | |
| 2237 | 2258 | export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll = true, insertBefore = null, forceId = null, showSwipes = true } = {}) { |
| 2238 | 2259 | let messageText = mes['mes']; |
| 2239 | 2260 | const momentDate = timestampToMoment(mes.send_date); |
| @@ -2262,7 +2283,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | ||
| 2262 | 2283 | } else if (this_chid === undefined) { |
| 2263 | 2284 | avatarImg = system_avatar; |
| 2264 | 2285 | } else { |
| 2265 | 2286 | if (characters[this_chid].avatar !== 'none') { |
| 2266 | 2287 | avatarImg = getThumbnailUrl('avatar', characters[this_chid].avatar); |
| 2267 | 2288 | } else { |
| 2268 | 2289 | avatarImg = default_avatar; |
| @@ -2277,12 +2298,16 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | ||
| 2277 | 2298 | avatarImg = mes['force_avatar']; |
| 2278 | 2299 | } |
| 2279 | 2300 | |
| 2301 | + // if mes.uses_system_ui is true, set an override on the sanitizer options | |
| 2302 | + const sanitizerOverrides = mes.uses_system_ui ? { MESSAGE_ALLOW_SYSTEM_UI: true } : {}; | |
| 2303 | + | |
| 2280 | 2304 | messageText = messageFormatting( |
| 2281 | 2305 | messageText, |
| 2282 | 2306 | mes.name, |
| 2283 | 2307 | isSystem, |
| 2284 | 2308 | mes.is_user, |
| 2285 | 2309 | chat.indexOf(mes), |
| 2310 | + sanitizerOverrides, | |
| 2286 | 2311 | ); |
| 2287 | 2312 | const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1); |
| 2288 | 2313 | let bookmarkLink = mes?.extra?.bookmark_link ?? ''; |
| @@ -2330,7 +2355,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | ||
| 2330 | 2355 | } |
| 2331 | 2356 | |
| 2332 | 2357 | //shows or hides the Prompt display button |
| 2333 | 2358 | let mesIdToFind = type === 'swipe' ? params.mesId - 1 : params.mesId; //Number(newMessage.attr('mesId')); |
| 2334 | 2359 | |
| 2335 | 2360 | //if we have itemized messages, and the array isn't null.. |
| 2336 | 2361 | if (params.isUser === false && Array.isArray(itemizedPrompts) && itemizedPrompts.length > 0) { |
| @@ -2651,7 +2676,7 @@ export function sendSystemMessage(type, text, extra = {}) { | ||
| 2651 | 2676 | newMessage.mes = text; |
| 2652 | 2677 | } |
| 2653 | 2678 | |
| 2654 | 2679 | if (type === system_message_types.SLASH_COMMANDS) { |
| 2655 | 2680 | newMessage.mes = getSlashCommandsHelp(); |
| 2656 | 2681 | } |
| 2657 | 2682 | |
| @@ -2665,7 +2690,7 @@ export function sendSystemMessage(type, text, extra = {}) { | ||
| 2665 | 2690 | chat.push(newMessage); |
| 2666 | 2691 | addOneMessage(newMessage); |
| 2667 | 2692 | is_send_press = false; |
| 2668 | 2693 | if (type === system_message_types.SLASH_COMMANDS) { |
| 2669 | 2694 | const browser = new SlashCommandBrowser(); |
| 2670 | 2695 | const spinner = document.querySelector('#chat .last_mes .custom-slashHelp'); |
| 2671 | 2696 | const parent = spinner.parentElement; |
| @@ -7854,7 +7879,7 @@ function openAlternateGreetings() { | ||
| 7854 | 7879 | if (menu_type !== 'create') { |
| 7855 | 7880 | await createOrEditCharacter(); |
| 7856 | 7881 | } |
| 7857 | 7882 | }, |
| 7858 | 7883 | }); |
| 7859 | 7884 | |
| 7860 | 7885 | for (let index = 0; index < getArray().length; index++) { |
| @@ -9070,6 +9095,90 @@ function doTogglePanels() { | ||
| 9070 | 9095 | return ''; |
| 9071 | 9096 | } |
| 9072 | 9097 | |
| 9098 | +/** | |
| 9099 | + * Event handler to open a navbar drawer when a drawer open button is clicked. | |
| 9100 | + * Handles click events on .drawer-opener elements. | |
| 9101 | + * Opens the drawer associated with the clicked button according to the data-target attribute. | |
| 9102 | + * @returns {void} | |
| 9103 | + */ | |
| 9104 | +function doDrawerOpenClick() { | |
| 9105 | + const targetDrawerID = $(this).attr('data-target'); | |
| 9106 | + const drawer = $(`#${targetDrawerID}`); | |
| 9107 | + const drawerToggle = drawer.find('.drawer-toggle'); | |
| 9108 | + const drawerWasOpenAlready = drawerToggle.parent().find('.drawer-content').hasClass('openDrawer'); | |
| 9109 | + if (drawerWasOpenAlready || drawer.hasClass('resizing') ) { return; } | |
| 9110 | + doNavbarIconClick.call(drawerToggle); | |
| 9111 | +} | |
| 9112 | + | |
| 9113 | +/** | |
| 9114 | + * Event handler to open or close a navbar drawer when a navbar icon is clicked. | |
| 9115 | + * Handles click events on .drawer-toggle elements. | |
| 9116 | + * @returns {void} | |
| 9117 | + */ | |
| 9118 | +function doNavbarIconClick() { | |
| 9119 | + var icon = $(this).find('.drawer-icon'); | |
| 9120 | + var drawer = $(this).parent().find('.drawer-content'); | |
| 9121 | + if (drawer.hasClass('resizing')) { return; } | |
| 9122 | + var drawerWasOpenAlready = $(this).parent().find('.drawer-content').hasClass('openDrawer'); | |
| 9123 | + let targetDrawerID = $(this).parent().find('.drawer-content').attr('id'); | |
| 9124 | + const pinnedDrawerClicked = drawer.hasClass('pinnedOpen'); | |
| 9125 | + | |
| 9126 | + if (!drawerWasOpenAlready) { //to open the drawer | |
| 9127 | + $('.openDrawer').not('.pinnedOpen').addClass('resizing').slideToggle(200, 'swing', async function () { | |
| 9128 | + await delay(50); $(this).closest('.drawer-content').removeClass('resizing'); | |
| 9129 | + }); | |
| 9130 | + $('.openIcon').toggleClass('closedIcon openIcon'); | |
| 9131 | + $('.openDrawer').not('.pinnedOpen').toggleClass('closedDrawer openDrawer'); | |
| 9132 | + icon.toggleClass('openIcon closedIcon'); | |
| 9133 | + drawer.toggleClass('openDrawer closedDrawer'); | |
| 9134 | + | |
| 9135 | + //console.log(targetDrawerID); | |
| 9136 | + if (targetDrawerID === 'right-nav-panel') { | |
| 9137 | + $(this).closest('.drawer').find('.drawer-content').addClass('resizing').slideToggle({ | |
| 9138 | + duration: 200, | |
| 9139 | + easing: 'swing', | |
| 9140 | + start: function () { | |
| 9141 | + jQuery(this).css('display', 'flex'); //flex needed to make charlist scroll | |
| 9142 | + }, | |
| 9143 | + complete: async function () { | |
| 9144 | + favsToHotswap(); | |
| 9145 | + await delay(50); | |
| 9146 | + $(this).closest('.drawer-content').removeClass('resizing'); | |
| 9147 | + $('#rm_print_characters_block').trigger('scroll'); | |
| 9148 | + }, | |
| 9149 | + }); | |
| 9150 | + } else { | |
| 9151 | + $(this).closest('.drawer').find('.drawer-content').addClass('resizing').slideToggle(200, 'swing', async function () { | |
| 9152 | + await delay(50); $(this).closest('.drawer-content').removeClass('resizing'); | |
| 9153 | + }); | |
| 9154 | + } | |
| 9155 | + | |
| 9156 | + // Set the height of "autoSetHeight" textareas within the drawer to their scroll height | |
| 9157 | + if (!CSS.supports('field-sizing', 'content')) { | |
| 9158 | + $(this).closest('.drawer').find('.drawer-content textarea.autoSetHeight').each(async function () { | |
| 9159 | + await resetScrollHeight($(this)); | |
| 9160 | + return; | |
| 9161 | + }); | |
| 9162 | + } | |
| 9163 | + | |
| 9164 | + } else if (drawerWasOpenAlready) { //to close manually | |
| 9165 | + icon.toggleClass('closedIcon openIcon'); | |
| 9166 | + | |
| 9167 | + if (pinnedDrawerClicked) { | |
| 9168 | + $(drawer).addClass('resizing').slideToggle(200, 'swing', async function () { | |
| 9169 | + await delay(50); $(this).removeClass('resizing'); | |
| 9170 | + }); | |
| 9171 | + } | |
| 9172 | + else { | |
| 9173 | + $('.openDrawer').not('.pinnedOpen').addClass('resizing').slideToggle(200, 'swing', async function () { | |
| 9174 | + await delay(50); $(this).closest('.drawer-content').removeClass('resizing'); | |
| 9175 | + }); | |
| 9176 | + } | |
| 9177 | + | |
| 9178 | + drawer.toggleClass('closedDrawer openDrawer'); | |
| 9179 | + } | |
| 9180 | +} | |
| 9181 | + | |
| 9073 | 9182 | function addDebugFunctions() { |
| 9074 | 9183 | const doBackfill = async () => { |
| 9075 | 9184 | for (const message of chat) { |
| @@ -10659,69 +10768,8 @@ jQuery(async function () { | ||
| 10659 | 10768 | stopScriptExecution(); |
| 10660 | 10769 | }); |
| 10661 | 10770 | |
| 10662 | 10771 | $('.drawer-toggle'document).on('click', function'.drawer-opener', (doDrawerOpenClick) {; |
| 10663 | - var icon = $(this).find('.drawer-icon'); | |
| 10772 | + $('.drawer-toggle').on('click', doNavbarIconClick); | |
| 10664 | - var drawer = $(this).parent().find('.drawer-content'); | |
| 10665 | - if (drawer.hasClass('resizing')) { return; } | |
| 10666 | - var drawerWasOpenAlready = $(this).parent().find('.drawer-content').hasClass('openDrawer'); | |
| 10667 | - let targetDrawerID = $(this).parent().find('.drawer-content').attr('id'); | |
| 10668 | - const pinnedDrawerClicked = drawer.hasClass('pinnedOpen'); | |
| 10669 | - | |
| 10670 | - if (!drawerWasOpenAlready) { //to open the drawer | |
| 10671 | - $('.openDrawer').not('.pinnedOpen').addClass('resizing').slideToggle(200, 'swing', async function () { | |
| 10672 | - await delay(50); $(this).closest('.drawer-content').removeClass('resizing'); | |
| 10673 | - }); | |
| 10674 | - $('.openIcon').not('.drawerPinnedOpen').toggleClass('closedIcon openIcon'); | |
| 10675 | - $('.openDrawer').not('.pinnedOpen').toggleClass('closedDrawer openDrawer'); | |
| 10676 | - icon.toggleClass('openIcon closedIcon'); | |
| 10677 | - drawer.toggleClass('openDrawer closedDrawer'); | |
| 10678 | - | |
| 10679 | - //console.log(targetDrawerID); | |
| 10680 | - if (targetDrawerID === 'right-nav-panel') { | |
| 10681 | - $(this).closest('.drawer').find('.drawer-content').addClass('resizing').slideToggle({ | |
| 10682 | - duration: 200, | |
| 10683 | - easing: 'swing', | |
| 10684 | - start: function () { | |
| 10685 | - jQuery(this).css('display', 'flex'); //flex needed to make charlist scroll | |
| 10686 | - }, | |
| 10687 | - complete: async function () { | |
| 10688 | - favsToHotswap(); | |
| 10689 | - await delay(50); | |
| 10690 | - $(this).closest('.drawer-content').removeClass('resizing'); | |
| 10691 | - $('#rm_print_characters_block').trigger('scroll'); | |
| 10692 | - }, | |
| 10693 | - }); | |
| 10694 | - } else { | |
| 10695 | - $(this).closest('.drawer').find('.drawer-content').addClass('resizing').slideToggle(200, 'swing', async function () { | |
| 10696 | - await delay(50); $(this).closest('.drawer-content').removeClass('resizing'); | |
| 10697 | - }); | |
| 10698 | - } | |
| 10699 | - | |
| 10700 | - // Set the height of "autoSetHeight" textareas within the drawer to their scroll height | |
| 10701 | - if (!CSS.supports('field-sizing', 'content')) { | |
| 10702 | - $(this).closest('.drawer').find('.drawer-content textarea.autoSetHeight').each(async function () { | |
| 10703 | - await resetScrollHeight($(this)); | |
| 10704 | - return; | |
| 10705 | - }); | |
| 10706 | - } | |
| 10707 | - | |
| 10708 | - } else if (drawerWasOpenAlready) { //to close manually | |
| 10709 | - icon.toggleClass('closedIcon openIcon'); | |
| 10710 | - | |
| 10711 | - if (pinnedDrawerClicked) { | |
| 10712 | - $(drawer).addClass('resizing').slideToggle(200, 'swing', async function () { | |
| 10713 | - await delay(50); $(this).removeClass('resizing'); | |
| 10714 | - }); | |
| 10715 | - } | |
| 10716 | - else { | |
| 10717 | - $('.openDrawer').not('.pinnedOpen').addClass('resizing').slideToggle(200, 'swing', async function () { | |
| 10718 | - await delay(50); $(this).closest('.drawer-content').removeClass('resizing'); | |
| 10719 | - }); | |
| 10720 | - } | |
| 10721 | - | |
| 10722 | - drawer.toggleClass('closedDrawer openDrawer'); | |
| 10723 | - } | |
| 10724 | - }); | |
| 10725 | 10773 | |
| 10726 | 10774 | $('html').on('touchstart mousedown', function (e) { |
| 10727 | 10775 | var clickTarget = $(e.target); |
| @@ -8,30 +8,59 @@ | ||
| 8 | 8 | <h3 data-i18n="How to start chatting?">How to start chatting?</h3> |
| 9 | 9 | <ol> |
| 10 | 10 | <li> |
| 11 | - <span data-i18n="Click _space">Click </span><code><i class="fa-solid fa-plug"></i></code><span data-i18n="and select a"> and select a </span><a href="https://docs.sillytavern.app/usage/api-connections/" target="_blank" data-i18n="Chat API">Chat API</a>.</span> | |
| 11 | + <span data-i18n="Click _space">Click </span> | |
| 12 | + <button class="menu_button menu_button_icon drawer-opener inline-flex" data-target="sys-settings-button"> | |
| 13 | + <i class="fa-solid fa-plug"></i> | |
| 14 | + <span data-i18n="[title]API Connections">API Connections</span> | |
| 15 | + </button> | |
| 16 | + <span data-i18n="and select a">and select a</span> | |
| 17 | + <a href="https://docs.sillytavern.app/usage/api-connections/" target="_blank"> | |
| 18 | + <span class="fa-solid fa-circle-question"></span> | |
| 19 | + <span data-i18n="Chat API">Chat API</span></a>. | |
| 12 | 20 | </li> |
| 13 | 21 | <li> |
| 14 | - <span data-i18n="Click _space">Click </span><code><i class="fa-solid fa-address-card"></i></code><span data-i18n="and pick a character."> and pick a character.</span> | |
| 22 | + <span data-i18n="Click _space">Click </span> | |
| 23 | + <button class="menu_button menu_button_icon drawer-opener inline-flex" data-target="rightNavHolder"> | |
| 24 | + <i class="fa-solid fa-address-card"></i> | |
| 25 | + <span data-i18n="[title]Character Management">Character Management</span> | |
| 26 | + </button> | |
| 27 | + <span data-i18n="and pick a character."> and pick a character.</span> | |
| 15 | 28 | </li> |
| 16 | 29 | </ol> |
| 17 | -<div> | |
| 30 | + | |
| 18 | - <span data-i18n="You can browse a list of bundled characters in the"> | |
| 31 | +<p> | |
| 19 | - You can browse a list of bundled characters in the | |
| 32 | + <span data-i18n="You can add more">You can add more</span> | |
| 20 | - </span> | |
| 33 | + <button class="open_characters_library menu_button menu_button_icon inline-flex"> | |
| 21 | - <i data-i18n="Download Extensions & Assets"> | |
| 34 | + <i class="fa-solid fa-image-portrait"></i> | |
| 22 | - Download Extensions & Assets | |
| 35 | + <span data-i18n="Sample characters">Sample characters</span> | |
| 23 | 36 | </ibutton> |
| 24 | 37 | <span data-i18n="menu withinor">or</span> |
| 25 | - menu within | |
| 38 | + <button class="external_import_button menu_button menu_button_icon inline-flex"> | |
| 26 | - </span> | |
| 39 | + <i class="fa-solid fa-cloud-arrow-down"></i> | |
| 27 | - <code><i class="fa-solid fa-cubes"></i></code> | |
| 40 | + <span data-i18n="Import Characters">Import characters</span> | |
| 28 | - <span>.</span> | |
| 41 | + </button> | |
| 29 | -</div> | |
| 42 | + <span data-i18n="from other websites">from other websites.</span> | |
| 43 | + | |
| 44 | + <span data-i18n="Go to the">Go to the</span> | |
| 45 | + | |
| 46 | + <i data-i18n="Download Extensions & Assets">Download Extensions & Assets</i> | |
| 47 | + <span data-i18n="menu within">menu within</span> | |
| 48 | + | |
| 49 | + <button class="menu_button menu_button_icon drawer-opener inline-flex" data-target="extensions-settings-button"> | |
| 50 | + <i class="fa-solid fa-cubes"></i> | |
| 51 | + <span data-i18n="[title]Extensions">Extensions</span> | |
| 52 | + </button> | |
| 53 | + | |
| 54 | + <span data-i18n="to install additional features.">to install additional features.</span> | |
| 55 | +</p> | |
| 56 | + | |
| 30 | 57 | <hr> |
| 31 | 58 | <h3 data-i18n="Confused or lost?">Confused or lost?</h3> |
| 32 | 59 | <ul> |
| 33 | 60 | <li> |
| 34 | 61 | <span class="note-link-span"><a class="fa-solid fa-circle-question" target="_blank" href="https://docs.sillytavern.app/"></a></span> - <span data-i18n="click these icons!">click these icons!</span> |
| 62 | + href="https://docs.sillytavern.app/"></a></span> - <span | |
| 63 | + data-i18n="click these icons!">click these icons!</span> | |
| 35 | 64 | </li> |
| 36 | 65 | <li> |
| 37 | 66 | <span data-i18n="Enter">Enter </span><code>/?</code><span data-i18n="in the chat bar"> in the chat bar</span> |
| @@ -57,7 +86,8 @@ | ||
| 57 | 86 | </a> |
| 58 | 87 | </li> |
| 59 | 88 | <li> |
| 60 | 89 | <a target="_blank" href="https://github.com/SillyTavern/SillyTavern#questions-or-suggestions" data-i18n="Contact the developers"> |
| 90 | + data-i18n="Contact the developers"> | |
| 61 | 91 | Contact the developers |
| 62 | 92 | </a> |
| 63 | 93 | </li> |
| @@ -1208,8 +1208,9 @@ textarea.autoSetHeight { | ||
| 1208 | 1208 | } |
| 1209 | 1209 | |
| 1210 | 1210 | input, |
| 1211 | -select { | |
| 1211 | +select, | |
| 1212 | - font-family: var(--mainFontFamily); | |
| 1212 | +button { | |
| 1213 | + font-family: var(--mainFontFamily), sans-serif; | |
| 1213 | 1214 | font-size: var(--mainFontSize); |
| 1214 | 1215 | color: var(--SmartThemeBodyColor); |
| 1215 | 1216 | } |