Merge pull request #2949 from ceruleandeep/feature/clickableWelcome Add clickable buttons in Welcome chat message.

6ecfdea8ef6bb8f3ef3b1afb924a0b478a21042f

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
3 files changed, +169 -90Ignore whitespace
public/script.js+119 -71
@@ -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,6 +657,7 @@ 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,
660 uses_system_ui: true,
653 mes: await renderTemplateAsync('welcome', { displayVersion }),661 mes: await renderTemplateAsync('welcome', { displayVersion }),
654 },662 },
655 group: {663 group: {
@@ -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+41 -17
@@ -4,30 +4,56 @@
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>
8<h3 data-i18n="How to start chatting?">How to start chatting?</h3>7<h3 data-i18n="How to start chatting?">How to start chatting?</h3>
9<ol>8<ol>
10 <li>9 <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>10 <span data-i18n="Click _space">Click </span>
11 <button class="menu_button menu_button_icon drawer-opener inline-flex" data-target="sys-settings-button">
12 <i class="fa-solid fa-plug"></i>
13 <span data-i18n="[title]API Connections">API Connections</span>
14 </button>
15 <span data-i18n="and connect to an">and connect to an</span>
16 <a href="https://docs.sillytavern.app/usage/api-connections/" target="_blank">
17 <span class="fa-solid fa-circle-question"></span>
18 <span data-i18n="API">API</span></a>.
12 </li>19 </li>
13 <li>20 <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>21 <span data-i18n="Click _space">Click </span>
22 <button class="menu_button menu_button_icon drawer-opener inline-flex" data-target="rightNavHolder">
23 <i class="fa-solid fa-address-card"></i>
24 <span data-i18n="[title]Character Management">Character Management</span>
25 </button>
26 <span data-i18n="and pick a character."> and pick a character.</span>
15 </li>27 </li>
16</ol>28</ol>
29
30<div>
31 <span data-i18n="You can add more">You can add more</span>
32 <button class="open_characters_library menu_button menu_button_icon inline-flex">
33 <i class="fa-solid fa-image-portrait"></i>
34 <span data-i18n="Sample characters">Sample characters</span>
35 </button>
36 <span data-i18n="or">or</span>
37 <button class="external_import_button menu_button menu_button_icon inline-flex">
38 <i class="fa-solid fa-cloud-arrow-down"></i>
39 <span data-i18n="Import Characters">Import characters</span>
40 </button>
41 <span data-i18n="from other websites">from other websites.</span>
42</div>
17<div>43<div>
18 <span data-i18n="You can browse a list of bundled characters in the">44 <span data-i18n="Go to the">Go to the</span>
19 You can browse a list of bundled characters in the45
20 </span>46 <i data-i18n="Download Extensions & Assets">Download Extensions & Assets</i>
21 <i data-i18n="Download Extensions & Assets">47 <span data-i18n="menu within">menu within</span>
22 Download Extensions & Assets48
23 </i>49 <button class="menu_button menu_button_icon drawer-opener inline-flex" data-target="extensions-settings-button">
24 <span data-i18n="menu within">50 <i class="fa-solid fa-cubes"></i>
25 menu within51 <span data-i18n="[title]Extensions">Extensions</span>
26 </span>52 </button>
27 <code><i class="fa-solid fa-cubes"></i></code>53
28 <span>.</span>54 <span data-i18n="to install additional features.">to install additional features.</span>
29</div>55</div>
30<hr>56
31<h3 data-i18n="Confused or lost?">Confused or lost?</h3>57<h3 data-i18n="Confused or lost?">Confused or lost?</h3>
32<ul>58<ul>
33 <li>59 <li>
@@ -43,7 +69,6 @@
43 </li>69 </li>
44</ul>70</ul>
4571
46<hr>
47<h3 data-i18n="Still have questions?">Still have questions?</h3>72<h3 data-i18n="Still have questions?">Still have questions?</h3>
48<ul>73<ul>
49 <li>74 <li>
@@ -62,4 +87,3 @@
62 </a>87 </a>
63 </li>88 </li>
64</ul>89</ul>
65
public/style.css+9 -2
@@ -314,6 +314,12 @@ input[type='checkbox']:focus-visible {
314 display: inline-block;314 display: inline-block;
315}315}
316316
317.mes_text ol,
318.mes_text ul {
319 margin-top: 5px;
320 margin-bottom: 5px;
321}
322
317.mes_text br,323.mes_text br,
318.mes_bias br {324.mes_bias br {
319 content: ' ';325 content: ' ';
@@ -1208,8 +1214,9 @@ textarea.autoSetHeight {
1208}1214}
12091215
1210input,1216input,
1211select {1217select,
1212 font-family: var(--mainFontFamily);1218button {
1219 font-family: var(--mainFontFamily), sans-serif;
1213 font-size: var(--mainFontSize);1220 font-size: var(--mainFontSize);
1214 color: var(--SmartThemeBodyColor);1221 color: var(--SmartThemeBodyColor);
1215}1222}