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

02b0000117e6b9136ed9014d9eb2c1655ccb7761

ceruleandeep <deep@cerulean.navy>

3 files changed, +200 -121Ignore whitespace
public/script.js+120 -72
@@ -293,10 +293,17 @@ DOMPurify.addHook('afterSanitizeAttributes', function (node) {
293 }293 }
294});294});
295295
296DOMPurify.addHook('uponSanitizeAttribute', (_, data, config) => {296DOMPurify.addHook('uponSanitizeAttribute', (node, data, config) => {
297 if (!config['MESSAGE_SANITIZE']) {297 if (!config['MESSAGE_SANITIZE']) {
298 return;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 switch (data.attrName) {307 switch (data.attrName) {
301 case 'class': {308 case 'class': {
302 if (data.attrValue) {309 if (data.attrValue) {
@@ -650,7 +657,8 @@ async function getSystemMessages() {
650 force_avatar: system_avatar,657 force_avatar: system_avatar,
651 is_user: false,658 is_user: false,
652 is_system: true,659 is_system: true,
653 mes: await renderTemplateAsync('welcome', { displayVersion }),660 uses_system_ui: true,
661 mes: await renderTemplateAsync('welcome', { displayVersion } ),
654 },662 },
655 group: {663 group: {
656 name: systemUserName,664 name: systemUserName,
@@ -1916,9 +1924,10 @@ export async function sendTextareaMessage() {
1916 * @param {boolean} isSystem If the message was sent by the system1924 * @param {boolean} isSystem If the message was sent by the system
1917 * @param {boolean} isUser If the message was sent by the user1925 * @param {boolean} isUser If the message was sent by the user
1918 * @param {number} messageId Message index in chat array1926 * @param {number} messageId Message index in chat array
1927 * @param {object} [sanitizerOverrides] DOMPurify sanitizer option overrides
1919 * @returns {string} HTML string1928 * @returns {string} HTML string
1920 */1929 */
1921export function messageFormatting(mes, ch_name, isSystem, isUser, messageId) {1930export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, sanitizerOverrides = {}) {
1922 if (!mes) {1931 if (!mes) {
1923 return '';1932 return '';
1924 }1933 }
@@ -2029,7 +2038,7 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId) {
2029 }2038 }
20302039
2031 /** @type {any} */2040 /** @type {any} */
2032 const config = { MESSAGE_SANITIZE: true, ADD_TAGS: ['custom-style'] };2041 const config = { MESSAGE_SANITIZE: true, ADD_TAGS: ['custom-style'], ...sanitizerOverrides };
2033 mes = encodeStyleTags(mes);2042 mes = encodeStyleTags(mes);
2034 mes = DOMPurify.sanitize(mes, config);2043 mes = DOMPurify.sanitize(mes, config);
2035 mes = decodeStyleTags(mes);2044 mes = decodeStyleTags(mes);
@@ -2234,6 +2243,18 @@ export function addCopyToCodeBlocks(messageElement) {
2234}2243}
22352244
22362245
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 */
2237export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll = true, insertBefore = null, forceId = null, showSwipes = true } = {}) {2258export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll = true, insertBefore = null, forceId = null, showSwipes = true } = {}) {
2238 let messageText = mes['mes'];2259 let messageText = mes['mes'];
2239 const momentDate = timestampToMoment(mes.send_date);2260 const momentDate = timestampToMoment(mes.send_date);
@@ -2262,7 +2283,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2262 } else if (this_chid === undefined) {2283 } else if (this_chid === undefined) {
2263 avatarImg = system_avatar;2284 avatarImg = system_avatar;
2264 } else {2285 } else {
2265 if (characters[this_chid].avatar != 'none') {2286 if (characters[this_chid].avatar !== 'none') {
2266 avatarImg = getThumbnailUrl('avatar', characters[this_chid].avatar);2287 avatarImg = getThumbnailUrl('avatar', characters[this_chid].avatar);
2267 } else {2288 } else {
2268 avatarImg = default_avatar;2289 avatarImg = default_avatar;
@@ -2277,12 +2298,16 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2277 avatarImg = mes['force_avatar'];2298 avatarImg = mes['force_avatar'];
2278 }2299 }
22792300
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 messageText = messageFormatting(2304 messageText = messageFormatting(
2281 messageText,2305 messageText,
2282 mes.name,2306 mes.name,
2283 isSystem,2307 isSystem,
2284 mes.is_user,2308 mes.is_user,
2285 chat.indexOf(mes),2309 chat.indexOf(mes),
2310 sanitizerOverrides,
2286 );2311 );
2287 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1);2312 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1);
2288 let bookmarkLink = mes?.extra?.bookmark_link ?? '';2313 let bookmarkLink = mes?.extra?.bookmark_link ?? '';
@@ -2330,7 +2355,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2330 }2355 }
23312356
2332 //shows or hides the Prompt display button2357 //shows or hides the Prompt display button
2333 let mesIdToFind = type == 'swipe' ? params.mesId - 1 : params.mesId; //Number(newMessage.attr('mesId'));2358 let mesIdToFind = type === 'swipe' ? params.mesId - 1 : params.mesId; //Number(newMessage.attr('mesId'));
23342359
2335 //if we have itemized messages, and the array isn't null..2360 //if we have itemized messages, and the array isn't null..
2336 if (params.isUser === false && Array.isArray(itemizedPrompts) && itemizedPrompts.length > 0) {2361 if (params.isUser === false && Array.isArray(itemizedPrompts) && itemizedPrompts.length > 0) {
@@ -2651,7 +2676,7 @@ export function sendSystemMessage(type, text, extra = {}) {
2651 newMessage.mes = text;2676 newMessage.mes = text;
2652 }2677 }
26532678
2654 if (type == system_message_types.SLASH_COMMANDS) {2679 if (type === system_message_types.SLASH_COMMANDS) {
2655 newMessage.mes = getSlashCommandsHelp();2680 newMessage.mes = getSlashCommandsHelp();
2656 }2681 }
26572682
@@ -2665,7 +2690,7 @@ export function sendSystemMessage(type, text, extra = {}) {
2665 chat.push(newMessage);2690 chat.push(newMessage);
2666 addOneMessage(newMessage);2691 addOneMessage(newMessage);
2667 is_send_press = false;2692 is_send_press = false;
2668 if (type == system_message_types.SLASH_COMMANDS) {2693 if (type === system_message_types.SLASH_COMMANDS) {
2669 const browser = new SlashCommandBrowser();2694 const browser = new SlashCommandBrowser();
2670 const spinner = document.querySelector('#chat .last_mes .custom-slashHelp');2695 const spinner = document.querySelector('#chat .last_mes .custom-slashHelp');
2671 const parent = spinner.parentElement;2696 const parent = spinner.parentElement;
@@ -7854,7 +7879,7 @@ function openAlternateGreetings() {
7854 if (menu_type !== 'create') {7879 if (menu_type !== 'create') {
7855 await createOrEditCharacter();7880 await createOrEditCharacter();
7856 }7881 }
7857 }7882 },
7858 });7883 });
78597884
7860 for (let index = 0; index < getArray().length; index++) {7885 for (let index = 0; index < getArray().length; index++) {
@@ -9070,6 +9095,90 @@ function doTogglePanels() {
9070 return '';9095 return '';
9071}9096}
90729097
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 */
9104function 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 */
9118function 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
9073function addDebugFunctions() {9182function addDebugFunctions() {
9074 const doBackfill = async () => {9183 const doBackfill = async () => {
9075 for (const message of chat) {9184 for (const message of chat) {
@@ -10659,69 +10768,8 @@ jQuery(async function () {
10659 stopScriptExecution();10768 stopScriptExecution();
10660 });10769 });
1066110770
10662 $('.drawer-toggle').on('click', function () {10771 $(document).on('click', '.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 });
1072510773
10726 $('html').on('touchstart mousedown', function (e) {10774 $('html').on('touchstart mousedown', function (e) {
10727 var clickTarget = $(e.target);10775 var clickTarget = $(e.target);
public/scripts/templates/welcome.html+77 -47
@@ -1,65 +1,95 @@
1<h3>1<h3>
2 <span id="version_display_welcome">{{displayVersion}}</span>2 <span id="version_display_welcome">{{displayVersion}}</span>
3</h3>3</h3>
4<a href="https://docs.sillytavern.app/usage/update/" target="_blank" data-i18n="Want to update?">4<a href="https://docs.sillytavern.app/usage/update/" target="_blank" data-i18n="Want to update?">
5 Want to update?5 Want to update?
6</a>6</a>
7<hr>7<hr>
8<h3 data-i18n="How to start chatting?">How to start chatting?</h3>8<h3 data-i18n="How to start chatting?">How to start chatting?</h3>
9<ol>9<ol>
10 <li>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 </li>12 <button class="menu_button menu_button_icon drawer-opener inline-flex" data-target="sys-settings-button">
13 <li>13 <i class="fa-solid fa-plug"></i>
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>14 <span data-i18n="[title]API Connections">API Connections</span>
15 </li>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>.
20 </li>
21 <li>
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>
28 </li>
16</ol>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 the32 <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 & Assets35 <span data-i18n="Sample characters">Sample characters</span>
23 </i>36 </button>
24 <span data-i18n="menu within">37 <span data-i18n="or">or</span>
25 menu within38 <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<hr>57<hr>
31<h3 data-i18n="Confused or lost?">Confused or lost?</h3>58<h3 data-i18n="Confused or lost?">Confused or lost?</h3>
32<ul>59<ul>
33 <li>60 <li>
34 <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>61 <span class="note-link-span"><a class="fa-solid fa-circle-question" target="_blank"
35 </li>62 href="https://docs.sillytavern.app/"></a></span> - <span
36 <li>63 data-i18n="click these icons!">click these icons!</span>
37 <span data-i18n="Enter">Enter </span><code>/?</code><span data-i18n="in the chat bar"> in the chat bar</span>64 </li>
38 </li>65 <li>
39 <li>66 <span data-i18n="Enter">Enter </span><code>/?</code><span data-i18n="in the chat bar"> in the chat bar</span>
40 <a target="_blank" href="https://docs.sillytavern.app/" data-i18n="SillyTavern Documentation Site">67 </li>
41 SillyTavern Documentation Site68 <li>
42 </a>69 <a target="_blank" href="https://docs.sillytavern.app/" data-i18n="SillyTavern Documentation Site">
43 </li>70 SillyTavern Documentation Site
71 </a>
72 </li>
44</ul>73</ul>
4574
46<hr>75<hr>
47<h3 data-i18n="Still have questions?">Still have questions?</h3>76<h3 data-i18n="Still have questions?">Still have questions?</h3>
48<ul>77<ul>
49 <li>78 <li>
50 <a target="_blank" href="https://discord.gg/sillytavern" data-i18n="Join the SillyTavern Discord">79 <a target="_blank" href="https://discord.gg/sillytavern" data-i18n="Join the SillyTavern Discord">
51 Join the SillyTavern Discord80 Join the SillyTavern Discord
52 </a>81 </a>
53 </li>82 </li>
54 <li>83 <li>
55 <a target="_blank" href="https://github.com/SillyTavern/SillyTavern/issues" data-i18n="Post a GitHub issue">84 <a target="_blank" href="https://github.com/SillyTavern/SillyTavern/issues" data-i18n="Post a GitHub issue">
56 Post a GitHub issue85 Post a GitHub issue
57 </a>86 </a>
58 </li>87 </li>
59 <li>88 <li>
60 <a target="_blank" href="https://github.com/SillyTavern/SillyTavern#questions-or-suggestions" data-i18n="Contact the developers">89 <a target="_blank" href="https://github.com/SillyTavern/SillyTavern#questions-or-suggestions"
61 Contact the developers90 data-i18n="Contact the developers">
62 </a>91 Contact the developers
63 </li>92 </a>
93 </li>
64</ul>94</ul>
6595
public/style.css+3 -2
@@ -1208,8 +1208,9 @@ textarea.autoSetHeight {
1208}1208}
12091209
1210input,1210input,
1211select {1211select,
1212 font-family: var(--mainFontFamily);1212button {
1213 font-family: var(--mainFontFamily), sans-serif;
1213 font-size: var(--mainFontSize);1214 font-size: var(--mainFontSize);
1214 color: var(--SmartThemeBodyColor);1215 color: var(--SmartThemeBodyColor);
1215}1216}