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) {
293293 }
294294});
295295
296296DOMPurify.addHook('uponSanitizeAttribute', (_node, data, config) => {
297297 if (!config['MESSAGE_SANITIZE']) {
298298 return;
299299 }
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+
300307 switch (data.attrName) {
301308 case 'class': {
302309 if (data.attrValue) {
@@ -650,7 +657,8 @@ async function getSystemMessages() {
650657 force_avatar: system_avatar,
651658 is_user: false,
652659 is_system: true,
653- mes: await renderTemplateAsync('welcome', { displayVersion }),
660+ uses_system_ui: true,
661+ mes: await renderTemplateAsync('welcome', { displayVersion } ),
654662 },
655663 group: {
656664 name: systemUserName,
@@ -1916,9 +1924,10 @@ export async function sendTextareaMessage() {
19161924 * @param {boolean} isSystem If the message was sent by the system
19171925 * @param {boolean} isUser If the message was sent by the user
19181926 * @param {number} messageId Message index in chat array
1927+ * @param {object} [sanitizerOverrides] DOMPurify sanitizer option overrides
19191928 * @returns {string} HTML string
19201929 */
19211930export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, sanitizerOverrides = {}) {
19221931 if (!mes) {
19231932 return '';
19241933 }
@@ -2029,7 +2038,7 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId) {
20292038 }
20302039
20312040 /** @type {any} */
20322041 const config = { MESSAGE_SANITIZE: true, ADD_TAGS: ['custom-style'], ...sanitizerOverrides };
20332042 mes = encodeStyleTags(mes);
20342043 mes = DOMPurify.sanitize(mes, config);
20352044 mes = decodeStyleTags(mes);
@@ -2234,6 +2243,18 @@ export function addCopyToCodeBlocks(messageElement) {
22342243}
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+ */
22372258export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll = true, insertBefore = null, forceId = null, showSwipes = true } = {}) {
22382259 let messageText = mes['mes'];
22392260 const momentDate = timestampToMoment(mes.send_date);
@@ -2262,7 +2283,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
22622283 } else if (this_chid === undefined) {
22632284 avatarImg = system_avatar;
22642285 } else {
22652286 if (characters[this_chid].avatar !== 'none') {
22662287 avatarImg = getThumbnailUrl('avatar', characters[this_chid].avatar);
22672288 } else {
22682289 avatarImg = default_avatar;
@@ -2277,12 +2298,16 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
22772298 avatarImg = mes['force_avatar'];
22782299 }
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+
22802304 messageText = messageFormatting(
22812305 messageText,
22822306 mes.name,
22832307 isSystem,
22842308 mes.is_user,
22852309 chat.indexOf(mes),
2310+ sanitizerOverrides,
22862311 );
22872312 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1);
22882313 let bookmarkLink = mes?.extra?.bookmark_link ?? '';
@@ -2330,7 +2355,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
23302355 }
23312356
23322357 //shows or hides the Prompt display button
23332358 let mesIdToFind = type === 'swipe' ? params.mesId - 1 : params.mesId; //Number(newMessage.attr('mesId'));
23342359
23352360 //if we have itemized messages, and the array isn't null..
23362361 if (params.isUser === false && Array.isArray(itemizedPrompts) && itemizedPrompts.length > 0) {
@@ -2651,7 +2676,7 @@ export function sendSystemMessage(type, text, extra = {}) {
26512676 newMessage.mes = text;
26522677 }
26532678
26542679 if (type === system_message_types.SLASH_COMMANDS) {
26552680 newMessage.mes = getSlashCommandsHelp();
26562681 }
26572682
@@ -2665,7 +2690,7 @@ export function sendSystemMessage(type, text, extra = {}) {
26652690 chat.push(newMessage);
26662691 addOneMessage(newMessage);
26672692 is_send_press = false;
26682693 if (type === system_message_types.SLASH_COMMANDS) {
26692694 const browser = new SlashCommandBrowser();
26702695 const spinner = document.querySelector('#chat .last_mes .custom-slashHelp');
26712696 const parent = spinner.parentElement;
@@ -7854,7 +7879,7 @@ function openAlternateGreetings() {
78547879 if (menu_type !== 'create') {
78557880 await createOrEditCharacter();
78567881 }
78577882 },
78587883 });
78597884
78607885 for (let index = 0; index < getArray().length; index++) {
@@ -9070,6 +9095,90 @@ function doTogglePanels() {
90709095 return '';
90719096}
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+ */
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+
90739182function addDebugFunctions() {
90749183 const doBackfill = async () => {
90759184 for (const message of chat) {
@@ -10659,69 +10768,8 @@ jQuery(async function () {
1065910768 stopScriptExecution();
1066010769 });
1066110770
1066210771 $('.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- });
1072510773
1072610774 $('html').on('touchstart mousedown', function (e) {
1072710775 var clickTarget = $(e.target);
public/scripts/templates/welcome.html+77 -47
@@ -1,65 +1,95 @@
11<h3>
22 <span id="version_display_welcome">{{displayVersion}}</span>
33</h3>
44<a href="https://docs.sillytavern.app/usage/update/" target="_blank" data-i18n="Want to update?">
55 Want to update?
66</a>
77<hr>
88<h3 data-i18n="How to start chatting?">How to start chatting?</h3>
99<ol>
1010 <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>
1515 </libutton>
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>
1629</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>
2336 </ibutton>
2437 <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+
3057<hr>
3158<h3 data-i18n="Confused or lost?">Confused or lost?</h3>
3259<ul>
3360 <li>
3461 <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>
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>
3865 </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 Site
68+ <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>
4473</ul>
4574
4675<hr>
4776<h3 data-i18n="Still have questions?">Still have questions?</h3>
4877<ul>
4978 <li>
5079 <a target="_blank" href="https://discord.gg/sillytavern" data-i18n="Join the SillyTavern Discord">
5180 Join the SillyTavern Discord
5281 </a>
5382 </li>
5483 <li>
5584 <a target="_blank" href="https://github.com/SillyTavern/SillyTavern/issues" data-i18n="Post a GitHub issue">
5685 Post a GitHub issue
5786 </a>
5887 </li>
5988 <li>
6089 <a target="_blank" href="https://github.com/SillyTavern/SillyTavern#questions-or-suggestions" data-i18n="Contact the developers">
6190 data-i18n="Contact the developers">
62- </a>
91+ Contact the developers
6392 </lia>
93+ </li>
6494</ul>
6595
public/style.css+3 -2
@@ -1208,8 +1208,9 @@ textarea.autoSetHeight {
12081208}
12091209
12101210input,
1211-select {
1211+select,
1212- font-family: var(--mainFontFamily);
1212+button {
1213+ font-family: var(--mainFontFamily), sans-serif;
12131214 font-size: var(--mainFontSize);
12141215 color: var(--SmartThemeBodyColor);
12151216}