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 -90Showing whitespace changes
public/script.js+119 -71
@@ -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,6 +657,7 @@ async function getSystemMessages() {
650657 force_avatar: system_avatar,
651658 is_user: false,
652659 is_system: true,
660+ uses_system_ui: true,
653661 mes: await renderTemplateAsync('welcome', { displayVersion }),
654662 },
655663 group: {
@@ -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+41 -17
@@ -4,30 +4,56 @@
44<a href="https://docs.sillytavern.app/usage/update/" target="_blank" data-i18n="Want to update?">
55 Want to update?
66</a>
7-<hr>
87<h3 data-i18n="How to start chatting?">How to start chatting?</h3>
98<ol>
109 <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>.
1219 </li>
1320 <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>
1527 </li>
1628</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>
1743<div>
1844 <span data-i18n="You can browse a list ofGo bundledto charactersthe">Go into the"</span>
19- You can browse a list of bundled characters in the
45+
20- </span>
46+ <i data-i18n="Download Extensions & Assets">Download Extensions & Assets</i>
2147 <ispan data-i18n="Download Extensions &menu Assetswithin">menu within</span>
22- Download Extensions & Assets
48+
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 within
51+ <span data-i18n="[title]Extensions">Extensions</span>
2652 </spanbutton>
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>
2955</div>
30-<hr>
56+
3157<h3 data-i18n="Confused or lost?">Confused or lost?</h3>
3258<ul>
3359 <li>
@@ -43,7 +69,6 @@
4369 </li>
4470</ul>
4571
46-<hr>
4772<h3 data-i18n="Still have questions?">Still have questions?</h3>
4873<ul>
4974 <li>
@@ -62,4 +87,3 @@
6287 </a>
6388 </li>
6489</ul>
65-
public/style.css+9 -2
@@ -314,6 +314,12 @@ input[type='checkbox']:focus-visible {
314314 display: inline-block;
315315}
316316
317+.mes_text ol,
318+.mes_text ul {
319+ margin-top: 5px;
320+ margin-bottom: 5px;
321+}
322+
317323.mes_text br,
318324.mes_bias br {
319325 content: ' ';
@@ -1208,8 +1214,9 @@ textarea.autoSetHeight {
12081214}
12091215
12101216input,
1211-select {
1217+select,
1212- font-family: var(--mainFontFamily);
1218+button {
1219+ font-family: var(--mainFontFamily), sans-serif;
12131220 font-size: var(--mainFontSize);
12141221 color: var(--SmartThemeBodyColor);
12151222}