Blame Raw
Cohee · 51ad27fb · · 4460 lines (167.8 KB)
2 contributors
1import { Fuse, Handlebars } from '../lib.js';
2
3import {
4 saveSettingsDebounced,
5 scrollChatToBottom,
6 characters,
7 reloadMarkdownProcessor,
8 reloadCurrentChat,
9 getRequestHeaders,
10 substituteParams,
11 eventSource,
12 event_types,
13 getCurrentChatId,
14 printCharactersDebounced,
15 setCharacterId,
16 setEditedMessageId,
17 chat,
18 getFirstDisplayedMessageId,
19 showMoreMessages,
20 saveSettings,
21 saveChatConditional,
22 setAnimationDuration,
23 ANIMATION_DURATION_DEFAULT,
24 setActiveGroup,
25 setActiveCharacter,
26 entitiesFilter,
27 doNewChat,
28 online_status,
29 messageFormatting,
30 extension_prompt_types,
31 extension_prompt_roles,
32 deleteMessage,
33 settingsReady,
34} from '../script.js';
35import { isMobile, initMovingUI, favsToHotswap } from './RossAscends-mods.js';
36import {
37 groups,
38 resetSelectedGroup,
39} from './group-chats.js';
40import {
41 instruct_presets,
42 loadInstructMode,
43 names_behavior_types,
44 selectInstructPreset,
45 updateBindModelTemplatesState,
46} from './instruct-mode.js';
47
48import { getTagsList, tag_import_setting, tag_map, tag_sort_mode, tags } from './tags.js';
49import { tokenizers } from './tokenizers.js';
50import { BIAS_CACHE } from './logit-bias.js';
51import { renderTemplateAsync } from './templates.js';
52
53import { countOccurrences, debounce, delay, download, getFileText, getSanitizedFilename, getStringHash, isOdd, isTrueBoolean, onlyUnique, resetScrollHeight, shuffle, sortMoments, stringToRange, timestampToMoment } from './utils.js';
54import { FILTER_TYPES } from './filters.js';
55import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
56import { SlashCommand } from './slash-commands/SlashCommand.js';
57import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
58import { AUTOCOMPLETE_SELECT_KEY, AUTOCOMPLETE_STATE, AUTOCOMPLETE_WIDTH } from './autocomplete/AutoComplete.js';
59import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
60import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
61import { POPUP_TYPE, callGenericPopup, fixToastrForDialogs } from './popup.js';
62import { loadSystemPrompts } from './sysprompt.js';
63import { fuzzySearchCategories } from './filters.js';
64import { accountStorage } from './util/AccountStorage.js';
65import { extractDominantColor, generateThemePalette, deriveBackgroundName } from './util/ThemeGenerator.js';
66import { DEFAULT_REASONING_TEMPLATE, loadReasoningTemplates } from './reasoning.js';
67import { bindModelTemplates } from './chat-templates.js';
68import { IMAGE_OVERSWIPE, MEDIA_DISPLAY } from './constants.js';
69import { t } from './i18n.js';
70import { getBackgroundPath, isCustomBackgroundUrl } from './backgrounds.js';
71import { persona_description_positions as _persona_description_positions } from './personas.js';
72
73export const toastPositionClasses = [
74 'toast-top-left',
75 'toast-top-center',
76 'toast-top-right',
77 'toast-bottom-left',
78 'toast-bottom-center',
79 'toast-bottom-right',
80];
81
82export const MAX_CONTEXT_DEFAULT = 8192;
83export const MAX_RESPONSE_DEFAULT = 2048;
84const MAX_CONTEXT_UNLOCKED = 512 * 1024;
85const MAX_RESPONSE_UNLOCKED = 64 * 1024;
86const unlockedMaxContextStep = 512;
87const maxContextMin = 512;
88const maxContextStep = 64;
89
90const defaultStoryString = '{{#if system}}{{system}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}\'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}';
91const defaultExampleSeparator = '***';
92const defaultChatStart = '***';
93const defaultToastPosition = 'toast-top-center';
94
95const avatar_styles = {
96 ROUND: 0,
97 RECTANGULAR: 1,
98 SQUARE: 2,
99 ROUNDED: 3,
100};
101
102export const chat_styles = {
103 DEFAULT: 0,
104 BUBBLES: 1,
105 DOCUMENT: 2,
106};
107
108export const send_on_enter_options = {
109 DISABLED: -1,
110 AUTO: 0,
111 ENABLED: 1,
112};
113
114export const persona_description_positions = _persona_description_positions;
115
116export const power_user = {
117 charListGrid: false,
118 tokenizer: tokenizers.BEST_MATCH,
119 token_padding: 64,
120 collapse_newlines: false,
121 pin_examples: false,
122 strip_examples: false,
123 trim_sentences: false,
124 always_force_name2: false,
125 user_prompt_bias: '',
126 show_user_prompt_bias: true,
127 auto_continue: {
128 enabled: false,
129 allow_chat_completions: false,
130 target_length: 400,
131 },
132 markdown_escape_strings: '',
133 chat_truncation: 100,
134 streaming_fps: 30,
135 smooth_streaming: false,
136 smooth_streaming_no_think: false,
137 smooth_streaming_speed: 50,
138 stream_fade_in: false,
139
140 fast_ui_mode: true,
141 avatar_style: avatar_styles.ROUND,
142 chat_display: chat_styles.DEFAULT,
143 toastr_position: defaultToastPosition,
144 chat_width: 50,
145 never_resize_avatars: false,
146 show_card_avatar_urls: false,
147 play_message_sound: false,
148 play_sound_unfocused: true,
149 auto_save_msg_edits: false,
150 confirm_message_delete: true,
151
152 sort_field: 'name',
153 sort_order: 'asc',
154 sort_rule: null,
155 font_scale: 1,
156 blur_strength: 10,
157 shadow_width: 2,
158
159 main_text_color: `${getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeBodyColor').trim()}`,
160 italics_text_color: `${getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeEmColor').trim()}`,
161 underline_text_color: `${getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeUnderlineColor').trim()}`,
162 quote_text_color: `${getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeQuoteColor').trim()}`,
163 blur_tint_color: `${getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeBlurTintColor').trim()}`,
164 chat_tint_color: `${getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeChatTintColor').trim()}`,
165 user_mes_blur_tint_color: `${getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeUserMesBlurTintColor').trim()}`,
166 bot_mes_blur_tint_color: `${getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeBotMesBlurTintColor').trim()}`,
167 shadow_color: `${getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeShadowColor').trim()}`,
168 border_color: `${getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeBorderColor').trim()}`,
169
170 custom_css: '',
171
172 waifuMode: false,
173 movingUI: false,
174 movingUIState: {},
175 movingUIPreset: '',
176 noShadows: false,
177 theme: 'Default (Dark) 1.7.1',
178
179 gestures: true,
180 auto_swipe: false,
181 auto_swipe_minimum_length: 0,
182 auto_swipe_blacklist: [],
183 auto_swipe_blacklist_threshold: 2,
184 auto_scroll_chat_to_bottom: true,
185 auto_fix_generated_markdown: true,
186 send_on_enter: send_on_enter_options.AUTO,
187 console_log_prompts: false,
188 request_token_probabilities: false,
189 show_group_chat_queue: false,
190 allow_name1_display: false,
191 allow_name2_display: false,
192 hotswap_enabled: true,
193 timer_enabled: true,
194 timestamps_enabled: true,
195 timestamp_model_icon: false,
196 mesIDDisplay_enabled: false,
197 hideChatAvatars_enabled: false,
198 max_context_unlocked: false,
199 message_token_count_enabled: false,
200 expand_message_actions: false,
201 enableZenSliders: false,
202 enableLabMode: false,
203 prefer_character_prompt: true,
204 prefer_character_jailbreak: true,
205 quick_continue: false,
206 quick_impersonate: false,
207 continue_on_send: false,
208 trim_spaces: true,
209 relaxed_api_urls: false,
210 world_import_dialog: true,
211 enable_auto_select_input: false,
212 enable_md_hotkeys: false,
213 tag_import_setting: tag_import_setting.ASK,
214 tag_sort_mode: tag_sort_mode.MANUAL,
215 disable_group_trimming: false,
216 single_line: false,
217
218 instruct: {
219 enabled: false,
220 preset: 'Alpaca',
221 input_sequence: '### Instruction:',
222 input_suffix: '',
223 output_sequence: '### Response:',
224 output_suffix: '',
225 system_sequence: '',
226 system_suffix: '',
227 last_system_sequence: '',
228 first_input_sequence: '',
229 first_output_sequence: '',
230 last_input_sequence: '',
231 last_output_sequence: '',
232 story_string_prefix: '',
233 story_string_suffix: '',
234 stop_sequence: '',
235 wrap: true,
236 macro: true,
237 names_behavior: names_behavior_types.FORCE,
238 activation_regex: '',
239 bind_to_context: false,
240 user_alignment_message: '',
241 system_same_as_user: false,
242 /** @deprecated Use output_suffix instead */
243 separator_sequence: '',
244 sequences_as_stop_strings: true,
245 },
246
247 context: {
248 preset: 'Default',
249 story_string: defaultStoryString,
250 chat_start: defaultChatStart,
251 example_separator: defaultExampleSeparator,
252 use_stop_strings: true,
253 names_as_stop_strings: true,
254 story_string_position: extension_prompt_types.IN_PROMPT,
255 story_string_role: extension_prompt_roles.SYSTEM,
256 story_string_depth: 1,
257 },
258
259 instruct_derived: false,
260 context_derived: false,
261 context_size_derived: false,
262 /** User-defined model identifier / chat template hash to instruct/context template mappings */
263 model_templates_mappings: {},
264 /** The chat template hash of the currently loaded model, if any; used when deriving mappings */
265 chat_template_hash: '',
266
267 sysprompt: {
268 enabled: true,
269 name: 'Neutral - Chat',
270 content: 'Write {{char}}\'s next reply in a fictional chat between {{char}} and {{user}}.',
271 post_history: '',
272 },
273
274 reasoning: {
275 name: DEFAULT_REASONING_TEMPLATE,
276 auto_parse: false,
277 add_to_prompts: false,
278 auto_expand: false,
279 show_hidden: false,
280 prefix: '<think>',
281 suffix: '</think>',
282 separator: '\n',
283 max_additions: 1,
284 },
285
286 personas: {},
287 default_persona: null,
288 persona_descriptions: {},
289
290 persona_description: '',
291 persona_description_position: persona_description_positions.IN_PROMPT,
292 persona_description_role: 0,
293 persona_description_depth: 2,
294 persona_description_lorebook: '',
295 persona_show_notifications: true,
296 persona_sort_order: 'asc',
297
298 custom_stopping_strings: '',
299 custom_stopping_strings_macro: true,
300 fuzzy_search: false,
301 encode_tags: false,
302 experimental_macro_engine: true,
303 servers: [],
304 bogus_folders: false,
305 zoomed_avatar_magnification: false,
306 show_tag_filters: false,
307 aux_field: 'character_version',
308 stscript: {
309 matching: 'fuzzy',
310 autocomplete: {
311 state: AUTOCOMPLETE_STATE.ALWAYS,
312 autoHide: false,
313 style: 'theme',
314 font: {
315 scale: 0.8,
316 },
317 width: {
318 left: AUTOCOMPLETE_WIDTH.CHAT,
319 right: AUTOCOMPLETE_WIDTH.CHAT,
320 },
321 select: AUTOCOMPLETE_SELECT_KEY.TAB + AUTOCOMPLETE_SELECT_KEY.ENTER,
322 /** Whether to show macro autocomplete in all macro-enabled fields (not just expanded editors) */
323 showInAllMacroFields: false,
324 },
325 parser: {
326 /**@type {Object.<PARSER_FLAG,boolean>} */
327 flags: {},
328 },
329 },
330 restore_user_input: true,
331 reduced_motion: false,
332 compact_input_area: true,
333 show_swipe_num_all_messages: false,
334 auto_connect: false,
335 auto_load_chat: false,
336 forbid_external_media: true,
337 external_media_allowed_overrides: [],
338 external_media_forbidden_overrides: [],
339 pin_styles: true,
340 click_to_edit: false,
341 media_display: MEDIA_DISPLAY.LIST,
342 image_overswipe: IMAGE_OVERSWIPE.GENERATE,
343};
344
345let themes = [];
346let movingUIPresets = [];
347/** @type {ContextSettings[]} */
348export let context_presets = [];
349
350const storage_keys = {
351 storyStringValidationCache: 'StoryStringValidationCache',
352};
353
354const contextControls = [
355 // Power user context scoped settings
356 { id: 'context_story_string', property: 'story_string', isCheckbox: false, isGlobalSetting: false },
357 { id: 'context_example_separator', property: 'example_separator', isCheckbox: false, isGlobalSetting: false },
358 { id: 'context_chat_start', property: 'chat_start', isCheckbox: false, isGlobalSetting: false },
359 { id: 'context_use_stop_strings', property: 'use_stop_strings', isCheckbox: true, isGlobalSetting: false, defaultValue: false },
360 { id: 'context_names_as_stop_strings', property: 'names_as_stop_strings', isCheckbox: true, isGlobalSetting: false, defaultValue: true },
361 { id: 'context_story_string_position', property: 'story_string_position', isCheckbox: false, isGlobalSetting: false, defaultValue: extension_prompt_types.IN_PROMPT, trigger: true },
362 { id: 'context_story_string_depth', property: 'story_string_depth', isCheckbox: false, isGlobalSetting: false, defaultValue: 1 },
363 { id: 'context_story_string_role', property: 'story_string_role', isCheckbox: false, isGlobalSetting: false, defaultValue: extension_prompt_roles.SYSTEM },
364
365 // Existing power user settings
366 { id: 'always-force-name2-checkbox', property: 'always_force_name2', isCheckbox: true, isGlobalSetting: true, defaultValue: true },
367 { id: 'trim_sentences_checkbox', property: 'trim_sentences', isCheckbox: true, isGlobalSetting: true, defaultValue: false },
368 { id: 'single_line', property: 'single_line', isCheckbox: true, isGlobalSetting: true, defaultValue: false },
369];
370
371let browser_has_focus = true;
372const debug_functions = [];
373
374const setHotswapsDebounced = debounce(favsToHotswap);
375
376/**
377 * Plays the message sound if enabled in power user settings.
378 * Passes through the `force` parameter to override settings.
379 * @param {object} [param] Arguments object.
380 * @param {boolean} [param.force] Whether to force play the sound.
381 * @returns {void}
382 */
383export function playMessageSound({ force } = {}) {
384 if (!power_user.play_message_sound && !force) {
385 return;
386 }
387
388 if (power_user.play_sound_unfocused && browser_has_focus && !force) {
389 return;
390 }
391
392 const audio = document.getElementById('audio_message_sound');
393 if (audio instanceof HTMLAudioElement) {
394 audio.volume = 0.8;
395 audio.pause();
396 audio.currentTime = 0;
397 audio.play();
398 }
399}
400
401/**
402 * Replaces consecutive newlines with a single newline.
403 * @param {string} x String to be processed.
404 * @returns {string} Processed string.
405 * @example
406 * collapseNewlines("\n\n\n"); // "\n"
407 */
408export function collapseNewlines(x) {
409 return x.replaceAll(/\n+/g, '\n');
410}
411
412/**
413 * Fix formatting problems in markdown.
414 * @param {string} text Text to be processed.
415 * @param {boolean} forDisplay Whether the text is being processed for display.
416 * @returns {string} Processed text.
417 * @example
418 * "^example * text*\n" // "^example *text*\n"
419 * "^*example * text\n"// "^*example* text\n"
420 * "^example *text *\n" // "^example *text*\n"
421 * "^* example * text\n" // "^*example* text\n"
422 * // take note that the side you move the asterisk depends on where its pairing is
423 * // i.e. both of the following strings have the same broken asterisk ' * ',
424 * // but you move the first to the left and the second to the right, to match the non-broken asterisk
425 * "^example * text*\n" // "^*example * text\n"
426 * // and you HAVE to handle the cases where multiple pairs of asterisks exist in the same line
427 * "^example * text* * harder problem *\n" // "^example *text* *harder problem*\n"
428 */
429export function fixMarkdown(text, forDisplay) {
430 // Find pairs of formatting characters and capture the text in between them
431 const format = /([*_]{1,2})([\s\S]*?)\1/gm;
432 let matches = [];
433 let match;
434 while ((match = format.exec(text)) !== null) {
435 matches.push(match);
436 }
437
438 // Iterate through the matches and replace adjacent spaces immediately beside formatting characters
439 let newText = text;
440 for (let i = matches.length - 1; i >= 0; i--) {
441 let matchText = matches[i][0];
442 let replacementText = matchText.replace(/(\*|_)([\t \u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\ufeff]+)|([\t \u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\ufeff]+)(\*|_)/g, '$1$4');
443 newText = newText.slice(0, matches[i].index) + replacementText + newText.slice(matches[i].index + matchText.length);
444 }
445
446 // Don't auto-fix asterisks if this is a message clean-up procedure.
447 // It botches the continue function. Apply this to display only.
448 if (!forDisplay) {
449 return newText;
450 }
451
452 const splitText = newText.split('\n');
453
454 // Fix asterisks, and quotes that are not paired
455 for (let index = 0; index < splitText.length; index++) {
456 const line = splitText[index];
457 const charsToCheck = ['*', '"'];
458 for (const char of charsToCheck) {
459 if (line.includes(char) && isOdd(countOccurrences(line, char))) {
460 splitText[index] = line.trimEnd() + char;
461 }
462 }
463 }
464
465 newText = splitText.join('\n');
466
467 return newText;
468}
469
470function switchHotswap() {
471 $('body').toggleClass('no-hotswap', !power_user.hotswap_enabled);
472 $('#hotswapEnabled').prop('checked', power_user.hotswap_enabled);
473}
474
475function switchTimer() {
476 $('body').toggleClass('no-timer', !power_user.timer_enabled);
477 $('#messageTimerEnabled').prop('checked', power_user.timer_enabled);
478}
479
480function switchTimestamps() {
481 $('body').toggleClass('no-timestamps', !power_user.timestamps_enabled);
482 $('#messageTimestampsEnabled').prop('checked', power_user.timestamps_enabled);
483}
484
485function switchIcons() {
486 $('body').toggleClass('no-modelIcons', !power_user.timestamp_model_icon);
487 $('#messageModelIconEnabled').prop('checked', power_user.timestamp_model_icon);
488}
489
490function switchTokenCount() {
491 $('body').toggleClass('no-tokenCount', !power_user.message_token_count_enabled);
492 $('#messageTokensEnabled').prop('checked', power_user.message_token_count_enabled);
493}
494
495function switchMesIDDisplay() {
496 $('body').toggleClass('no-mesIDDisplay', !power_user.mesIDDisplay_enabled);
497 $('#mesIDDisplayEnabled').prop('checked', power_user.mesIDDisplay_enabled);
498}
499
500function switchHideChatAvatars() {
501 $('body').toggleClass('hideChatAvatars', power_user.hideChatAvatars_enabled);
502 $('#hideChatAvatarsEnabled').prop('checked', power_user.hideChatAvatars_enabled);
503}
504
505function switchMessageActions() {
506 $('body').toggleClass('expandMessageActions', power_user.expand_message_actions);
507 $('#expandMessageActions').prop('checked', power_user.expand_message_actions);
508 $('.extraMesButtons, .extraMesButtonsHint').removeAttr('style');
509}
510
511function switchReducedMotion() {
512 const osReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
513 if (osReduced) {
514 power_user.reduced_motion = true;
515 }
516 jQuery.fx.off = power_user.reduced_motion;
517 const overrideDuration = power_user.reduced_motion ? 0 : ANIMATION_DURATION_DEFAULT;
518 setAnimationDuration(overrideDuration);
519 $('#reduced_motion').prop('checked', power_user.reduced_motion);
520 $('#reduced_motion').prop('disabled', osReduced);
521 $('#reduced_motion').closest('label').attr('title',
522 osReduced
523 ? t`Controlled by your operating system's reduced motion setting`
524 : t`Disable animations and transitions`,
525 );
526 $('body').toggleClass('reduced-motion', power_user.reduced_motion);
527}
528
529function switchCompactInputArea() {
530 $('#send_form').toggleClass('compact', power_user.compact_input_area);
531 $('#compact_input_area').prop('checked', power_user.compact_input_area);
532}
533
534function switchSwipeNumAllMessages() {
535 $('#show_swipe_num_all_messages').prop('checked', power_user.show_swipe_num_all_messages);
536 $('body').toggleClass('swipeAllMessages', !!power_user.show_swipe_num_all_messages);
537}
538
539var originalSliderValues = [];
540
541async function switchLabMode({ noReset = false } = {}) {
542 /* if (power_user.enableZenSliders && power_user.enableLabMode) {
543 toastr.warning("Can't start Lab Mode while Zen Sliders are active")
544 return
545 //$("#enableZenSliders").trigger('click')
546 }
547 */
548 await delay(100);
549 $('body').toggleClass('enableLabMode', power_user.enableLabMode);
550 $('#enableLabMode').prop('checked', power_user.enableLabMode);
551
552 if (power_user.enableLabMode) {
553 //save all original slider values into an array
554 $('#advanced-ai-config-block input').each(function () {
555 let id = $(this).attr('id');
556 let min = $(this).attr('min');
557 let max = $(this).attr('max');
558 let step = $(this).attr('step');
559 originalSliderValues.push({ id, min, max, step });
560 });
561 //console.log(originalSliderValues)
562 //remove limits on all inputs and hide sliders
563 $('#advanced-ai-config-block input')
564 .attr('min', '-99999')
565 .attr('max', '99999')
566 .attr('step', '0.001');
567 $('#labModeWarning').removeClass('displayNone');
568 //$("#advanced-ai-config-block input[type='range']").hide()
569
570 $('#amount_gen_counter').attr('min', '1')
571 .attr('max', '99999')
572 .attr('step', '1');
573 $('#amount_gen').attr('min', '1')
574 .attr('max', '99999')
575 .attr('step', '1');
576 } else if (!noReset) {
577 //re apply the original sliders values to each input
578 originalSliderValues.forEach(function (slider) {
579 $('#' + slider.id)
580 .attr('min', slider.min)
581 .attr('max', slider.max)
582 .attr('step', slider.step)
583 .trigger('input');
584 });
585 $('#advanced-ai-config-block input[type=\'range\']').show();
586 $('#labModeWarning').addClass('displayNone');
587
588 // To set the correct amount_gen back, we just call the function calculating it correctly
589 switchMaxContextSize();
590 }
591}
592
593async function switchZenSliders() {
594 await delay(100);
595 $('body').toggleClass('enableZenSliders', power_user.enableZenSliders);
596 $('#enableZenSliders').prop('checked', power_user.enableZenSliders);
597
598 if (power_user.enableZenSliders) {
599 $('#clickSlidersTips').hide();
600 $('#pro-settings-block input[type=\'number\']').hide();
601 //hide number inputs that are not 'seed' inputs
602 $(`#textgenerationwebui_api-settings :input[type='number']:not([id^='seed']):not([id^='n_']),
603 #kobold_api-settings :input[type='number']:not([id^='seed'])`).hide();
604 //hide original sliders
605 $(`#textgenerationwebui_api-settings input[type='range'],
606 #kobold_api-settings input[type='range'],
607 #pro-settings-block input[type='range']:not(#max_context)`) //exclude max context because its creation is handled by switchMaxContext()
608 .hide()
609 .each(function () {
610 //make a zen slider for each original slider
611 CreateZenSliders($(this));
612 });
613 //this is for when zensliders is toggled after pageload
614 switchMaxContextSize();
615 } else {
616 $('#clickSlidersTips').show();
617 revertOriginalSliders();
618 }
619
620 function revertOriginalSliders() {
621 $('#pro-settings-block input[type=\'number\']').show();
622 $(`#textgenerationwebui_api-settings input[type='number'],
623 #kobold_api-settings input[type='number']`).show();
624 $(`#textgenerationwebui_api-settings input[type='range'],
625 #kobold_api-settings input[type='range'],
626 #pro-settings-block input[type='range']`).each(function () {
627 $(this).show();
628 });
629 $('div[id$="_zenslider"]').remove();
630 }
631}
632async function CreateZenSliders(elmnt) {
633 var originalSlider = elmnt;
634 var sliderID = originalSlider.attr('id');
635 var sliderMin = Number(originalSlider.attr('min'));
636 var sliderMax = Number(originalSlider.attr('max'));
637 var sliderValue = originalSlider.val();
638 var sliderRange = sliderMax - sliderMin;
639 var numSteps = 20;
640 var decimals = 2;
641 var offVal, allVal;
642 var stepScale;
643 var steps;
644 if (sliderID == 'amount_gen') {
645 decimals = 0;
646 steps = [16, 50, 100, 150, 200, 256, 300, 400, 512, 1024];
647 sliderMin = 0;
648 sliderMax = steps.length - 1;
649 stepScale = 1;
650 numSteps = 10;
651 sliderValue = steps.indexOf(Number(sliderValue));
652 if (sliderValue === -1) { sliderValue = 4; } // default to '200' if origSlider has value we can't use
653 }
654 if (sliderID == 'rep_pen_range_textgenerationwebui') {
655 if (power_user.max_context_unlocked) {
656 steps = [0, 256, 512, 768, 1024, 2048, 4096, 8192, 16355, 24576, 32768, 49152, 65536, -1];
657 numSteps = 13;
658 allVal = 13;
659 } else {
660 steps = [0, 256, 512, 768, 1024, 2048, 4096, 8192, -1];
661 numSteps = 8;
662 allVal = 8;
663 }
664 decimals = 0;
665 offVal = 0;
666 sliderMin = 0;
667 sliderMax = steps.length - 1;
668 stepScale = 1;
669 sliderValue = steps.indexOf(Number(sliderValue));
670 if (sliderValue === -1) { sliderValue = allVal; } // default to allValue if origSlider has value we can't use
671 }
672 //customize decimals
673 if (sliderID == 'max_context' ||
674 sliderID == 'mirostat_mode_textgenerationwebui' ||
675 sliderID == 'mirostat_tau_textgenerationwebui' ||
676 sliderID == 'top_k_textgenerationwebui' ||
677 sliderID == 'num_beams_textgenerationwebui' ||
678 sliderID == 'no_repeat_ngram_size_textgenerationwebui' ||
679 sliderID == 'min_length_textgenerationwebui' ||
680 sliderID == 'top_k' ||
681 sliderID == 'mirostat_mode_kobold' ||
682 sliderID == 'rep_pen_range' ||
683 sliderID == 'dry_allowed_length_textgenerationwebui' ||
684 sliderID == 'rep_pen_decay_textgenerationwebui' ||
685 sliderID == 'dry_penalty_last_n_textgenerationwebui' ||
686 sliderID == 'max_tokens_second_textgenerationwebui') {
687 decimals = 0;
688 }
689 if (sliderID == 'min_temp_textgenerationwebui' ||
690 sliderID == 'max_temp_textgenerationwebui' ||
691 sliderID == 'smoothing_curve_textgenerationwebui' ||
692 sliderID == 'smoothing_factor_textgenerationwebui' ||
693 sliderID == 'dry_multiplier_textgenerationwebui' ||
694 sliderID == 'dry_base_textgenerationwebui') {
695 decimals = 2;
696 }
697 if (sliderID == 'eta_cutoff_textgenerationwebui' ||
698 sliderID == 'epsilon_cutoff_textgenerationwebui') {
699 numSteps = 50;
700 decimals = 1;
701 }
702 if (sliderID == 'nsigma') {
703 numSteps = 50;
704 decimals = 1;
705 }
706 //customize steps
707 if (sliderID == 'mirostat_mode_textgenerationwebui' ||
708 sliderID == 'mirostat_mode_kobold') {
709 numSteps = 2;
710 }
711 if (sliderID == 'encoder_rep_pen_textgenerationwebui') {
712 numSteps = 14;
713 }
714 if (sliderID == 'max_context') {
715 numSteps = 15;
716 }
717 if (sliderID == 'mirostat_tau_textgenerationwebui' ||
718 sliderID == 'top_k_textgenerationwebui' ||
719 sliderID == 'num_beams_textgenerationwebui' ||
720 sliderID == 'no_repeat_ngram_size_textgenerationwebui' ||
721 sliderID == 'epsilon_cutoff_textgenerationwebui' ||
722 sliderID == 'tfs_textgenerationwebui' ||
723 sliderID == 'min_p_textgenerationwebui' ||
724 sliderID == 'temp_textgenerationwebui' ||
725 sliderID == 'temp') {
726 numSteps = 20;
727 }
728 if (sliderID == 'mirostat_eta_textgenerationwebui' ||
729 sliderID == 'penalty_alpha_textgenerationwebui' ||
730 sliderID == 'length_penalty_textgenerationwebui' ||
731 sliderID == 'min_temp_textgenerationwebui' ||
732 sliderID == 'max_temp_textgenerationwebui') {
733 numSteps = 50;
734 }
735 //customize off values
736 if (sliderID == 'presence_pen_textgenerationwebui' ||
737 sliderID == 'freq_pen_textgenerationwebui' ||
738 sliderID == 'mirostat_mode_textgenerationwebui' ||
739 sliderID == 'mirostat_mode_kobold' ||
740 sliderID == 'mirostat_tau_textgenerationwebui' ||
741 sliderID == 'mirostat_tau_kobold' ||
742 sliderID == 'mirostat_eta_textgenerationwebui' ||
743 sliderID == 'mirostat_eta_kobold' ||
744 sliderID == 'min_p_textgenerationwebui' ||
745 sliderID == 'min_p' ||
746 sliderID == 'no_repeat_ngram_size_textgenerationwebui' ||
747 sliderID == 'penalty_alpha_textgenerationwebui' ||
748 sliderID == 'length_penalty_textgenerationwebui' ||
749 sliderID == 'epsilon_cutoff_textgenerationwebui' ||
750 sliderID == 'nsigma' ||
751 sliderID == 'rep_pen_range' ||
752 sliderID == 'eta_cutoff_textgenerationwebui' ||
753 sliderID == 'top_a_textgenerationwebui' ||
754 sliderID == 'top_a' ||
755 sliderID == 'top_k_textgenerationwebui' ||
756 sliderID == 'top_k' ||
757 sliderID == 'rep_pen_slope' ||
758 sliderID == 'smoothing_factor_textgenerationwebui' ||
759 sliderID == 'smoothing_curve_textgenerationwebui' ||
760 sliderID == 'skew_textgenerationwebui' ||
761 sliderID == 'dry_multiplier_textgenerationwebui' ||
762 sliderID == 'min_length_textgenerationwebui') {
763 offVal = 0;
764 }
765 if (sliderID == 'rep_pen_textgenerationwebui' ||
766 sliderID == 'rep_pen' ||
767 sliderID == 'tfs_textgenerationwebui' ||
768 sliderID == 'tfs' ||
769 sliderID == 'top_p_textgenerationwebui' ||
770 sliderID == 'top_p' ||
771 sliderID == 'typical_p_textgenerationwebui' ||
772 sliderID == 'typical_p' ||
773 sliderID == 'encoder_rep_pen_textgenerationwebui' ||
774 sliderID == 'temp_textgenerationwebui' ||
775 sliderID == 'temp' ||
776 sliderID == 'min_temp_textgenerationwebui' ||
777 sliderID == 'max_temp_textgenerationwebui' ||
778 sliderID == 'dynatemp_exponent_textgenerationwebui' ||
779 sliderID == 'guidance_scale_textgenerationwebui' ||
780 sliderID == 'rep_pen_slope_textgenerationwebui' ||
781 sliderID == 'guidance_scale') {
782 offVal = 1;
783 }
784 if (sliderID == 'guidance_scale_textgenerationwebui') {
785 numSteps = 78;
786 }
787 if (sliderID == 'top_k_textgenerationwebui') {
788 sliderMin = 0;
789 }
790 //customize amt gen steps
791 if (sliderID !== 'amount_gen' && sliderID !== 'rep_pen_range_textgenerationwebui') {
792 stepScale = sliderRange / numSteps;
793 }
794 var newSlider = $('<div>')
795 .attr('id', `${sliderID}_zenslider`)
796 .css('width', '100%')
797 .insertBefore(originalSlider);
798 newSlider.slider({
799 value: sliderValue,
800 step: stepScale,
801 min: sliderMin,
802 max: sliderMax,
803 create: async function () {
804 await delay(100);
805 var handle = $(this).find('.ui-slider-handle');
806 var handleText, stepNumber, leftMargin;
807
808 //handling creation of amt_gen
809 if (newSlider.attr('id') == 'amount_gen_zenslider') {
810 handleText = steps[sliderValue];
811 stepNumber = sliderValue;
812 leftMargin = ((stepNumber) / numSteps) * 50 * -1;
813 handle.text(handleText)
814 .css('margin-left', `${leftMargin}px`);
815 //console.log(`${newSlider.attr('id')} initial value:${handleText}, stepNum:${stepNumber}, numSteps:${numSteps}, left-margin:${leftMargin}`)
816 } else if (newSlider.attr('id') == 'rep_pen_range_textgenerationwebui_zenslider') {
817 //handling creation of rep_pen_range for ooba
818 if ($('#rep_pen_range_textgenerationwebui_zensliders').length !== 0) {
819 $('#rep_pen_range_textgenerationwebui_zensliders').remove();
820 }
821 handleText = steps[sliderValue];
822 stepNumber = sliderValue;
823 leftMargin = ((stepNumber) / numSteps) * 50 * -1;
824 if (sliderValue === offVal) {
825 handleText = 'Off';
826 handle.css('color', 'rgba(128,128,128,0.5)');
827 } else if (sliderValue === allVal) { handleText = 'All'; } else { handle.css('color', ''); }
828 handle.text(handleText)
829 .css('margin-left', `${leftMargin}px`);
830 //console.log(sliderValue, handleText, offVal, allVal)
831 //console.log(`${newSlider.attr('id')} sliderValue = ${sliderValue}, handleText:${handleText}, stepNum:${stepNumber}, numSteps:${numSteps}, left-margin:${leftMargin}`)
832 originalSlider.val(steps[sliderValue]);
833 } else {
834 //create all other sliders
835 var numVal = Number(sliderValue).toFixed(decimals);
836 offVal = Number(offVal).toFixed(decimals);
837 if (numVal === offVal) {
838 handle.text('Off').css('color', 'rgba(128,128,128,0.5)');
839 } else {
840 handle.text(numVal).css('color', '');
841 }
842 stepNumber = ((sliderValue - sliderMin) / stepScale);
843 leftMargin = (stepNumber / numSteps) * 50 * -1;
844 originalSlider.val(numVal)
845 .data('newSlider', newSlider);
846 //console.log(`${newSlider.attr('id')} sliderValue = ${sliderValue}, handleText:${handleText, numVal}, stepNum:${stepNumber}, numSteps:${numSteps}, left-margin:${leftMargin}`)
847 var isManualInput = false;
848 var valueBeforeManualInput;
849 handle.css('margin-left', `${leftMargin}px`)
850
851 .attr('contenteditable', 'true')
852 //these sliders need listeners for manual inputs
853 .on('click', function () {
854 //this just selects all the text in the handle so user can overwrite easily
855 //needed because JQUery UI uses left/right arrow keys as well as home/end to move the slider..
856 valueBeforeManualInput = newSlider.val();
857 console.log(valueBeforeManualInput);
858 let handleElement = handle.get(0);
859 let range = document.createRange();
860 range.selectNodeContents(handleElement);
861 let selection = window.getSelection();
862 selection.removeAllRanges();
863 selection.addRange(range);
864 })
865 .on('keyup', function (e) {
866 valueBeforeManualInput = numVal;
867 //console.log(valueBeforeManualInput, numVal, handleText);
868 isManualInput = true;
869 //allow enter to trigger slider update
870 if (e.key === 'Enter') {
871 e.preventDefault();
872 handle.trigger('blur');
873 }
874 })
875 //trigger slider changes when user clicks away
876 .on('mouseup blur', function () {
877 let manualInput = parseFloat(parseFloat(handle.text()).toFixed(decimals));
878 if (isManualInput) {
879 //disallow manual inputs outside acceptable range
880 if (manualInput >= sliderMin && manualInput <= sliderMax) {
881 //if value is ok, assign to slider and update handle text and position
882 newSlider.val(manualInput);
883 handleSlideEvent.call(newSlider, null, { value: manualInput }, 'manual');
884 valueBeforeManualInput = manualInput;
885 } else {
886 //if value not ok, warn and reset to last known valid value
887 toastr.warning(`Invalid value. Must be between ${sliderMin} and ${sliderMax}`);
888 console.log(valueBeforeManualInput);
889 newSlider.val(valueBeforeManualInput);
890 handle.text(valueBeforeManualInput);
891 handleSlideEvent.call(newSlider, null, { value: parseFloat(valueBeforeManualInput) }, 'manual');
892 }
893 }
894 isManualInput = false;
895 });
896 }
897 //zenSlider creation done, hide the original
898 originalSlider.hide();
899 },
900 slide: handleSlideEvent,
901 });
902
903 function handleSlideEvent(event, ui, type) {
904 var handle = $(this).find('.ui-slider-handle');
905 var numVal = parseFloat(Number(ui.value).toFixed(decimals));
906 offVal = parseFloat(Number(offVal).toFixed(decimals));
907 allVal = parseFloat(Number(allVal).toFixed(decimals));
908 console.log(numVal, sliderMin, sliderMax, numVal > sliderMax, numVal < sliderMin);
909 if (numVal > sliderMax) { numVal = sliderMax; }
910 if (numVal < sliderMin) { numVal = sliderMin; }
911 var stepNumber = parseFloat(((ui.value - sliderMin) / stepScale).toFixed(0));
912 var handleText = (ui.value);
913 var leftMargin = (stepNumber / numSteps) * 50 * -1;
914 var perStepPercent = 1 / numSteps; //how far in % each step should be on the slider
915 var leftPos = newSlider.width() * (stepNumber * perStepPercent); //how big of a left margin to give the slider for manual inputs
916 /* console.log(`
917 numVal: ${numVal},
918 sliderMax: ${sliderMax}
919 sliderMin: ${sliderMin}
920 sliderValRange: ${sliderValRange}
921 stepScale: ${stepScale}
922 Step: ${stepNumber} of ${numSteps}
923 offVal: ${offVal}
924 allVal = ${allVal}
925 initial value: ${handleText}
926 left-margin: ${leftMargin}
927 width: ${newSlider.width()}
928 percent of max: ${percentOfMax}
929 left: ${leftPos}`) */
930 if (newSlider.attr('id') == 'amount_gen_zenslider') {
931 //special handling for response length slider, pulls text aliases for step values from an array
932 handleText = steps[stepNumber];
933 handle.text(handleText);
934 newSlider.val(stepNumber);
935 numVal = steps[stepNumber];
936 } else if (newSlider.attr('id') == 'rep_pen_range_textgenerationwebui_zenslider') {
937 //special handling for TextCompletion rep pen range slider, pulls text aliases for step values from an array
938 handleText = steps[stepNumber];
939 handle.text(handleText);
940 newSlider.val(stepNumber);
941 if (numVal === offVal) { handle.text('Off').css('color', 'rgba(128,128,128,0.5)'); } else if (numVal === allVal) { handle.text('All'); } else { handle.css('color', ''); }
942 numVal = steps[stepNumber];
943 } else {
944 //everything else uses the flat slider value
945 //also note: the above sliders are not custom inputtable due to the array aliasing
946 //show 'off' if disabled value is set
947 if (numVal === offVal) { handle.text('Off').css('color', 'rgba(128,128,128,0.5)'); } else { handle.text(ui.value.toFixed(decimals)).css('color', ''); }
948 newSlider.val(handleText);
949 }
950 //for manually typed-in values we must adjust left position because JQUI doesn't do it for us
951 handle.css('left', leftPos);
952 //adjust a negative left margin to avoid overflowing right side of slider body
953 handle.css('margin-left', `${leftMargin}px`);
954 originalSlider.val(numVal);
955 originalSlider.trigger('input');
956 originalSlider.trigger('change');
957 }
958}
959function switchUiMode() {
960 $('body').toggleClass('no-blur', power_user.fast_ui_mode);
961 $('#fast_ui_mode').prop('checked', power_user.fast_ui_mode);
962 if (power_user.fast_ui_mode) {
963 $('#blur-strength-block').css('opacity', '0.2');
964 $('#blur_strength').prop('disabled', true);
965 } else {
966 $('#blur-strength-block').css('opacity', '1');
967 $('#blur_strength').prop('disabled', false);
968 }
969}
970
971function toggleWaifu() {
972 $('#waifuMode').trigger('click');
973 return '';
974}
975
976function switchWaifuMode() {
977 $('body').toggleClass('waifuMode', power_user.waifuMode);
978 $('#waifuMode').prop('checked', power_user.waifuMode);
979 scrollChatToBottom();
980}
981
982function switchSpoilerMode() {
983 if (power_user.spoiler_free_mode) {
984 $('#descriptionWrapper').hide();
985 $('#firstMessageWrapper').hide();
986 $('#spoiler_free_desc').addClass('flex1');
987 $('#creators_note_desc_hidden').show();
988 } else {
989 $('#descriptionWrapper').show();
990 $('#firstMessageWrapper').show();
991 $('#spoiler_free_desc').removeClass('flex1');
992 $('#creators_note_desc_hidden').hide();
993 }
994}
995
996function peekSpoilerMode() {
997 $('#descriptionWrapper').toggle();
998 $('#firstMessageWrapper').toggle();
999 $('#spoiler_free_desc').toggleClass('flex1');
1000 $('#creators_note_desc_hidden').toggle();
1001}
1002
1003function switchMovingUI() {
1004 $('.drawer-content.maximized').each(function () {
1005 $(this).find('.inline-drawer-maximize').trigger('click');
1006 });
1007 $('body').toggleClass('movingUI', power_user.movingUI);
1008 if (power_user.movingUI === true) {
1009 initMovingUI();
1010 if (power_user.movingUIState) {
1011 loadMovingUIState();
1012 }
1013 } else {
1014 if (Object.keys(power_user.movingUIState).length !== 0) {
1015 power_user.movingUIState = {};
1016 resetMovablePanels();
1017 saveSettingsDebounced();
1018 }
1019 }
1020}
1021
1022function applyNoShadows() {
1023 $('body').toggleClass('noShadows', power_user.noShadows);
1024 $('#noShadowsmode').prop('checked', power_user.noShadows);
1025 if (power_user.noShadows) {
1026 $('#shadow-width-block').css('opacity', '0.2');
1027 $('#shadow_width').prop('disabled', true);
1028 } else {
1029 $('#shadow-width-block').css('opacity', '1');
1030 $('#shadow_width').prop('disabled', false);
1031 }
1032 scrollChatToBottom();
1033}
1034
1035function applyAvatarStyle() {
1036 $('body').toggleClass('big-avatars', power_user.avatar_style === avatar_styles.RECTANGULAR);
1037 $('body').toggleClass('square-avatars', power_user.avatar_style === avatar_styles.SQUARE);
1038 $('body').toggleClass('rounded-avatars', power_user.avatar_style === avatar_styles.ROUNDED);
1039 $('#avatar_style').val(power_user.avatar_style).prop('selected', true);
1040}
1041
1042function applyChatDisplay() {
1043 if ([null, undefined].includes(power_user.chat_display)) {
1044 console.debug('applyChatDisplay: saw no chat display type defined');
1045 power_user.chat_display = chat_styles.DEFAULT;
1046 }
1047 console.debug(`poweruser.chat_display ${power_user.chat_display}`);
1048 $('#chat_display').val(power_user.chat_display).prop('selected', true);
1049
1050 switch (power_user.chat_display) {
1051 case 0: {
1052 console.debug('applying default chat');
1053 $('body').removeClass('bubblechat');
1054 $('body').removeClass('documentstyle');
1055 break;
1056 }
1057 case 1: {
1058 console.debug('applying bubblechat');
1059 $('body').addClass('bubblechat');
1060 $('body').removeClass('documentstyle');
1061 break;
1062 }
1063 case 2: {
1064 console.debug('applying document style');
1065 $('body').removeClass('bubblechat');
1066 $('body').addClass('documentstyle');
1067 break;
1068 }
1069 }
1070}
1071
1072function applyToastrPosition() {
1073 if (!toastPositionClasses.includes(power_user.toastr_position)) {
1074 power_user.toastr_position = defaultToastPosition;
1075 console.warn(`applyToastrPosition: invalid toastr position, defaulting to ${defaultToastPosition}`);
1076 }
1077
1078 toastr.options.positionClass = power_user.toastr_position;
1079 fixToastrForDialogs();
1080 $('#toastr_position').val(power_user.toastr_position);
1081 $(`#toastr_position option[value="${power_user.toastr_position}"]`).prop('selected', true);
1082}
1083
1084function applyChatWidth(type) {
1085 if (type === 'forced') {
1086 let r = document.documentElement;
1087 r.style.setProperty('--sheldWidth', `${power_user.chat_width}vw`);
1088 $('#chat_width_slider').val(power_user.chat_width);
1089 //document.documentElement.style.setProperty('--sheldWidth', power_user.chat_width);
1090 } else {
1091 //this is to prevent the slider from updating page in real time
1092 $('#chat_width_slider').off('mouseup touchend').on('mouseup touchend', async () => {
1093 // This is a hack for Firefox to let it render before applying the block width.
1094 // Otherwise it takes the incorrect slider position with the new value AFTER the resizing.
1095 await delay(1);
1096 document.documentElement.style.setProperty('--sheldWidth', `${power_user.chat_width}vw`);
1097 await delay(1);
1098 });
1099 }
1100
1101 $('#chat_width_slider_counter').val(power_user.chat_width);
1102}
1103
1104function applyThemeColor(type) {
1105 if (type === 'main') {
1106 document.documentElement.style.setProperty('--SmartThemeBodyColor', power_user.main_text_color);
1107 const color = power_user.main_text_color.split('(')[1].split(')')[0].split(',');
1108 document.documentElement.style.setProperty('--SmartThemeCheckboxBgColorR', color[0]);
1109 document.documentElement.style.setProperty('--SmartThemeCheckboxBgColorG', color[1]);
1110 document.documentElement.style.setProperty('--SmartThemeCheckboxBgColorB', color[2]);
1111 document.documentElement.style.setProperty('--SmartThemeCheckboxBgColorA', color[3]);
1112 }
1113 if (type === 'italics') {
1114 document.documentElement.style.setProperty('--SmartThemeEmColor', power_user.italics_text_color);
1115 }
1116 if (type === 'underline') {
1117 document.documentElement.style.setProperty('--SmartThemeUnderlineColor', power_user.underline_text_color);
1118 }
1119 if (type === 'quote') {
1120 document.documentElement.style.setProperty('--SmartThemeQuoteColor', power_user.quote_text_color);
1121 }
1122 /* if (type === 'fastUIBG') {
1123 document.documentElement.style.setProperty('--SmartThemeFastUIBGColor', power_user.fastui_bg_color);
1124 } */
1125 if (type === 'blurTint') {
1126 let metaThemeColor = document.querySelector('meta[name=theme-color]');
1127 document.documentElement.style.setProperty('--SmartThemeBlurTintColor', power_user.blur_tint_color);
1128 metaThemeColor.setAttribute('content', power_user.blur_tint_color);
1129 }
1130 if (type === 'chatTint') {
1131 document.documentElement.style.setProperty('--SmartThemeChatTintColor', power_user.chat_tint_color);
1132 }
1133 if (type === 'userMesBlurTint') {
1134 document.documentElement.style.setProperty('--SmartThemeUserMesBlurTintColor', power_user.user_mes_blur_tint_color);
1135 }
1136 if (type === 'botMesBlurTint') {
1137 document.documentElement.style.setProperty('--SmartThemeBotMesBlurTintColor', power_user.bot_mes_blur_tint_color);
1138 }
1139 if (type === 'shadow') {
1140 document.documentElement.style.setProperty('--SmartThemeShadowColor', power_user.shadow_color);
1141 }
1142 if (type === 'border') {
1143 document.documentElement.style.setProperty('--SmartThemeBorderColor', power_user.border_color);
1144 }
1145}
1146
1147function applyCustomCSS() {
1148 $('#customCSS').val(power_user.custom_css);
1149 var styleId = 'custom-style';
1150 var style = document.getElementById(styleId);
1151 if (!style) {
1152 style = document.createElement('style');
1153 style.setAttribute('type', 'text/css');
1154 style.setAttribute('id', styleId);
1155 document.head.appendChild(style);
1156 }
1157 style.innerHTML = power_user.custom_css;
1158}
1159
1160function applyBlurStrength() {
1161 document.documentElement.style.setProperty('--blurStrength', String(power_user.blur_strength));
1162 $('#blur_strength_counter').val(power_user.blur_strength);
1163 $('#blur_strength').val(power_user.blur_strength);
1164}
1165
1166function applyShadowWidth() {
1167 document.documentElement.style.setProperty('--shadowWidth', String(power_user.shadow_width));
1168 $('#shadow_width_counter').val(power_user.shadow_width);
1169 $('#shadow_width').val(power_user.shadow_width);
1170}
1171
1172function applyFontScale(type) {
1173 //this is to allow forced setting on page load, theme swap, etc
1174 if (type === 'forced') {
1175 document.documentElement.style.setProperty('--fontScale', String(power_user.font_scale));
1176 } else {
1177 //this is to prevent the slider from updating page in real time
1178 $('#font_scale').off('mouseup touchend').on('mouseup touchend', () => {
1179 document.documentElement.style.setProperty('--fontScale', String(power_user.font_scale));
1180 });
1181 }
1182
1183 $('#font_scale_counter').val(power_user.font_scale);
1184 $('#font_scale').val(power_user.font_scale);
1185}
1186
1187/**
1188 * Checks if the chat needs to be reloaded to apply media display settings.
1189 * @returns {boolean} True if the chat needs reload to apply media display settings
1190 */
1191function isMediaDisplayReloadNeeded() {
1192 // A user is not currently in a chat.
1193 const chatId = getCurrentChatId();
1194 if (!chatId) {
1195 return false;
1196 }
1197
1198 const firstDisplayedIndex = getFirstDisplayedMessageId();
1199 const hasUnprocessedMediaMessages = chat.some((message, index) => {
1200 // Skip messages that are not currently displayed
1201 if (index < firstDisplayedIndex) {
1202 return false;
1203 }
1204 const hasMediaAttachments = Array.isArray(message?.extra?.media) && message.extra.media.length > 0;
1205 const lacksMediaDisplay = !message?.extra?.media_display;
1206 return hasMediaAttachments && lacksMediaDisplay;
1207 });
1208
1209 return hasUnprocessedMediaMessages;
1210}
1211
1212/**
1213 * Shows a toast notification prompting the user to reload the chat if media display settings have changed
1214 * and there are messages with media attachments that haven't been processed with the new display format.
1215 */
1216function showMediaDisplayReloadPrompt() {
1217 if (!isMediaDisplayReloadNeeded()) {
1218 return;
1219 }
1220 toastr.info(
1221 t`Reload the chat to apply the changes. Click here to reload.`,
1222 t`Media Style changed`,
1223 { onclick: () => void reloadCurrentChat() },
1224 );
1225}
1226
1227function applyTheme(name) {
1228 const theme = themes.find(x => x.name == name);
1229
1230 if (!theme) {
1231 return;
1232 }
1233
1234 const themeProperties = [
1235 { key: 'main_text_color', selector: '#main-text-color-picker', type: 'main' },
1236 { key: 'italics_text_color', selector: '#italics-color-picker', type: 'italics' },
1237 { key: 'underline_text_color', selector: '#underline-color-picker', type: 'underline' },
1238 { key: 'quote_text_color', selector: '#quote-color-picker', type: 'quote' },
1239 { key: 'blur_tint_color', selector: '#blur-tint-color-picker', type: 'blurTint' },
1240 { key: 'chat_tint_color', selector: '#chat-tint-color-picker', type: 'chatTint' },
1241 { key: 'user_mes_blur_tint_color', selector: '#user-mes-blur-tint-color-picker', type: 'userMesBlurTint' },
1242 { key: 'bot_mes_blur_tint_color', selector: '#bot-mes-blur-tint-color-picker', type: 'botMesBlurTint' },
1243 { key: 'shadow_color', selector: '#shadow-color-picker', type: 'shadow' },
1244 { key: 'border_color', selector: '#border-color-picker', type: 'border' },
1245 {
1246 key: 'blur_strength',
1247 action: () => {
1248 applyBlurStrength();
1249 },
1250 },
1251 {
1252 key: 'custom_css',
1253 action: () => {
1254 applyCustomCSS();
1255 },
1256 },
1257 {
1258 key: 'shadow_width',
1259 action: () => {
1260 applyShadowWidth();
1261 },
1262 },
1263 {
1264 key: 'font_scale',
1265 action: () => {
1266 applyFontScale('forced');
1267 },
1268 },
1269 {
1270 key: 'fast_ui_mode',
1271 action: () => {
1272 switchUiMode();
1273 },
1274 },
1275 {
1276 key: 'waifuMode',
1277 action: () => {
1278 switchWaifuMode();
1279 },
1280 },
1281 {
1282 key: 'chat_display',
1283 action: () => {
1284 applyChatDisplay();
1285 },
1286 },
1287 {
1288 key: 'toastr_position',
1289 action: () => {
1290 applyToastrPosition();
1291 },
1292 },
1293 {
1294 key: 'avatar_style',
1295 action: () => {
1296 applyAvatarStyle();
1297 },
1298 },
1299 {
1300 key: 'noShadows',
1301 action: () => {
1302 applyNoShadows();
1303 },
1304 },
1305 {
1306 key: 'chat_width',
1307 action: () => {
1308 // If chat width is not set, set it to 50
1309 if (!power_user.chat_width) {
1310 power_user.chat_width = 50;
1311 }
1312 applyChatWidth('forced');
1313 },
1314 },
1315 {
1316 key: 'timer_enabled',
1317 action: () => {
1318 switchTimer();
1319 },
1320 },
1321 {
1322 key: 'timestamps_enabled',
1323 action: () => {
1324 switchTimestamps();
1325 },
1326 },
1327 {
1328 key: 'timestamp_model_icon',
1329 action: () => {
1330 switchIcons();
1331 },
1332 },
1333 {
1334 key: 'message_token_count_enabled',
1335 action: () => {
1336 switchTokenCount();
1337 },
1338 },
1339 {
1340 key: 'mesIDDisplay_enabled',
1341 action: () => {
1342 switchMesIDDisplay();
1343 },
1344 },
1345 {
1346 key: 'hideChatAvatars_enabled',
1347 action: () => {
1348 switchHideChatAvatars();
1349 },
1350 },
1351 {
1352 key: 'expand_message_actions',
1353 action: () => {
1354 switchMessageActions();
1355 },
1356 },
1357 {
1358 key: 'enableZenSliders',
1359 action: () => {
1360 switchMessageActions();
1361 },
1362 },
1363 {
1364 key: 'enableLabMode',
1365 action: () => {
1366 switchMessageActions();
1367 },
1368 },
1369 {
1370 key: 'hotswap_enabled',
1371 action: () => {
1372 switchHotswap();
1373 },
1374 },
1375 {
1376 key: 'bogus_folders',
1377 action: () => {
1378 $('#bogus_folders').prop('checked', power_user.bogus_folders);
1379 printCharactersDebounced();
1380 },
1381 },
1382 {
1383 key: 'zoomed_avatar_magnification',
1384 action: () => {
1385 $('#zoomed_avatar_magnification').prop('checked', power_user.zoomed_avatar_magnification);
1386 printCharactersDebounced();
1387 },
1388 },
1389 {
1390 key: 'reduced_motion',
1391 action: () => {
1392 $('#reduced_motion').prop('checked', power_user.reduced_motion);
1393 switchReducedMotion();
1394 },
1395 },
1396 {
1397 key: 'compact_input_area',
1398 action: () => {
1399 $('#compact_input_area').prop('checked', power_user.compact_input_area);
1400 switchCompactInputArea();
1401 },
1402 },
1403 {
1404 key: 'show_swipe_num_all_messages',
1405 action: () => {
1406 $('#show_swipe_num_all_messages').prop('checked', power_user.show_swipe_num_all_messages);
1407 switchSwipeNumAllMessages();
1408 },
1409 },
1410 {
1411 key: 'click_to_edit',
1412 action: () => {
1413 $('#click_to_edit').prop('checked', power_user.click_to_edit);
1414 },
1415 },
1416 {
1417 key: 'media_display',
1418 action: (oldValue, newValue) => {
1419 $('#media_display').val(power_user.media_display);
1420 if (oldValue !== newValue) {
1421 showMediaDisplayReloadPrompt();
1422 }
1423 },
1424 },
1425 ];
1426
1427 for (const { key, selector, type, action } of themeProperties) {
1428 if (theme[key] !== undefined) {
1429 const oldValue = power_user[key];
1430 const newValue = theme[key];
1431 power_user[key] = newValue;
1432 if (selector) $(selector).attr('color', newValue);
1433 if (type) applyThemeColor(type);
1434 if (action) action(oldValue, newValue);
1435 } else {
1436 console.debug(`Empty theme key: ${key}`);
1437 }
1438 }
1439
1440 console.log('theme applied: ' + name);
1441}
1442
1443async function applyMovingUIPreset(name) {
1444 await resetMovablePanels('quiet');
1445 const movingUIPreset = movingUIPresets.find(x => x.name == name);
1446
1447 if (!movingUIPreset) {
1448 return;
1449 }
1450
1451 power_user.movingUIState = movingUIPreset.movingUIState;
1452
1453
1454 console.log('MovingUI Preset applied: ' + name);
1455 loadMovingUIState();
1456 saveSettingsDebounced();
1457}
1458
1459/**
1460 * Register a function to be executed when the debug menu is opened.
1461 * @param {string} functionId Unique ID for the function.
1462 * @param {string} name Name of the function.
1463 * @param {string} description Description of the function.
1464 * @param {function} func Function to be executed.
1465 */
1466export function registerDebugFunction(functionId, name, description, func) {
1467 debug_functions.push({ functionId, name, description, func });
1468}
1469
1470async function showDebugMenu() {
1471 const template = await renderTemplateAsync('debug', { functions: debug_functions });
1472 callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: true, large: true, allowVerticalScrolling: true });
1473}
1474
1475export function applyPowerUserSettings() {
1476 switchUiMode();
1477 applyFontScale('forced');
1478 applyThemeColor();
1479 applyChatWidth('forced');
1480 applyAvatarStyle();
1481 applyBlurStrength();
1482 applyShadowWidth();
1483 applyCustomCSS();
1484 switchMovingUI();
1485 applyNoShadows();
1486 switchHotswap();
1487 switchTimer();
1488 switchTimestamps();
1489 switchIcons();
1490 switchMesIDDisplay();
1491 switchHideChatAvatars();
1492 switchTokenCount();
1493 switchMessageActions();
1494 switchSwipeNumAllMessages();
1495}
1496
1497export function applyStylePins() {
1498 try {
1499 const existingPins = document.querySelector('#chat > .style-pins');
1500 if (existingPins) {
1501 existingPins.remove();
1502 }
1503
1504 if (!power_user.pin_styles) {
1505 return;
1506 }
1507
1508 const firstDisplayed = getFirstDisplayedMessageId();
1509 if (firstDisplayed === 0 || !isFinite(firstDisplayed)) {
1510 return;
1511 }
1512
1513 const chatElement = document.getElementById('chat');
1514 if (!chatElement) {
1515 return;
1516 }
1517
1518 const firstMessage = chat[0];
1519 if (!firstMessage) {
1520 return;
1521 }
1522
1523 const formattedMessage = messageFormatting(firstMessage.mes, firstMessage.name, firstMessage.is_system, firstMessage.is_user, 0, {}, false);
1524 const htmlElement = document.createElement('div');
1525 htmlElement.innerHTML = formattedMessage;
1526
1527 const styleTags = htmlElement.querySelectorAll('style');
1528 if (styleTags.length === 0) {
1529 return;
1530 }
1531
1532 const pinsElement = document.createElement('div');
1533 pinsElement.classList.add('style-pins');
1534 pinsElement.append(...Array.from(styleTags));
1535 chatElement.prepend(pinsElement);
1536 } catch (error) {
1537 console.error('Error applying style pins:', error);
1538 }
1539}
1540
1541function getExampleMessagesBehavior() {
1542 if (power_user.strip_examples) {
1543 return 'strip';
1544 }
1545
1546 if (power_user.pin_examples) {
1547 return 'keep';
1548 }
1549
1550 return 'normal';
1551}
1552
1553//MARK: loadPowerUser
1554export async function loadPowerUserSettings(settings, data) {
1555 const defaultStscript = JSON.parse(JSON.stringify(power_user.stscript));
1556 // Load from settings.json
1557 if (settings.power_user !== undefined) {
1558 // Migrate old preference to a new setting
1559 if (settings.power_user.click_to_edit === undefined && settings.power_user.chat_display === chat_styles.DOCUMENT) {
1560 settings.power_user.click_to_edit = true;
1561 }
1562 if (Object.hasOwn(settings.power_user, 'auto_sort_tags') && !Object.hasOwn(settings.power_user, 'tag_sort_mode')) {
1563 settings.power_user.tag_sort_mode = settings.power_user.auto_sort_tags ? tag_sort_mode.ALPHABETICAL : tag_sort_mode.MANUAL;
1564 delete settings.power_user.auto_sort_tags;
1565 }
1566 Object.assign(power_user, settings.power_user);
1567 }
1568
1569 if (power_user.stscript === undefined) {
1570 power_user.stscript = defaultStscript;
1571 } else {
1572 if (power_user.stscript.autocomplete === undefined) {
1573 power_user.stscript.autocomplete = defaultStscript.autocomplete;
1574 } else {
1575 if (power_user.stscript.autocomplete.state === undefined) {
1576 power_user.stscript.autocomplete.state = defaultStscript.autocomplete.state;
1577 }
1578 if (power_user.stscript.autocomplete.width === undefined) {
1579 power_user.stscript.autocomplete.width = defaultStscript.autocomplete.width;
1580 }
1581 if (power_user.stscript.autocomplete.font === undefined) {
1582 power_user.stscript.autocomplete.font = defaultStscript.autocomplete.font;
1583 }
1584 if (power_user.stscript.autocomplete.style === undefined) {
1585 power_user.stscript.autocomplete.style = power_user.stscript.autocomplete_style || defaultStscript.autocomplete.style;
1586 }
1587 if (power_user.stscript.autocomplete.select === undefined) {
1588 power_user.stscript.autocomplete.select = defaultStscript.autocomplete.select;
1589 }
1590 if (power_user.stscript.autocomplete.showInAllMacroFields === undefined) {
1591 power_user.stscript.autocomplete.showInAllMacroFields = defaultStscript.autocomplete.showInAllMacroFields;
1592 }
1593 }
1594 if (power_user.stscript.parser === undefined) {
1595 power_user.stscript.parser = defaultStscript.parser;
1596 } else if (power_user.stscript.parser.flags === undefined) {
1597 power_user.stscript.parser.flags = defaultStscript.parser.flags;
1598 }
1599
1600 // Cleanup old flags
1601 delete power_user.stscript.autocomplete_style;
1602 }
1603
1604 if (data.themes !== undefined) {
1605 themes = data.themes;
1606 }
1607
1608 if (data.movingUIPresets !== undefined) {
1609 movingUIPresets = data.movingUIPresets;
1610 }
1611
1612
1613 if (data.context !== undefined) {
1614 context_presets = data.context;
1615 }
1616
1617 if (typeof power_user.chat_display !== 'number') {
1618 power_user.chat_display = chat_styles.DEFAULT;
1619 }
1620
1621 if (typeof power_user.waifuMode !== 'boolean') {
1622 power_user.waifuMode = false;
1623 }
1624
1625 if (typeof power_user.chat_width !== 'number') {
1626 power_user.chat_width = 50;
1627 }
1628
1629 if (power_user.tokenizer === tokenizers.LEGACY) {
1630 power_user.tokenizer = tokenizers.GPT2;
1631 }
1632
1633 // Clean up old/legacy settings
1634 if (power_user.import_card_tags !== undefined) {
1635 power_user.tag_import_setting = power_user.import_card_tags ? tag_import_setting.ASK : tag_import_setting.NONE;
1636 delete power_user.import_card_tags;
1637 }
1638
1639 if (power_user?.instruct?.derived === true) {
1640 power_user.instruct_derived = true;
1641 delete power_user.instruct.derived;
1642 }
1643
1644 // Reset the saved chat template hash
1645 power_user.chat_template_hash = '';
1646
1647 $('#single_line').prop('checked', power_user.single_line);
1648 $('#relaxed_api_urls').prop('checked', power_user.relaxed_api_urls);
1649 $('#world_import_dialog').prop('checked', power_user.world_import_dialog);
1650 $('#enable_auto_select_input').prop('checked', power_user.enable_auto_select_input);
1651 $('#enable_md_hotkeys').prop('checked', power_user.enable_md_hotkeys);
1652 $('#trim_spaces').prop('checked', power_user.trim_spaces);
1653 $('#continue_on_send').prop('checked', power_user.continue_on_send);
1654 $('#quick_continue').prop('checked', power_user.quick_continue);
1655 $('#quick_impersonate').prop('checked', power_user.quick_continue);
1656 $('#mes_continue').css('display', power_user.quick_continue ? '' : 'none');
1657 $('#mes_impersonate').css('display', power_user.quick_impersonate ? '' : 'none');
1658 $('#gestures-checkbox').prop('checked', power_user.gestures);
1659 $('#auto_swipe').prop('checked', power_user.auto_swipe);
1660 $('#auto_swipe_minimum_length').val(power_user.auto_swipe_minimum_length);
1661 $('#auto_swipe_blacklist').val(power_user.auto_swipe_blacklist.join(', '));
1662 $('#auto_swipe_blacklist_threshold').val(power_user.auto_swipe_blacklist_threshold);
1663 $('#custom_stopping_strings').text(power_user.custom_stopping_strings);
1664 $('#custom_stopping_strings_macro').prop('checked', power_user.custom_stopping_strings_macro);
1665 $('#fuzzy_search_checkbox').prop('checked', power_user.fuzzy_search);
1666 $('#persona_show_notifications').prop('checked', power_user.persona_show_notifications);
1667 $('#persona_allow_multi_connections').prop('checked', power_user.persona_allow_multi_connections);
1668 $('#persona_auto_lock').prop('checked', power_user.persona_auto_lock);
1669 $('#encode_tags').prop('checked', power_user.encode_tags);
1670 $('#experimental_macro_engine').prop('checked', power_user.experimental_macro_engine);
1671 $('#example_messages_behavior').val(getExampleMessagesBehavior());
1672 $(`#example_messages_behavior option[value="${getExampleMessagesBehavior()}"]`).prop('selected', true);
1673 $('#instruct_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct_derived);
1674 $('#context_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.context_derived);
1675 $('#context_size_derived').prop('checked', !!power_user.context_size_derived);
1676
1677 $('#console_log_prompts').prop('checked', power_user.console_log_prompts);
1678 $('#request_token_probabilities').prop('checked', power_user.request_token_probabilities);
1679 $('#show_group_chat_queue').prop('checked', power_user.show_group_chat_queue);
1680 $('#auto_fix_generated_markdown').prop('checked', power_user.auto_fix_generated_markdown);
1681 $('#auto_scroll_chat_to_bottom').prop('checked', power_user.auto_scroll_chat_to_bottom);
1682 $('#bogus_folders').prop('checked', power_user.bogus_folders);
1683 $('#zoomed_avatar_magnification').prop('checked', power_user.zoomed_avatar_magnification);
1684 $(`#tokenizer option[value="${power_user.tokenizer}"]`).prop('selected', true);
1685 $(`#send_on_enter option[value=${power_user.send_on_enter}]`).prop('selected', true);
1686 $('#confirm_message_delete').prop('checked', power_user.confirm_message_delete !== undefined ? !!power_user.confirm_message_delete : true);
1687 $('#spoiler_free_mode').prop('checked', power_user.spoiler_free_mode);
1688 $('#collapse-newlines-checkbox').prop('checked', power_user.collapse_newlines);
1689 $('#always-force-name2-checkbox').prop('checked', power_user.always_force_name2);
1690 $('#trim_sentences_checkbox').prop('checked', power_user.trim_sentences);
1691 $('#disable_group_trimming').prop('checked', power_user.disable_group_trimming);
1692 $('#markdown_escape_strings').val(power_user.markdown_escape_strings);
1693 $('#fast_ui_mode').prop('checked', power_user.fast_ui_mode);
1694 $('#waifuMode').prop('checked', power_user.waifuMode);
1695 $('#movingUImode').prop('checked', power_user.movingUI);
1696 $('#noShadowsmode').prop('checked', power_user.noShadows);
1697 $('#start_reply_with').text(power_user.user_prompt_bias);
1698 $('#chat-show-reply-prefix-checkbox').prop('checked', power_user.show_user_prompt_bias);
1699 $('#auto_continue_enabled').prop('checked', power_user.auto_continue.enabled);
1700 $('#auto_continue_allow_chat_completions').prop('checked', power_user.auto_continue.allow_chat_completions);
1701 $('#auto_continue_target_length').val(power_user.auto_continue.target_length);
1702 $('#play_message_sound').prop('checked', power_user.play_message_sound);
1703 $('#play_sound_unfocused').prop('checked', power_user.play_sound_unfocused);
1704 $('#never_resize_avatars').prop('checked', power_user.never_resize_avatars);
1705 $('#show_card_avatar_urls').prop('checked', power_user.show_card_avatar_urls);
1706 $('#auto_save_msg_edits').prop('checked', power_user.auto_save_msg_edits);
1707 $('#allow_name1_display').prop('checked', power_user.allow_name1_display);
1708 $('#allow_name2_display').prop('checked', power_user.allow_name2_display);
1709 //$("#removeXML").prop("checked", power_user.removeXML);
1710 $('#hotswapEnabled').prop('checked', power_user.hotswap_enabled);
1711 $('#messageTimerEnabled').prop('checked', power_user.timer_enabled);
1712 $('#messageTimestampsEnabled').prop('checked', power_user.timestamps_enabled);
1713 $('#messageModelIconEnabled').prop('checked', power_user.timestamp_model_icon);
1714 $('#mesIDDisplayEnabled').prop('checked', power_user.mesIDDisplay_enabled);
1715 $('#hideChatAvatarsEnabled').prop('checked', power_user.hideChatAvatars_enabled);
1716 $('#prefer_character_prompt').prop('checked', power_user.prefer_character_prompt);
1717 $('#prefer_character_jailbreak').prop('checked', power_user.prefer_character_jailbreak);
1718 $('#enableZenSliders').prop('checked', power_user.enableZenSliders).trigger('input');
1719 $('#enableLabMode').prop('checked', power_user.enableLabMode).trigger('input', { fromInit: true });
1720 $(`input[name="avatar_style"][value="${power_user.avatar_style}"]`).prop('checked', true);
1721 $(`#chat_display option[value=${power_user.chat_display}]`).prop('selected', true).trigger('change');
1722 $(`#toastr_position option[value=${power_user.toastr_position}]`).prop('selected', true).trigger('change');
1723 $('#chat_width_slider').val(power_user.chat_width);
1724 $('#token_padding').val(power_user.token_padding);
1725 $('#aux_field').val(power_user.aux_field);
1726 $('#tag_import_setting').val(power_user.tag_import_setting);
1727
1728 $('#stscript_autocomplete_state').val(power_user.stscript.autocomplete.state).trigger('input');
1729 $('#stscript_autocomplete_autoHide').prop('checked', power_user.stscript.autocomplete.autoHide ?? false).trigger('input');
1730 $('#stscript_autocomplete_showInAllMacroFields').prop('checked', power_user.stscript.autocomplete.showInAllMacroFields ?? false).trigger('input');
1731 $('#stscript_matching').val(power_user.stscript.matching ?? 'fuzzy');
1732 $('#stscript_autocomplete_style').val(power_user.stscript.autocomplete.style ?? 'theme');
1733 document.body.setAttribute('data-stscript-style', power_user.stscript.autocomplete.style);
1734 $('#stscript_autocomplete_select').val(power_user.stscript.autocomplete.select ?? (AUTOCOMPLETE_SELECT_KEY.TAB + AUTOCOMPLETE_SELECT_KEY.ENTER));
1735 $('#stscript_parser_flag_strict_escaping').prop('checked', power_user.stscript.parser.flags[PARSER_FLAG.STRICT_ESCAPING] ?? false);
1736 $('#stscript_parser_flag_replace_getvar').prop('checked', power_user.stscript.parser.flags[PARSER_FLAG.REPLACE_GETVAR] ?? false);
1737 $('#stscript_autocomplete_font_scale').val(power_user.stscript.autocomplete.font.scale ?? defaultStscript.autocomplete.font.scale);
1738 $('#stscript_autocomplete_font_scale_counter').val(power_user.stscript.autocomplete.font.scale ?? defaultStscript.autocomplete.font.scale);
1739 document.body.style.setProperty('--ac-font-scale', power_user.stscript.autocomplete.font.scale ?? defaultStscript.autocomplete.font.scale.toString());
1740 $('#stscript_autocomplete_width_left').val(power_user.stscript.autocomplete.width.left ?? AUTOCOMPLETE_WIDTH.CHAT);
1741 document.querySelector('#stscript_autocomplete_width_left')?.dispatchEvent(new Event('input', { bubbles: true }));
1742 $('#stscript_autocomplete_width_right').val(power_user.stscript.autocomplete.width.right ?? AUTOCOMPLETE_WIDTH.CHAT);
1743 document.querySelector('#stscript_autocomplete_width_right')?.dispatchEvent(new Event('input', { bubbles: true }));
1744
1745 $('#restore_user_input').prop('checked', power_user.restore_user_input);
1746
1747 $('#chat_truncation').val(power_user.chat_truncation);
1748 $('#chat_truncation_counter').val(power_user.chat_truncation);
1749
1750 $('#streaming_fps').val(power_user.streaming_fps);
1751 $('#streaming_fps_counter').val(power_user.streaming_fps);
1752
1753 $('#smooth_streaming').prop('checked', power_user.smooth_streaming);
1754 $('#smooth_streaming_no_think').prop('checked', power_user.smooth_streaming_no_think);
1755 $('#smooth_streaming_speed').val(power_user.smooth_streaming_speed);
1756
1757 $('#stream_fade_in').prop('checked', power_user.stream_fade_in);
1758
1759 $('#font_scale').val(power_user.font_scale);
1760 $('#font_scale_counter').val(power_user.font_scale);
1761
1762 $('#blur_strength').val(power_user.blur_strength);
1763 $('#blur_strength_counter').val(power_user.blur_strength);
1764
1765 $('#shadow_width').val(power_user.shadow_width);
1766 $('#shadow_width_counter').val(power_user.shadow_width);
1767
1768 $('#main-text-color-picker').attr('color', power_user.main_text_color);
1769 $('#italics-color-picker').attr('color', power_user.italics_text_color);
1770 $('#underline-color-picker').attr('color', power_user.underline_text_color);
1771 $('#quote-color-picker').attr('color', power_user.quote_text_color);
1772 $('#blur-tint-color-picker').attr('color', power_user.blur_tint_color);
1773 $('#chat-tint-color-picker').attr('color', power_user.chat_tint_color);
1774 $('#user-mes-blur-tint-color-picker').attr('color', power_user.user_mes_blur_tint_color);
1775 $('#bot-mes-blur-tint-color-picker').attr('color', power_user.bot_mes_blur_tint_color);
1776 $('#shadow-color-picker').attr('color', power_user.shadow_color);
1777 $('#border-color-picker').attr('color', power_user.border_color);
1778 $('#reduced_motion').prop('checked', power_user.reduced_motion);
1779 $('#auto-connect-checkbox').prop('checked', power_user.auto_connect);
1780 $('#auto-load-chat-checkbox').prop('checked', power_user.auto_load_chat);
1781 $('#forbid_external_media').prop('checked', power_user.forbid_external_media);
1782 $('#pin_styles').prop('checked', power_user.pin_styles);
1783 $('#click_to_edit').prop('checked', power_user.click_to_edit);
1784 $('#media_display').val(power_user.media_display);
1785 $('#image_overswipe').val(power_user.image_overswipe);
1786
1787 for (const theme of themes) {
1788 const option = document.createElement('option');
1789 option.value = theme.name;
1790 option.innerText = theme.name;
1791 option.selected = theme.name == power_user.theme;
1792 $('#themes').append(option);
1793 }
1794
1795 for (const movingUIPreset of movingUIPresets) {
1796 const option = document.createElement('option');
1797 option.value = movingUIPreset.name;
1798 option.innerText = movingUIPreset.name;
1799 option.selected = movingUIPreset.name == power_user.movingUIPreset;
1800 $('#movingUIPresets').append(option);
1801 }
1802
1803
1804 $(`#character_sort_order option[data-order="${power_user.sort_order}"][data-field="${power_user.sort_field}"]`).prop('selected', true);
1805 switchReducedMotion();
1806 switchCompactInputArea();
1807 reloadMarkdownProcessor();
1808 await loadInstructMode(data);
1809 await loadContextSettings();
1810 await loadSystemPrompts(data);
1811 await loadReasoningTemplates(data);
1812 loadMaxContextUnlocked();
1813 switchWaifuMode();
1814 switchSpoilerMode();
1815 loadMovingUIState();
1816 loadCharListState();
1817 toggleMDHotkeyIconDisplay();
1818 applyToastrPosition();
1819}
1820
1821function toggleMDHotkeyIconDisplay() {
1822 if (power_user.enable_md_hotkeys) {
1823 $('.mdhotkey_location').each(function () {
1824 $(this).parent().append('<i class="fa-brands fa-markdown mdhotkey_icon"></i>');
1825 });
1826 } else {
1827 $('.mdhotkey_icon').remove();
1828 }
1829}
1830
1831function loadCharListState() {
1832 document.body.classList.toggle('charListGrid', power_user.charListGrid);
1833}
1834
1835export function loadMovingUIState() {
1836 if (!isMobile()
1837 && power_user.movingUIState
1838 && power_user.movingUI === true) {
1839 console.debug('loading movingUI state');
1840 for (var elmntName of Object.keys(power_user.movingUIState)) {
1841 var elmntState = power_user.movingUIState[elmntName];
1842 try {
1843 var elmnt = $('#' + $.escapeSelector(elmntName));
1844 if (elmnt.length) {
1845 console.debug(`loading state for ${elmntName}`);
1846 elmnt.css(elmntState);
1847 } else {
1848 console.debug(`skipping ${elmntName} because it doesn't exist in the DOM`);
1849 }
1850 } catch (err) {
1851 console.debug(`error occurred while processing ${elmntName}: ${err}`);
1852 }
1853 }
1854 } else {
1855 console.debug('skipping movingUI state load');
1856 return;
1857 }
1858}
1859
1860function loadMaxContextUnlocked() {
1861 $('#max_context_unlocked').prop('checked', power_user.max_context_unlocked);
1862 $('#max_context_unlocked').on('change', function () {
1863 power_user.max_context_unlocked = !!$(this).prop('checked');
1864 switchMaxContextSize();
1865 saveSettingsDebounced();
1866 });
1867 switchMaxContextSize();
1868}
1869
1870function switchMaxContextSize() {
1871 const elements = [
1872 $('#max_context'),
1873 $('#max_context_counter'),
1874 $('#rep_pen_range'),
1875 $('#rep_pen_range_counter'),
1876 $('#rep_pen_range_textgenerationwebui'),
1877 $('#rep_pen_range_counter_textgenerationwebui'),
1878 $('#dry_penalty_last_n_textgenerationwebui'),
1879 $('#dry_penalty_last_n_counter_textgenerationwebui'),
1880 $('#rep_pen_decay_textgenerationwebui'),
1881 $('#rep_pen_decay_counter_textgenerationwebui'),
1882 ];
1883 const maxValue = power_user.max_context_unlocked ? MAX_CONTEXT_UNLOCKED : MAX_CONTEXT_DEFAULT;
1884 const minValue = power_user.max_context_unlocked ? maxContextMin : maxContextMin;
1885 const steps = power_user.max_context_unlocked ? unlockedMaxContextStep : maxContextStep;
1886 $('#rep_pen_range_textgenerationwebui_zenslider').remove(); //unsure why, but this is necessary.
1887 $('#dry_penalty_last_n_textgenerationwebui_zenslider').remove();
1888 $('#rep_pen_decay_textgenerationwebui_zenslider').remove();
1889 for (const element of elements) {
1890 const id = element.attr('id');
1891 element.attr('max', maxValue);
1892
1893 if (typeof id === 'string' && id?.indexOf('max_context') !== -1) {
1894 element.attr('min', minValue);
1895 element.attr('step', steps); //only change setps for max context, because rep pen range needs step of 1 due to important values of -1 and 0
1896 }
1897 const value = Number(element.val());
1898
1899 if (value >= maxValue) {
1900 element.val(maxValue).trigger('input');
1901 }
1902 }
1903
1904 const maxAmountGen = power_user.max_context_unlocked ? MAX_RESPONSE_UNLOCKED : MAX_RESPONSE_DEFAULT;
1905 $('#amount_gen').attr('max', maxAmountGen);
1906 $('#amount_gen_counter').attr('max', maxAmountGen);
1907
1908 if (Number($('#amount_gen').val()) >= maxAmountGen) {
1909 $('#amount_gen').val(maxAmountGen).trigger('input');
1910 }
1911
1912 if (power_user.enableZenSliders) {
1913 $('#max_context_zenslider').remove();
1914 CreateZenSliders($('#max_context'));
1915 $('#rep_pen_range_textgenerationwebui_zenslider').remove();
1916 CreateZenSliders($('#rep_pen_range_textgenerationwebui'));
1917 $('#dry_penalty_last_n_textgenerationwebui_zenslider').remove();
1918 CreateZenSliders($('#dry_penalty_last_n_textgenerationwebui'));
1919 $('#rep_pen_decay_textgenerationwebui_zenslider').remove();
1920 CreateZenSliders($('#rep_pen_decay_textgenerationwebui'));
1921 }
1922}
1923
1924// Fetch a compiled object of all preset settings
1925export function getContextSettings() {
1926 let compiledSettings = {};
1927
1928 contextControls.forEach((control) => {
1929 let value = control.isGlobalSetting ? power_user[control.property] : power_user.context[control.property];
1930
1931 // Force to a boolean if the setting is a checkbox
1932 if (control.isCheckbox) {
1933 value = !!value;
1934 }
1935
1936 compiledSettings[control.property] = value;
1937 });
1938
1939 return compiledSettings;
1940}
1941
1942// TODO: Maybe add a refresh button to reset settings to preset
1943// TODO: Add "global state" if a preset doesn't set the power_user checkboxes
1944async function loadContextSettings() {
1945 /**
1946 * Auto-fix missing fields in the story string
1947 * @param {ContextSettings} contextSettings Context settings instance
1948 */
1949 function autoFixStoryString(contextSettings) {
1950 // Already migrated, no need to fix
1951 if (!contextSettings || Object.hasOwn(contextSettings, 'story_string_position')) {
1952 return;
1953 }
1954
1955 let storyString = contextSettings.story_string || '';
1956
1957 /**
1958 * @param {string} field Missing field name
1959 * @param {'start'|'end'} position Position of auto-fix
1960 */
1961 function autoFixMissingField(field, position) {
1962 if (storyString.includes(`{{${field}}}`)) {
1963 return;
1964 }
1965
1966 console.warn(`[Story String Validation] Story String is missing a field: ${field}. Adding it at the ${position}.`);
1967 const fieldTemplate = `{{#if ${field}}}{{${field}}}\n{{/if}}`;
1968 const firstCurlyPosition = storyString.includes('{{') ? storyString.indexOf('{{') : 0;
1969 const lastCurlyPosition = storyString.includes('}}') ? storyString.lastIndexOf('}}') + '}}'.length : storyString.length;
1970 const lastTrimPosition = storyString.includes('{{trim}}') ? storyString.lastIndexOf('{{trim}}') : storyString.length;
1971 const endPosition = Math.min(lastTrimPosition, lastCurlyPosition);
1972 storyString = position === 'start'
1973 ? storyString.substring(0, firstCurlyPosition) + fieldTemplate + storyString.substring(firstCurlyPosition)
1974 : storyString.substring(0, endPosition) + fieldTemplate + storyString.substring(endPosition);
1975 }
1976
1977 autoFixMissingField('anchorBefore', 'start');
1978 autoFixMissingField('anchorAfter', 'end');
1979
1980 contextSettings.story_string = storyString;
1981 }
1982
1983 // Migrate story string to add missing fields
1984 autoFixStoryString(power_user.context);
1985
1986 contextControls.forEach(control => {
1987 const $element = $(`#${control.id}`);
1988
1989 if (control.isGlobalSetting) {
1990 return;
1991 }
1992
1993 if (control.defaultValue !== undefined && power_user.context[control.property] === undefined) {
1994 power_user.context[control.property] = control.defaultValue;
1995 }
1996
1997 if (control.isCheckbox) {
1998 $element.prop('checked', power_user.context[control.property]);
1999 } else {
2000 $element.val(power_user.context[control.property]);
2001 }
2002 console.debug(`Setting ${$element.prop('id')} to ${power_user.context[control.property]}`);
2003
2004 // If the setting already exists, no need to duplicate it
2005 // TODO: Maybe check the power_user object for the setting instead of a flag?
2006 $element.on('input', async function () {
2007 let value = control.isCheckbox ? !!$(this).prop('checked') : $(this).val();
2008 if (typeof control.defaultValue === 'number') {
2009 value = Number(value);
2010 }
2011 if (control.isGlobalSetting) {
2012 power_user[control.property] = value;
2013 } else {
2014 power_user.context[control.property] = value;
2015 }
2016 console.debug(`Setting ${$element.prop('id')} to ${value}`);
2017 if (!CSS.supports('field-sizing', 'content') && $(this).is('textarea')) {
2018 await resetScrollHeight($(this));
2019 }
2020 saveSettingsDebounced();
2021 });
2022
2023 if (control.trigger) {
2024 $element.trigger('input');
2025 }
2026 });
2027
2028 context_presets.forEach((preset) => {
2029 const name = preset.name;
2030 const option = document.createElement('option');
2031 option.value = name;
2032 option.innerText = name;
2033 option.selected = name === power_user.context.preset;
2034 $('#context_presets').append(option);
2035 });
2036
2037 $('#context_presets').on('change', function () {
2038 const name = String($(this).find(':selected').text());
2039 const preset = context_presets.find(x => x.name === name);
2040
2041 if (!preset) {
2042 return;
2043 }
2044
2045 // Migrate story string to add missing fields
2046 autoFixStoryString(preset);
2047
2048 power_user.context.preset = name;
2049
2050 contextControls.forEach(control => {
2051 const presetValue = preset[control.property] ?? control.defaultValue;
2052
2053 if (presetValue !== undefined) {
2054 if (control.isGlobalSetting) {
2055 power_user[control.property] = presetValue;
2056 } else {
2057 power_user.context[control.property] = presetValue;
2058 }
2059
2060 const $element = $(`#${control.id}`);
2061
2062 if (control.isCheckbox) {
2063 $element
2064 .prop('checked', control.isGlobalSetting ? power_user[control.property] : power_user.context[control.property])
2065 .trigger('input');
2066 } else {
2067 $element.val(control.isGlobalSetting ? power_user[control.property] : power_user.context[control.property]);
2068 $element.trigger('input');
2069 }
2070 }
2071 });
2072
2073 if (power_user.instruct.bind_to_context) {
2074 // Select matching instruct preset
2075 for (const instruct_preset of instruct_presets) {
2076 // If instruct preset matches the context template
2077 if (instruct_preset.name === name) {
2078 selectInstructPreset(instruct_preset.name, { isAuto: true });
2079 break;
2080 }
2081 }
2082 }
2083
2084 updateBindModelTemplatesState();
2085
2086 saveSettingsDebounced();
2087 });
2088}
2089
2090
2091/**
2092 * Common function to perform fuzzy search with optional caching
2093 * @template T
2094 * @param {string} type - Type of search from fuzzySearchCategories
2095 * @param {T[]} data - Data array to search in
2096 * @param {Array<{name: string, weight: number, getFn?: (obj: T) => string}>} keys - Fuse.js keys configuration
2097 * @param {string} searchValue - The search term
2098 * @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
2099 * @returns {import('fuse.js').FuseResult<T>[]} Results as items with their score
2100 */
2101export function performFuzzySearch(type, data, keys, searchValue, fuzzySearchCaches = null) {
2102 // Check cache if provided
2103 if (fuzzySearchCaches) {
2104 const cache = fuzzySearchCaches[type];
2105 if (cache?.resultMap.has(searchValue)) {
2106 return cache.resultMap.get(searchValue);
2107 }
2108 }
2109
2110 const fuse = new Fuse(data, {
2111 keys: keys,
2112 includeScore: true,
2113 ignoreLocation: true,
2114 useExtendedSearch: true,
2115 threshold: 0.2,
2116 });
2117
2118 const results = fuse.search(searchValue);
2119
2120 // Store in cache if provided
2121 if (fuzzySearchCaches) {
2122 fuzzySearchCaches[type].resultMap.set(searchValue, results);
2123 }
2124 return results;
2125}
2126
2127/**
2128 * Fuzzy search characters by a search term
2129 * @param {string} searchValue - The search term
2130 * @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
2131 * @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
2132 */
2133export function fuzzySearchCharacters(searchValue, fuzzySearchCaches = null) {
2134 const keys = [
2135 { name: 'data.name', weight: 20 },
2136 { name: '#tags', weight: 10, getFn: (character) => getTagsList(character.avatar).map(x => x.name).join('||') },
2137 { name: 'data.description', weight: 3 },
2138 { name: 'data.mes_example', weight: 3 },
2139 { name: 'data.scenario', weight: 2 },
2140 { name: 'data.personality', weight: 2 },
2141 { name: 'data.first_mes', weight: 2 },
2142 { name: 'data.creator_notes', weight: 2 },
2143 { name: 'data.creator', weight: 1 },
2144 { name: 'data.tags', weight: 1 },
2145 { name: 'data.alternate_greetings', weight: 1 },
2146 ];
2147
2148 return performFuzzySearch(fuzzySearchCategories.characters, characters, keys, searchValue, fuzzySearchCaches);
2149}
2150
2151/**
2152 * Fuzzy search world info entries by a search term
2153 * @param {*[]} data - WI items data array
2154 * @param {string} searchValue - The search term
2155 * @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
2156 * @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
2157 */
2158export function fuzzySearchWorldInfo(data, searchValue, fuzzySearchCaches = null) {
2159 const keys = [
2160 { name: 'key', weight: 20 },
2161 { name: 'group', weight: 15 },
2162 { name: 'comment', weight: 10 },
2163 { name: 'keysecondary', weight: 10 },
2164 { name: 'content', weight: 3 },
2165 { name: 'uid', weight: 1 },
2166 { name: 'automationId', weight: 1 },
2167 ];
2168
2169 return performFuzzySearch(fuzzySearchCategories.worldInfo, data, keys, searchValue, fuzzySearchCaches);
2170}
2171
2172/**
2173 * Fuzzy search persona entries by a search term
2174 * @param {*[]} data - persona data array
2175 * @param {string} searchValue - The search term
2176 * @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
2177 * @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
2178 */
2179export function fuzzySearchPersonas(data, searchValue, fuzzySearchCaches = null) {
2180 const mappedData = data.map(x => ({
2181 key: x,
2182 name: power_user.personas[x] ?? '',
2183 description: power_user.persona_descriptions[x]?.description ?? '',
2184 }));
2185
2186 const keys = [
2187 { name: 'name', weight: 20 },
2188 { name: 'description', weight: 3 },
2189 ];
2190
2191 return performFuzzySearch(fuzzySearchCategories.personas, mappedData, keys, searchValue, fuzzySearchCaches);
2192}
2193
2194/**
2195 * Fuzzy search tags by a search term
2196 * @param {string} searchValue - The search term
2197 * @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
2198 * @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
2199 */
2200export function fuzzySearchTags(searchValue, fuzzySearchCaches = null) {
2201 const keys = [
2202 { name: 'name', weight: 1 },
2203 ];
2204
2205 return performFuzzySearch(fuzzySearchCategories.tags, tags, keys, searchValue, fuzzySearchCaches);
2206}
2207
2208/**
2209 * Fuzzy search groups by a search term
2210 * @param {string} searchValue - The search term
2211 * @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
2212 * @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
2213 */
2214export function fuzzySearchGroups(searchValue, fuzzySearchCaches = null) {
2215 const keys = [
2216 { name: 'name', weight: 20 },
2217 { name: 'members', weight: 15 },
2218 { name: '#tags', weight: 10, getFn: (group) => getTagsList(group.id).map(x => x.name).join('||') },
2219 { name: 'id', weight: 1 },
2220 ];
2221
2222 return performFuzzySearch(fuzzySearchCategories.groups, groups, keys, searchValue, fuzzySearchCaches);
2223}
2224
2225/**
2226 * Renders a story string template with the given parameters.
2227 * @param {object} params Template parameters.
2228 * @param {object} [options] Additional options.
2229 * @param {string} [options.customStoryString] Custom story string template.
2230 * @param {InstructSettings} [options.customInstructSettings] Custom instruct settings.
2231 * @param {ContextSettings} [options.customContextSettings] Custom context settings.
2232 * @returns {string} The rendered story string.
2233 */
2234export function renderStoryString(params, { customStoryString = null, customInstructSettings = null, customContextSettings = null } = {}) {
2235 try {
2236 const instructSettings = structuredClone(customInstructSettings ?? power_user.instruct);
2237 const contextSettings = structuredClone(customContextSettings ?? power_user.context);
2238 const storyString = customStoryString ?? contextSettings.story_string;
2239 const storyStringPosition = contextSettings.story_string_position ?? extension_prompt_types.IN_PROMPT;
2240
2241 // Validate and log possible warnings/errors
2242 validateStoryString(storyString, params);
2243
2244 // compile the story string template into a function, with no HTML escaping
2245 const compiledTemplate = Handlebars.compile(storyString, { noEscape: true });
2246
2247 // render the story string template with the given params
2248 let output = compiledTemplate(params);
2249
2250 // substitute {{macro}} params that are not defined in the story string
2251 output = substituteParams(output, params.user, params.char);
2252
2253 // remove leading newlines
2254 output = output.replace(/^\n+/, '');
2255
2256 // add a newline to the end of the story string if it doesn't have one
2257 if (output.length > 0 && !output.endsWith('\n') && storyStringPosition !== extension_prompt_types.IN_CHAT) {
2258 if (!instructSettings.enabled || (instructSettings.wrap && !instructSettings.story_string_suffix)) {
2259 output += '\n';
2260 }
2261 }
2262
2263 return output;
2264 } catch (e) {
2265 toastr.error('Check the story string template for validity', 'Error rendering story string');
2266 console.error('Error rendering story string', e);
2267 throw e; // rethrow the error
2268 }
2269}
2270
2271/**
2272 * Validate the story string for possible warnings or issues
2273 *
2274 * @param {string} storyString - The story string
2275 * @param {Object} params - The story string parameters
2276 */
2277function validateStoryString(storyString, params) {
2278 /** @type {{hashCache: {[hash: string]: {fieldsWarned: {[key: string]: boolean}}}}} */
2279 const cache = JSON.parse(accountStorage.getItem(storage_keys.storyStringValidationCache)) ?? { hashCache: {} };
2280
2281 const hash = getStringHash(storyString);
2282
2283 // Initialize the cache for the current hash if it doesn't exist
2284 if (!cache.hashCache[hash]) {
2285 cache.hashCache[hash] = { fieldsWarned: {} };
2286 }
2287
2288 const currentCache = cache.hashCache[hash];
2289 const fieldsToWarn = [];
2290
2291 function validateMissingField(field, fallbackLegacyField = null) {
2292 const contains = storyString.includes(`{{${field}}}`) || (!!fallbackLegacyField && storyString.includes(`{{${fallbackLegacyField}}}`));
2293 if (!contains && params[field]) {
2294 const wasLogged = currentCache.fieldsWarned[field];
2295 if (!wasLogged) {
2296 fieldsToWarn.push(field);
2297 currentCache.fieldsWarned[field] = true;
2298 }
2299 console.warn(`The story string does not contain {{${field}}}, but it would contain content:\n`, params[field]);
2300 }
2301 }
2302
2303 validateMissingField('description');
2304 validateMissingField('personality');
2305 validateMissingField('persona');
2306 validateMissingField('scenario');
2307 // validateMissingField('system');
2308 validateMissingField('wiBefore', 'loreBefore');
2309 validateMissingField('wiAfter', 'loreAfter');
2310
2311 if (fieldsToWarn.length > 0) {
2312 const fieldsList = fieldsToWarn.map(field => `{{${field}}}`).join(', ');
2313 toastr.warning(`The story string does not contain the following fields, but they would contain content: ${fieldsList}`, 'Story String Validation');
2314 }
2315
2316 accountStorage.setItem(storage_keys.storyStringValidationCache, JSON.stringify(cache));
2317}
2318
2319
2320const sortFunc = (a, b) => power_user.sort_order == 'asc' ? compareFunc(a, b) : compareFunc(b, a);
2321const compareFunc = (first, second) => {
2322 const a = first[power_user.sort_field];
2323 const b = second[power_user.sort_field];
2324
2325 if (power_user.sort_field === 'create_date') {
2326 return sortMoments(timestampToMoment(b), timestampToMoment(a));
2327 }
2328
2329 switch (power_user.sort_rule) {
2330 case 'boolean':
2331 if (a === true || a === 'true') return 1; // Prioritize 'true' or true
2332 if (b === true || b === 'true') return -1; // Prioritize 'true' or true
2333 if (a && !b) return -1; // Move truthy values to the end
2334 if (!a && b) return 1; // Move falsy values to the beginning
2335 if (a === b) return 0; // Sort equal values normally
2336 return a < b ? -1 : 1; // Sort non-boolean values normally
2337 default:
2338 return typeof a == 'string'
2339 ? a.localeCompare(b)
2340 : a - b;
2341 }
2342};
2343
2344/**
2345 * Sorts an array of entities based on the current sort settings
2346 * @param {any[]} entities An array of objects with an `item` property
2347 * @param {boolean} forceSearch Whether to force search sorting
2348 * @param {import('./filters.js').FilterHelper} [filterHelper=null] Filter helper to use
2349 */
2350export function sortEntitiesList(entities, forceSearch, filterHelper = null) {
2351 filterHelper = filterHelper ?? entitiesFilter;
2352 if (power_user.sort_field == undefined || entities.length === 0) {
2353 return;
2354 }
2355
2356 const isSearch = forceSearch || $('#character_sort_order option[data-field="search"]').is(':selected');
2357
2358 if (!isSearch && power_user.sort_order === 'random') {
2359 shuffle(entities);
2360 return;
2361 }
2362
2363 entities.sort((a, b) => {
2364 // Sort tags/folders will always be at the top. Their original sorting will be kept, to respect manual tag sorting.
2365 if (a.type === 'tag' || b.type === 'tag') {
2366 // The one that is a tag will be at the top
2367 return (a.type === 'tag' ? -1 : 1) - (b.type === 'tag' ? -1 : 1);
2368 }
2369
2370 // If we have search sorting, we take scores and use those
2371 if (isSearch) {
2372 const aScore = filterHelper.getScore(FILTER_TYPES.SEARCH, `${a.type}.${a.id}`);
2373 const bScore = filterHelper.getScore(FILTER_TYPES.SEARCH, `${b.type}.${b.id}`);
2374 return (aScore - bScore);
2375 }
2376
2377 return sortFunc(a.item, b.item);
2378 });
2379}
2380
2381/**
2382 * Updates the current UI theme file.
2383 */
2384async function updateTheme() {
2385 await saveTheme(power_user.theme);
2386 toastr.success('Theme saved.');
2387}
2388
2389async function deleteTheme() {
2390 const themeName = power_user.theme;
2391
2392 if (!themeName) {
2393 toastr.info('No theme selected.');
2394 return;
2395 }
2396
2397 const template = $(await renderTemplateAsync('themeDelete', { themeName }));
2398 const confirm = await callGenericPopup(template, POPUP_TYPE.CONFIRM);
2399
2400 if (!confirm) {
2401 return;
2402 }
2403
2404 const response = await fetch('/api/themes/delete', {
2405 method: 'POST',
2406 headers: getRequestHeaders(),
2407 body: JSON.stringify({ name: themeName }),
2408 });
2409
2410 if (!response.ok) {
2411 toastr.error('Failed to delete theme. Check the console for more information.');
2412 return;
2413 }
2414
2415 const themeIndex = themes.findIndex(x => x.name == themeName);
2416
2417 if (themeIndex !== -1) {
2418 themes.splice(themeIndex, 1);
2419 $(`#themes option[value="${themeName}"]`).remove();
2420 power_user.theme = themes[0]?.name;
2421 saveSettingsDebounced();
2422 if (power_user.theme) {
2423 applyTheme(power_user.theme);
2424 }
2425 toastr.success('Theme deleted.');
2426 }
2427}
2428
2429/**
2430 * Exports the current theme to a file.
2431 */
2432async function exportTheme() {
2433 const themeFile = await saveTheme(power_user.theme);
2434 const fileName = `${themeFile.name}.json`;
2435 download(JSON.stringify(themeFile, null, 4), fileName, 'application/json');
2436}
2437
2438/**
2439 * Imports a theme from a file.
2440 * @param {File} file File to import.
2441 * @returns {Promise<void>} A promise that resolves when the theme is imported.
2442 */
2443async function importTheme(file) {
2444 if (!file) {
2445 return;
2446 }
2447
2448 const fileText = await getFileText(file);
2449 const parsed = JSON.parse(fileText);
2450
2451 if (!parsed.name) {
2452 throw new Error('Missing name');
2453 }
2454
2455 if (themes.some(t => t.name === parsed.name)) {
2456 throw new Error('Theme with that name already exists');
2457 }
2458
2459 if (typeof parsed.custom_css === 'string' && parsed.custom_css.includes('@import')) {
2460 const template = $(await renderTemplateAsync('themeImportWarning'));
2461 const confirm = await callGenericPopup(template, POPUP_TYPE.CONFIRM);
2462 if (!confirm) {
2463 throw new Error('Theme contains @import lines');
2464 }
2465 }
2466
2467 themes.push(parsed);
2468 await saveTheme(parsed.name, getNewTheme(parsed));
2469 const option = document.createElement('option');
2470 option.selected = false;
2471 option.value = parsed.name;
2472 option.innerText = parsed.name;
2473 $('#themes').append(option);
2474 saveSettingsDebounced();
2475 toastr.success(parsed.name, 'Theme imported');
2476}
2477
2478/**
2479 * Saves the current theme to the server.
2480 * @param {string|undefined} name Theme name. If undefined, a popup will be shown to enter a name.
2481 * @param {object|undefined} theme Theme object. If undefined, the current theme will be saved.
2482 * @returns {Promise<object>} A promise that resolves when the theme is saved.
2483 */
2484async function saveTheme(name = undefined, theme = undefined) {
2485 if (typeof name !== 'string') {
2486 const newName = await callGenericPopup('Enter a theme preset name:', POPUP_TYPE.INPUT, power_user.theme);
2487
2488 if (!newName) {
2489 return;
2490 }
2491
2492 name = await getSanitizedFilename(String(newName));
2493 }
2494
2495 if (typeof theme !== 'object') {
2496 theme = getThemeObject(name);
2497 }
2498
2499 const response = await fetch('/api/themes/save', {
2500 method: 'POST',
2501 headers: getRequestHeaders(),
2502 body: JSON.stringify(theme),
2503 });
2504
2505 if (!response.ok) {
2506 toastr.error('Check the server connection and reload the page to prevent data loss.', 'Theme could not be saved');
2507 console.error('Theme could not be saved', response);
2508 throw new Error('Theme could not be saved');
2509 }
2510
2511 const themeIndex = themes.findIndex(x => x.name == name);
2512
2513 if (themeIndex == -1) {
2514 themes.push(theme);
2515 const option = document.createElement('option');
2516 option.selected = true;
2517 option.value = name;
2518 option.innerText = name;
2519 $('#themes').append(option);
2520 } else {
2521 themes[themeIndex] = theme;
2522 $(`#themes option[value="${name}"]`).prop('selected', true);
2523 }
2524
2525 power_user.theme = name;
2526 saveSettingsDebounced();
2527
2528 return theme;
2529}
2530
2531/**
2532 * Gets a snapshot of the current theme settings.
2533 * @param {string} name Name of the theme
2534 */
2535export function getThemeObject(name) {
2536 return {
2537 name,
2538 blur_strength: power_user.blur_strength,
2539 main_text_color: power_user.main_text_color,
2540 italics_text_color: power_user.italics_text_color,
2541 underline_text_color: power_user.underline_text_color,
2542 quote_text_color: power_user.quote_text_color,
2543 blur_tint_color: power_user.blur_tint_color,
2544 chat_tint_color: power_user.chat_tint_color,
2545 user_mes_blur_tint_color: power_user.user_mes_blur_tint_color,
2546 bot_mes_blur_tint_color: power_user.bot_mes_blur_tint_color,
2547 shadow_color: power_user.shadow_color,
2548 shadow_width: power_user.shadow_width,
2549 border_color: power_user.border_color,
2550 font_scale: power_user.font_scale,
2551 fast_ui_mode: power_user.fast_ui_mode,
2552 waifuMode: power_user.waifuMode,
2553 avatar_style: power_user.avatar_style,
2554 chat_display: power_user.chat_display,
2555 toastr_position: power_user.toastr_position,
2556 noShadows: power_user.noShadows,
2557 chat_width: power_user.chat_width,
2558 timer_enabled: power_user.timer_enabled,
2559 timestamps_enabled: power_user.timestamps_enabled,
2560 timestamp_model_icon: power_user.timestamp_model_icon,
2561
2562 mesIDDisplay_enabled: power_user.mesIDDisplay_enabled,
2563 hideChatAvatars_enabled: power_user.hideChatAvatars_enabled,
2564 message_token_count_enabled: power_user.message_token_count_enabled,
2565 expand_message_actions: power_user.expand_message_actions,
2566 enableZenSliders: power_user.enableZenSliders,
2567 enableLabMode: power_user.enableLabMode,
2568 hotswap_enabled: power_user.hotswap_enabled,
2569 custom_css: power_user.custom_css,
2570 bogus_folders: power_user.bogus_folders,
2571 zoomed_avatar_magnification: power_user.zoomed_avatar_magnification,
2572 reduced_motion: power_user.reduced_motion,
2573 compact_input_area: power_user.compact_input_area,
2574 show_swipe_num_all_messages: power_user.show_swipe_num_all_messages,
2575 click_to_edit: power_user.click_to_edit,
2576 media_display: power_user.media_display,
2577 };
2578}
2579
2580/**
2581 * Applies imported theme properties to the theme object.
2582 * @param {object} parsed Parsed object to get the theme from.
2583 * @returns {Theme} Theme assigned to the parsed object.
2584 */
2585function getNewTheme(parsed) {
2586 const theme = getThemeObject(parsed.name);
2587 for (const key in parsed) {
2588 if (Object.hasOwn(theme, key)) {
2589 theme[key] = parsed[key];
2590 }
2591 }
2592 return theme;
2593}
2594
2595async function saveMovingUI() {
2596 const popupResult = await callGenericPopup('Enter a name for the MovingUI Preset:', POPUP_TYPE.INPUT);
2597
2598 if (!popupResult) {
2599 return;
2600 }
2601
2602 const name = await getSanitizedFilename(String(popupResult));
2603
2604 const movingUIPreset = {
2605 name,
2606 movingUIState: power_user.movingUIState,
2607 };
2608 console.log(movingUIPreset);
2609
2610 const response = await fetch('/api/moving-ui/save', {
2611 method: 'POST',
2612 headers: getRequestHeaders(),
2613 body: JSON.stringify(movingUIPreset),
2614 });
2615
2616 if (response.ok) {
2617 const movingUIPresetIndex = movingUIPresets.findIndex(x => x.name == name);
2618
2619 if (movingUIPresetIndex == -1) {
2620 movingUIPresets.push(movingUIPreset);
2621 const option = document.createElement('option');
2622 option.selected = true;
2623 option.value = name;
2624 option.innerText = name;
2625 $('#movingUIPresets').append(option);
2626 } else {
2627 movingUIPresets[movingUIPresetIndex] = movingUIPreset;
2628 $(`#movingUIPresets option[value="${name}"]`).prop('selected', true);
2629 }
2630
2631 power_user.movingUIPreset = name;
2632 saveSettingsDebounced();
2633 } else {
2634 toastr.error('Failed to save MovingUI state.');
2635 console.error('MovingUI could not be saved', response);
2636 }
2637}
2638
2639/**
2640 * Resets the movable styles of the given element to their unset values.
2641 * @param {string} id Element ID
2642 */
2643export function resetMovableStyles(id) {
2644 const panelStyles = ['top', 'left', 'right', 'bottom', 'height', 'width', 'margin'];
2645
2646 const panel = document.getElementById(id);
2647
2648 if (panel) {
2649 panelStyles.forEach((style) => {
2650 panel.style[style] = '';
2651 });
2652 }
2653}
2654
2655async function resetMovablePanels(type) {
2656 const panelIds = [
2657 'sheld',
2658 'left-nav-panel',
2659 'right-nav-panel',
2660 'WorldInfo',
2661 'floatingPrompt',
2662 'expression-holder',
2663 'groupMemberListPopout',
2664 'summaryExtensionPopout',
2665 'gallery',
2666 'logprobsViewer',
2667 'cfgConfig',
2668 ];
2669
2670 /**
2671 * @type {HTMLElement[]} Generic panels that don't have a known ID
2672 */
2673 const draggedElements = Array.from(document.querySelectorAll('[data-dragged]'));
2674 const allDraggable = panelIds.map(id => document.getElementById(id)).concat(draggedElements).filter(onlyUnique);
2675
2676 const panelStyles = ['top', 'left', 'right', 'bottom', 'height', 'width', 'margin'];
2677 allDraggable.forEach((panel) => {
2678 if (panel) {
2679 $(panel).addClass('resizing');
2680 panelStyles.forEach((style) => {
2681 panel.style[style] = '';
2682 });
2683 }
2684 });
2685
2686 /**
2687 * @type {HTMLElement[]} Zoomed avatars that are currently being resized
2688 */
2689 const zoomedAvatars = Array.from(document.querySelectorAll('.zoomed_avatar'));
2690 if (zoomedAvatars.length > 0) {
2691 zoomedAvatars.forEach((avatar) => {
2692 avatar.classList.add('resizing');
2693 panelStyles.forEach((style) => {
2694 avatar.style[style] = '';
2695 });
2696 });
2697 }
2698
2699 $('[data-dragged="true"]').removeAttr('data-dragged');
2700 await delay(50);
2701
2702 power_user.movingUIState = {};
2703
2704 //if user manually resets panels, deselect the current preset
2705 if (type !== 'quiet' && type !== 'resize') {
2706 power_user.movingUIPreset = 'Default';
2707 $('#movingUIPresets option[value="Default"]').prop('selected', true);
2708 }
2709
2710 saveSettingsDebounced();
2711 await eventSource.emit(event_types.MOVABLE_PANELS_RESET);
2712
2713 eventSource.once(event_types.SETTINGS_UPDATED, () => {
2714 $('.resizing').removeClass('resizing');
2715 //if happening as part of preset application, do it quietly.
2716 if (type === 'quiet') {
2717 return;
2718 //if happening due to resize, tell user.
2719 } else if (type === 'resize') {
2720 toastr.warning('Panel positions reset due to zoom/resize');
2721 //if happening due to manual button press
2722 } else {
2723 toastr.success('Panel positions reset');
2724 }
2725 });
2726}
2727
2728/**
2729 * Finds the ID of the tag with the given name.
2730 * @param {string} name
2731 * @returns {string} The ID of the tag with the given name.
2732 */
2733function findTagIdByName(name) {
2734 const matchTypes = [
2735 (a, b) => a === b,
2736 (a, b) => a.startsWith(b),
2737 (a, b) => a.includes(b),
2738 ];
2739
2740 // Only get tags that contain at least one record in the tag_map
2741 const liveTagIds = new Set(Object.values(tag_map).flat());
2742 const liveTags = tags.filter(x => liveTagIds.has(x.id));
2743
2744 const exactNameMatchIndex = liveTags.map(x => x.name.toLowerCase()).indexOf(name.toLowerCase());
2745
2746 if (exactNameMatchIndex !== -1) {
2747 return liveTags[exactNameMatchIndex].id;
2748 }
2749
2750 for (const matchType of matchTypes) {
2751 const index = liveTags.findIndex(x => matchType(x.name.toLowerCase(), name.toLowerCase()));
2752 if (index !== -1) {
2753 return liveTags[index].id;
2754 }
2755 }
2756}
2757
2758async function doRandomChat(_, tagName) {
2759 /**
2760 * Gets the ID of a random character.
2761 * @returns {string} The order index of the randomly selected character.
2762 */
2763 function getRandomCharacterId() {
2764 if (!tagName) {
2765 return Math.floor(Math.random() * characters.length).toString();
2766 }
2767
2768 const tagId = findTagIdByName(tagName);
2769 const taggedCharacters = Object.entries(tag_map)
2770 .filter(x => x[1].includes(tagId)) // Get only records that include the tag
2771 .map(x => x[0]) // Map the character avatar
2772 .filter(x => characters.find(y => y.avatar === x)); // Filter out characters that don't exist
2773 const randomCharacter = taggedCharacters[Math.floor(Math.random() * taggedCharacters.length)];
2774 const randomIndex = characters.findIndex(x => x.avatar === randomCharacter);
2775 if (randomIndex === -1) {
2776 return;
2777 }
2778 return randomIndex.toString();
2779 }
2780
2781 resetSelectedGroup();
2782 const characterId = getRandomCharacterId();
2783 if (!characterId) {
2784 toastr.error('No characters found');
2785 return;
2786 }
2787 setCharacterId(characterId);
2788 setActiveCharacter(characters[characterId]?.avatar);
2789 setActiveGroup(null);
2790 await delay(1);
2791 await reloadCurrentChat();
2792 return characters[characterId]?.name;
2793}
2794
2795/**
2796 * Loads the chat until the given message ID is displayed.
2797 * @param {number} mesId
2798 * @returns JQuery<HTMLElement>
2799 */
2800async function loadUntilMesId(mesId) {
2801 let target;
2802
2803 while (getFirstDisplayedMessageId() > mesId && getFirstDisplayedMessageId() !== 0) {
2804 await showMoreMessages();
2805 await delay(1);
2806 target = $('#chat').find(`.mes[mesid="${mesId}"]`);
2807
2808 if (target.length) {
2809 break;
2810 }
2811 }
2812
2813 if (!target.length) {
2814 toastr.error(`Could not find message with ID: ${mesId}`);
2815 return target;
2816 }
2817
2818 return target;
2819}
2820
2821async function doMesCut(_, text) {
2822 console.debug(`was asked to cut message id #${text}`);
2823 const range = stringToRange(text, 0, chat.length - 1);
2824
2825 //reject invalid args or no args
2826 if (!range) {
2827 toastr.warning('Must provide a Message ID or a range to cut.');
2828 return;
2829 }
2830
2831 let totalMesToCut = (range.end - range.start) + 1;
2832 let mesIDToCut = range.start;
2833 let cutText = '';
2834
2835 for (let i = 0; i < totalMesToCut; i++) {
2836 cutText += (chat[mesIDToCut]?.mes || '') + '\n';
2837 let mesToCut = $('#chat').find(`.mes[mesid=${mesIDToCut}]`);
2838
2839 if (!mesToCut.length) {
2840 mesToCut = await loadUntilMesId(mesIDToCut);
2841
2842 if (!mesToCut || !mesToCut.length) {
2843 return;
2844 }
2845 }
2846
2847 setEditedMessageId(mesIDToCut);
2848 await deleteMessage(mesIDToCut, null, false);
2849 }
2850
2851 await saveChatConditional();
2852
2853 return cutText;
2854}
2855
2856async function doDelMode(_, text) {
2857 //reject invalid args
2858 if (text && isNaN(text)) {
2859 toastr.warning('Must enter a number or nothing.');
2860 return '';
2861 }
2862
2863 // Just enter the delete mode.
2864 if (!text) {
2865 $('#option_delete_mes').trigger('click', { fromSlashCommand: true });
2866 return '';
2867 }
2868
2869 const count = Number(text);
2870
2871 // Nothing to delete.
2872 if (count < 1) {
2873 return '';
2874 }
2875
2876 if (count > chat.length) {
2877 toastr.warning(`Cannot delete more than ${chat.length} messages.`);
2878 return '';
2879 }
2880
2881 const range = `${chat.length - count}-${chat.length - 1}`;
2882 return doMesCut(_, range);
2883}
2884
2885function doResetPanels() {
2886 $('#movingUIreset').trigger('click');
2887 return '';
2888}
2889
2890async function setAvgBG(args) {
2891 const nameOverride = args?.name ? String(args.name).trim() : '';
2892 const bgOverride = args?.bg ? String(args.bg).trim() : '';
2893 const force = isTrueBoolean(args?.force?.toString());
2894
2895 let bgUrl;
2896
2897 if (bgOverride) {
2898 // Use the specified background file
2899 const isCustom = isCustomBackgroundUrl(bgOverride);
2900 bgUrl = isCustom ? bgOverride : getBackgroundPath(bgOverride);
2901 } else {
2902 // Use the currently active background
2903 bgUrl = $('#bg1')
2904 .css('background-image')
2905 .replace(/^url\(['"]?/, '')
2906 .replace(/['"]?\)$/, '');
2907 }
2908
2909 if (!bgUrl || bgUrl === 'none') {
2910 toastr.warning('No background image set.');
2911 return '';
2912 }
2913
2914 // Build theme name from background filename or use override
2915 const bgName = deriveBackgroundName(bgUrl);
2916 const themeName = nameOverride || `bgcol - ${bgName}`;
2917
2918 // Check if a theme with the same name already exists
2919 if (themes.some(t => t.name === themeName) && !force) {
2920 toastr.warning('Pass "force=true" to overwrite.', `A theme named "${themeName}" already exists.`);
2921 return '';
2922 }
2923
2924 const bgimg = new Image();
2925 bgimg.crossOrigin = 'anonymous';
2926 bgimg.src = bgUrl;
2927
2928 await new Promise((resolve, reject) => {
2929 bgimg.onload = resolve;
2930 bgimg.onerror = () => reject(new Error('Failed to load background image'));
2931 });
2932
2933 // Extract dominant vivid color using Oklch-weighted sampling
2934 const dominantRgb = extractDominantColor(bgimg);
2935
2936 // Generate a full theme palette from the dominant color
2937 const palette = generateThemePalette(dominantRgb);
2938
2939 // Create theme object from current settings, then override colors
2940 const theme = getThemeObject(themeName);
2941 Object.assign(theme, palette);
2942
2943 // Save as a new theme
2944 await saveTheme(themeName, theme);
2945 applyTheme(themeName);
2946
2947 toastr.success(`Theme "${themeName}" generated and applied.`);
2948 return '';
2949}
2950
2951async function setThemeCallback(_, themeName) {
2952 if (!themeName) {
2953 // allow reporting of the theme name if called without args
2954 // for use in ST Scripts via pipe
2955 return power_user.theme;
2956 }
2957
2958 // @ts-ignore
2959 const fuse = new Fuse(themes, {
2960 keys: [
2961 { name: 'name', weight: 1 },
2962 ],
2963 });
2964
2965 const results = fuse.search(themeName);
2966 console.debug('Theme fuzzy search results for ' + themeName, results);
2967 const theme = results[0]?.item;
2968
2969 if (!theme) {
2970 toastr.warning(`Could not find theme with name: ${themeName}`);
2971 return;
2972 }
2973
2974 power_user.theme = theme.name;
2975 applyTheme(theme.name);
2976 $('#themes').val(theme.name);
2977 saveSettingsDebounced();
2978 return '';
2979}
2980
2981async function setmovingUIPreset(_, text) {
2982 // @ts-ignore
2983 const fuse = new Fuse(movingUIPresets, {
2984 keys: [
2985 { name: 'name', weight: 1 },
2986 ],
2987 });
2988
2989 const results = fuse.search(text);
2990 console.debug('movingUI preset fuzzy search results for ' + text, results);
2991 const preset = results[0]?.item;
2992
2993 if (!preset) {
2994 toastr.warning(`Could not find preset with name: ${text}`);
2995 return;
2996 }
2997
2998 power_user.movingUIPreset = preset.name;
2999 applyMovingUIPreset(preset.name);
3000 $('#movingUIPresets').val(preset.name);
3001 saveSettingsDebounced();
3002 return '';
3003}
3004
3005const EPHEMERAL_STOPPING_STRINGS = [];
3006
3007/**
3008 * Adds a stopping string to the list of stopping strings that are only used for the next generation.
3009 * @param {string} value The stopping string to add
3010 */
3011export function addEphemeralStoppingString(value) {
3012 if (!EPHEMERAL_STOPPING_STRINGS.includes(value)) {
3013 console.debug('Adding ephemeral stopping string:', value);
3014 EPHEMERAL_STOPPING_STRINGS.push(value);
3015 }
3016}
3017
3018export function flushEphemeralStoppingStrings() {
3019 if (EPHEMERAL_STOPPING_STRINGS.length === 0) {
3020 return;
3021 }
3022
3023 console.debug('Flushing ephemeral stopping strings:', EPHEMERAL_STOPPING_STRINGS);
3024 EPHEMERAL_STOPPING_STRINGS.splice(0, EPHEMERAL_STOPPING_STRINGS.length);
3025}
3026
3027/**
3028 * Checks if the generated text should be filtered based on the auto-swipe settings.
3029 * @param {string} text The text to check
3030 * @returns {boolean} If the generated text should be filtered
3031 */
3032export function generatedTextFiltered(text) {
3033 /**
3034 * Checks if the given text contains any of the blacklisted words.
3035 * @param {string} text The text to check
3036 * @param {string[]} blacklist The list of blacklisted words
3037 * @param {number} threshold The number of blacklisted words that need to be present to trigger the check
3038 * @returns {boolean} Whether the text contains blacklisted words
3039 */
3040 function containsBlacklistedWords(text, blacklist, threshold) {
3041 const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
3042 const matches = text.match(regex) || [];
3043 return matches.length >= threshold;
3044 }
3045
3046 // Make sure a generated text is non-empty
3047 // Otherwise we might get in a loop with a broken API
3048 text = text.trim();
3049 if (text.length > 0) {
3050 if (power_user.auto_swipe_minimum_length) {
3051 if (text.length < power_user.auto_swipe_minimum_length) {
3052 console.log('Generated text size too small');
3053 return true;
3054 }
3055 }
3056 if (power_user.auto_swipe_blacklist.length && power_user.auto_swipe_blacklist_threshold) {
3057 if (containsBlacklistedWords(text, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
3058 console.log('Generated text has blacklisted words');
3059 return true;
3060 }
3061 }
3062 }
3063
3064 return false;
3065}
3066
3067/**
3068 * Gets the custom stopping strings from the power user settings.
3069 * @param {number | undefined} limit Number of strings to return. If 0 or undefined, returns all strings.
3070 * @returns {string[]} An array of custom stopping strings
3071 */
3072export function getCustomStoppingStrings(limit = undefined) {
3073 function getPermanent() {
3074 try {
3075 // If there's no custom stopping strings, return an empty array
3076 if (!power_user.custom_stopping_strings) {
3077 return [];
3078 }
3079
3080 // Parse the JSON string
3081 let strings = JSON.parse(power_user.custom_stopping_strings);
3082
3083 // Make sure it's an array
3084 if (!Array.isArray(strings)) {
3085 return [];
3086 }
3087
3088 // Make sure all the elements are strings and non-empty.
3089 strings = strings.filter(s => typeof s === 'string' && s.length > 0);
3090
3091 // Substitute params if necessary
3092 if (power_user.custom_stopping_strings_macro) {
3093 strings = strings.map(x => substituteParams(x));
3094 }
3095
3096 return strings;
3097 } catch (error) {
3098 // If there's an error, return an empty array
3099 console.warn('Error parsing custom stopping strings:', error);
3100 return [];
3101 }
3102 }
3103
3104 const permanent = getPermanent();
3105 const ephemeral = EPHEMERAL_STOPPING_STRINGS;
3106 const strings = [...permanent, ...ephemeral];
3107
3108 // Apply the limit. If limit is 0, return all strings.
3109 if (limit > 0) {
3110 return strings.slice(0, limit);
3111 }
3112
3113 return strings;
3114}
3115
3116export function forceCharacterEditorTokenize() {
3117 $('[data-token-counter]').each(function () {
3118 $(document.getElementById($(this).data('token-counter'))).data('last-value-hash', '');
3119 });
3120 $('#rm_ch_create_block').trigger('input');
3121 $('#character_popup').trigger('input');
3122}
3123
3124jQuery(() => {
3125 const adjustAutocompleteDebounced = debounce(() => {
3126 $('.ui-autocomplete-input').each(function () {
3127 const isOpen = $(this).autocomplete('widget')[0].style.display !== 'none';
3128 if (isOpen) {
3129 $(this).autocomplete('search');
3130 }
3131 });
3132 });
3133
3134 const reportZoomLevelDebounced = debounce(() => {
3135 const zoomLevel = parseFloat(Number(window.devicePixelRatio).toFixed(2)) || 1;
3136 const winWidth = window.innerWidth;
3137 const winHeight = window.innerHeight;
3138 const originalWidth = winWidth * zoomLevel;
3139 const originalHeight = winHeight * zoomLevel;
3140 console.debug(`Window resize: ${coreTruthWinWidth}x${coreTruthWinHeight} -> ${window.innerWidth}x${window.innerHeight}`);
3141 console.debug(`Zoom: ${zoomLevel}, X:${winWidth}, Y:${winHeight}, original: ${originalWidth}x${originalHeight} `);
3142 return zoomLevel;
3143 });
3144
3145 var coreTruthWinWidth = window.innerWidth;
3146 var coreTruthWinHeight = window.innerHeight;
3147
3148 $(window).on('resize', async () => {
3149 adjustAutocompleteDebounced();
3150 setHotswapsDebounced();
3151
3152 if (isMobile()) {
3153 return;
3154 }
3155
3156 reportZoomLevelDebounced();
3157
3158 //attempt to scale movingUI elements naturally across window resizing/zooms
3159 //this will still break if the zoom level causes mobile styles to come into play.
3160 const scaleY = parseFloat(Number(window.innerHeight / coreTruthWinHeight).toFixed(4));
3161 const scaleX = parseFloat(Number(window.innerWidth / coreTruthWinWidth).toFixed(4));
3162
3163 if (Object.keys(power_user.movingUIState).length > 0) {
3164 for (var elmntName of Object.keys(power_user.movingUIState)) {
3165 var elmntState = power_user.movingUIState[elmntName];
3166 var oldHeight = elmntState.height;
3167 var oldWidth = elmntState.width;
3168 var oldLeft = elmntState.left;
3169 var oldTop = elmntState.top;
3170 var oldBottom = elmntState.bottom;
3171 var oldRight = elmntState.right;
3172 var newHeight, newWidth, newTop, newBottom, newLeft, newRight;
3173
3174 newHeight = Number(oldHeight * scaleY).toFixed(0);
3175 newWidth = Number(oldWidth * scaleX).toFixed(0);
3176 newLeft = Number(oldLeft * scaleX).toFixed(0);
3177 newTop = Number(oldTop * scaleY).toFixed(0);
3178 newBottom = Number(oldBottom * scaleY).toFixed(0);
3179 newRight = Number(oldRight * scaleX).toFixed(0);
3180 try {
3181 var elmnt = $('#' + $.escapeSelector(elmntName));
3182 if (elmnt.length) {
3183 console.log(`scaling ${elmntName} by ${scaleX}x${scaleY} to ${newWidth}x${newHeight}`);
3184 elmnt.css('height', newHeight);
3185 elmnt.css('width', newWidth);
3186 elmnt.css('inset', `${newTop}px ${newRight}px ${newBottom}px ${newLeft}px`);
3187 power_user.movingUIState[elmntName].height = newHeight;
3188 power_user.movingUIState[elmntName].width = newWidth;
3189 power_user.movingUIState[elmntName].top = newTop;
3190 power_user.movingUIState[elmntName].bottom = newBottom;
3191 power_user.movingUIState[elmntName].left = newLeft;
3192 power_user.movingUIState[elmntName].right = newRight;
3193 } else {
3194 console.log(`skipping ${elmntName} because it doesn't exist in the DOM`);
3195 }
3196 } catch (err) {
3197 console.log(`error occurred while processing ${elmntName}: ${err}`);
3198 }
3199 }
3200 } else {
3201 console.debug('aborting MUI reset', Object.keys(power_user.movingUIState).length);
3202 }
3203 saveSettingsDebounced();
3204 coreTruthWinWidth = window.innerWidth;
3205 coreTruthWinHeight = window.innerHeight;
3206 });
3207
3208 // Settings that go to settings.json
3209 $('#collapse-newlines-checkbox').on('change', function () {
3210 power_user.collapse_newlines = !!$(this).prop('checked');
3211 saveSettingsDebounced();
3212 });
3213
3214 // include newline is the child of trim sentences
3215 // if include newline is checked, trim sentences must be checked
3216 // if trim sentences is unchecked, include newline must be unchecked
3217 $('#trim_sentences_checkbox').on('change', function () {
3218 power_user.trim_sentences = !!$(this).prop('checked');
3219 saveSettingsDebounced();
3220 });
3221
3222 $('#single_line').on('input', function () {
3223 const value = !!$(this).prop('checked');
3224 power_user.single_line = value;
3225 saveSettingsDebounced();
3226 });
3227
3228 $('#context_derived').on('input', function () {
3229 const value = !!$(this).prop('checked');
3230 power_user.context_derived = value;
3231 saveSettingsDebounced();
3232 });
3233
3234 $('#context_derived').on('change', function () {
3235 $('#context_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.context_derived);
3236 });
3237
3238 $('#instruct_derived').on('input', function () {
3239 const value = !!$(this).prop('checked');
3240 power_user.instruct_derived = value;
3241 saveSettingsDebounced();
3242 });
3243
3244 $('#instruct_derived').on('change', function () {
3245 $('#instruct_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct_derived);
3246 });
3247
3248 $('#context_size_derived').on('input', function () {
3249 const value = !!$(this).prop('checked');
3250 power_user.context_size_derived = value;
3251 saveSettingsDebounced();
3252 });
3253
3254 $('#context_size_derived').on('change', function () {
3255 $('#context_size_derived').prop('checked', !!power_user.context_size_derived);
3256 });
3257
3258 $('#context_story_string_position').on('input', function () {
3259 const value = Number($(this).val());
3260 $('#context_story_string_inject_settings').toggle(value === extension_prompt_types.IN_CHAT);
3261 });
3262
3263 $('#bind_model_templates').on('input', function () {
3264 if (bindModelTemplates(power_user, online_status)) {
3265 saveSettingsDebounced();
3266 }
3267 });
3268
3269 $('#bind_model_templates').on('change', updateBindModelTemplatesState);
3270
3271 $('#always-force-name2-checkbox').on('change', function () {
3272 power_user.always_force_name2 = !!$(this).prop('checked');
3273 saveSettingsDebounced();
3274 });
3275
3276 $('#markdown_escape_strings').on('input', function () {
3277 power_user.markdown_escape_strings = String($(this).val());
3278 saveSettingsDebounced();
3279 reloadMarkdownProcessor();
3280 });
3281
3282 $('#start_reply_with').on('input', function () {
3283 power_user.user_prompt_bias = String($(this).val());
3284 saveSettingsDebounced();
3285 });
3286
3287 $('#chat-show-reply-prefix-checkbox').on('change', function () {
3288 power_user.show_user_prompt_bias = !!$(this).prop('checked');
3289 reloadCurrentChat();
3290 saveSettingsDebounced();
3291 });
3292
3293 $('#auto_continue_enabled').on('change', function () {
3294 power_user.auto_continue.enabled = $(this).prop('checked');
3295 saveSettingsDebounced();
3296 });
3297
3298 $('#auto_continue_allow_chat_completions').on('change', function () {
3299 power_user.auto_continue.allow_chat_completions = !!$(this).prop('checked');
3300 saveSettingsDebounced();
3301 });
3302
3303 $('#auto_continue_target_length').on('input', function () {
3304 power_user.auto_continue.target_length = Number($(this).val());
3305 saveSettingsDebounced();
3306 });
3307
3308 $('#example_messages_behavior').on('change', function () {
3309 const selectedOption = String($(this).find(':selected').val());
3310 console.log('Setting example messages behavior to', selectedOption);
3311
3312 switch (selectedOption) {
3313 case 'normal':
3314 power_user.pin_examples = false;
3315 power_user.strip_examples = false;
3316 break;
3317 case 'keep':
3318 power_user.pin_examples = true;
3319 power_user.strip_examples = false;
3320 break;
3321 case 'strip':
3322 power_user.pin_examples = false;
3323 power_user.strip_examples = true;
3324 break;
3325 }
3326
3327 console.debug('power_user.pin_examples', power_user.pin_examples);
3328 console.debug('power_user.strip_examples', power_user.strip_examples);
3329
3330 saveSettingsDebounced();
3331 });
3332
3333 $('#fast_ui_mode').on('change', function () {
3334 power_user.fast_ui_mode = $(this).prop('checked');
3335 switchUiMode();
3336 saveSettingsDebounced();
3337 });
3338
3339 $('#waifuMode').on('change', () => {
3340 power_user.waifuMode = !!$('#waifuMode').prop('checked');
3341 switchWaifuMode();
3342 saveSettingsDebounced();
3343 });
3344
3345 $('#customCSS').on('input', () => {
3346 power_user.custom_css = String($('#customCSS').val());
3347 saveSettingsDebounced();
3348 applyCustomCSS();
3349 });
3350
3351 $('#movingUImode').on('change', function () {
3352 power_user.movingUI = $(this).prop('checked');
3353 switchMovingUI();
3354 saveSettingsDebounced();
3355 });
3356
3357 $('#noShadowsmode').on('change', function () {
3358 power_user.noShadows = $(this).prop('checked');
3359 applyNoShadows();
3360 saveSettingsDebounced();
3361 });
3362
3363 $('#movingUIreset').on('click', resetMovablePanels);
3364
3365 $('#avatar_style').on('change', function () {
3366 const value = $(this).find(':selected').val();
3367 power_user.avatar_style = Number(value);
3368 applyAvatarStyle();
3369 saveSettingsDebounced();
3370 });
3371
3372 $('#chat_display').on('change', function () {
3373 const value = $(this).find(':selected').val();
3374 power_user.chat_display = Number(value);
3375 applyChatDisplay();
3376 saveSettingsDebounced();
3377 });
3378
3379 $('#toastr_position').on('change', function () {
3380 const value = $(this).find(':selected').val();
3381 power_user.toastr_position = String(value);
3382 applyToastrPosition();
3383 saveSettingsDebounced();
3384 });
3385
3386 $('#chat_width_slider').on('input', function (e, data) {
3387 const applyMode = data?.forced ? 'forced' : 'normal';
3388 power_user.chat_width = Number($(this).val());
3389 applyChatWidth(applyMode);
3390 saveSettingsDebounced();
3391 setHotswapsDebounced();
3392 });
3393
3394 $('#chat_truncation').on('input', function () {
3395 power_user.chat_truncation = Number($('#chat_truncation').val());
3396 $('#chat_truncation_counter').val(power_user.chat_truncation);
3397 saveSettingsDebounced();
3398 });
3399
3400 $('#streaming_fps').on('input', function () {
3401 power_user.streaming_fps = Number($('#streaming_fps').val());
3402 $('#streaming_fps_counter').val(power_user.streaming_fps);
3403 saveSettingsDebounced();
3404 });
3405
3406 $('#smooth_streaming').on('input', function () {
3407 power_user.smooth_streaming = !!$(this).prop('checked');
3408 saveSettingsDebounced();
3409 });
3410
3411 $('#smooth_streaming_no_think').on('input', function () {
3412 power_user.smooth_streaming_no_think = !!$(this).prop('checked');
3413 saveSettingsDebounced();
3414 });
3415
3416 $('#smooth_streaming_speed').on('input', function () {
3417 power_user.smooth_streaming_speed = Number($('#smooth_streaming_speed').val());
3418 saveSettingsDebounced();
3419 });
3420
3421 $('#stream_fade_in').on('input', function () {
3422 power_user.stream_fade_in = !!$(this).prop('checked');
3423 saveSettingsDebounced();
3424 });
3425
3426 $('input[name="font_scale"]').on('input', async function (e, data) {
3427 const applyMode = data?.forced ? 'forced' : 'normal';
3428 power_user.font_scale = Number($(this).val());
3429 $('#font_scale_counter').val(power_user.font_scale);
3430 applyFontScale(applyMode);
3431 saveSettingsDebounced();
3432 });
3433
3434 $('input[name="blur_strength"]').on('input', async function (e) {
3435 power_user.blur_strength = Number($(this).val());
3436 $('#blur_strength_counter').val(power_user.blur_strength);
3437 applyBlurStrength();
3438 saveSettingsDebounced();
3439 });
3440
3441 $('input[name="shadow_width"]').on('input', async function (e) {
3442 power_user.shadow_width = Number($(this).val());
3443 $('#shadow_width_counter').val(power_user.shadow_width);
3444 applyShadowWidth();
3445 saveSettingsDebounced();
3446 });
3447
3448 $('#main-text-color-picker').on('change', (/** @type {ColorPickerEvent} */ evt) => {
3449 power_user.main_text_color = evt.detail.rgba;
3450 applyThemeColor('main');
3451 saveSettingsDebounced();
3452 });
3453
3454 $('#italics-color-picker').on('change', (/** @type {ColorPickerEvent} */ evt) => {
3455 power_user.italics_text_color = evt.detail.rgba;
3456 applyThemeColor('italics');
3457 saveSettingsDebounced();
3458 });
3459
3460 $('#underline-color-picker').on('change', (/** @type {ColorPickerEvent} */ evt) => {
3461 power_user.underline_text_color = evt.detail.rgba;
3462 applyThemeColor('underline');
3463 saveSettingsDebounced();
3464 });
3465
3466 $('#quote-color-picker').on('change', (/** @type {ColorPickerEvent} */ evt) => {
3467 power_user.quote_text_color = evt.detail.rgba;
3468 applyThemeColor('quote');
3469 saveSettingsDebounced();
3470 });
3471
3472 $('#blur-tint-color-picker').on('change', (/** @type {ColorPickerEvent} */ evt) => {
3473 power_user.blur_tint_color = evt.detail.rgba;
3474 applyThemeColor('blurTint');
3475 saveSettingsDebounced();
3476 });
3477
3478 $('#chat-tint-color-picker').on('change', (/** @type {ColorPickerEvent} */ evt) => {
3479 power_user.chat_tint_color = evt.detail.rgba;
3480 applyThemeColor('chatTint');
3481 saveSettingsDebounced();
3482 });
3483
3484 $('#user-mes-blur-tint-color-picker').on('change', (/** @type {ColorPickerEvent} */ evt) => {
3485 power_user.user_mes_blur_tint_color = evt.detail.rgba;
3486 applyThemeColor('userMesBlurTint');
3487 saveSettingsDebounced();
3488 });
3489
3490 $('#bot-mes-blur-tint-color-picker').on('change', (/** @type {ColorPickerEvent} */ evt) => {
3491 power_user.bot_mes_blur_tint_color = evt.detail.rgba;
3492 applyThemeColor('botMesBlurTint');
3493 saveSettingsDebounced();
3494 });
3495
3496 $('#shadow-color-picker').on('change', (/** @type {ColorPickerEvent} */ evt) => {
3497 power_user.shadow_color = evt.detail.rgba;
3498 applyThemeColor('shadow');
3499 saveSettingsDebounced();
3500 });
3501
3502 $('#border-color-picker').on('change', (/** @type {ColorPickerEvent} */ evt) => {
3503 power_user.border_color = evt.detail.rgba;
3504 applyThemeColor('border');
3505 saveSettingsDebounced();
3506 });
3507
3508 $('#themes').on('change', function () {
3509 const themeSelected = String($(this).find(':selected').val());
3510 power_user.theme = themeSelected;
3511 applyTheme(themeSelected);
3512 saveSettingsDebounced();
3513 });
3514
3515 $('#movingUIPresets').on('change', async function () {
3516 console.log('saw MUI preset change');
3517 const movingUIPresetSelected = String($(this).find(':selected').val());
3518 power_user.movingUIPreset = movingUIPresetSelected;
3519 applyMovingUIPreset(movingUIPresetSelected);
3520 saveSettingsDebounced();
3521 });
3522
3523 $('#ui-preset-save-button').on('click', () => saveTheme());
3524 $('#ui-preset-update-button').on('click', () => updateTheme());
3525 $('#ui-preset-delete-button').on('click', () => deleteTheme());
3526 $('#movingui-preset-save-button').on('click', saveMovingUI);
3527
3528 $('#never_resize_avatars').on('input', function () {
3529 power_user.never_resize_avatars = !!$(this).prop('checked');
3530 saveSettingsDebounced();
3531 });
3532
3533 $('#show_card_avatar_urls').on('input', function () {
3534 power_user.show_card_avatar_urls = !!$(this).prop('checked');
3535 printCharactersDebounced();
3536 saveSettingsDebounced();
3537 });
3538
3539 $('#play_message_sound').on('input', function () {
3540 power_user.play_message_sound = !!$(this).prop('checked');
3541 saveSettingsDebounced();
3542 });
3543
3544 $('#play_sound_unfocused').on('input', function () {
3545 power_user.play_sound_unfocused = !!$(this).prop('checked');
3546 saveSettingsDebounced();
3547 });
3548
3549 $('#auto_save_msg_edits').on('input', function () {
3550 power_user.auto_save_msg_edits = !!$(this).prop('checked');
3551 saveSettingsDebounced();
3552 });
3553
3554 $('#character_sort_order').on('change', function () {
3555 const field = String($(this).find(':selected').data('field'));
3556 // Save sort order, but do not save search sorting, as this is a temporary sorting option
3557 if (field !== 'search') {
3558 power_user.sort_field = field;
3559 power_user.sort_order = $(this).find(':selected').data('order');
3560 power_user.sort_rule = $(this).find(':selected').data('rule');
3561 }
3562 printCharactersDebounced();
3563 saveSettingsDebounced();
3564 });
3565
3566 $('#gestures-checkbox').on('change', function () {
3567 power_user.gestures = !!$('#gestures-checkbox').prop('checked');
3568 saveSettingsDebounced();
3569 });
3570
3571 $('#auto_swipe').on('input', function () {
3572 power_user.auto_swipe = !!$(this).prop('checked');
3573 saveSettingsDebounced();
3574 });
3575
3576 $('#auto_swipe_blacklist').on('input', function () {
3577 power_user.auto_swipe_blacklist = String($(this).val())
3578 .split(',')
3579 .map(str => str.trim())
3580 .filter(str => str);
3581 console.log('power_user.auto_swipe_blacklist', power_user.auto_swipe_blacklist);
3582 saveSettingsDebounced();
3583 });
3584
3585 $('#auto_swipe_minimum_length').on('input', function () {
3586 const number = Number($(this).val());
3587 if (!isNaN(number)) {
3588 power_user.auto_swipe_minimum_length = number;
3589 saveSettingsDebounced();
3590 }
3591 });
3592
3593 $('#auto_swipe_blacklist_threshold').on('input', function () {
3594 const number = Number($(this).val());
3595 if (!isNaN(number)) {
3596 power_user.auto_swipe_blacklist_threshold = number;
3597 saveSettingsDebounced();
3598 }
3599 });
3600
3601 $('#auto_fix_generated_markdown').on('input', function () {
3602 power_user.auto_fix_generated_markdown = !!$(this).prop('checked');
3603 reloadCurrentChat();
3604 saveSettingsDebounced();
3605 });
3606
3607 $('#console_log_prompts').on('input', function () {
3608 power_user.console_log_prompts = !!$(this).prop('checked');
3609 saveSettingsDebounced();
3610 });
3611
3612 $('#request_token_probabilities').on('input', function () {
3613 power_user.request_token_probabilities = !!$(this).prop('checked');
3614 saveSettingsDebounced();
3615 });
3616
3617 $('#show_group_chat_queue').on('input', function () {
3618 power_user.show_group_chat_queue = !!$(this).prop('checked');
3619 saveSettingsDebounced();
3620 });
3621
3622 $('#auto_scroll_chat_to_bottom').on('input', function () {
3623 power_user.auto_scroll_chat_to_bottom = !!$(this).prop('checked');
3624 saveSettingsDebounced();
3625 });
3626
3627 $('#tokenizer').on('change', function () {
3628 const value = $(this).find(':selected').val();
3629 power_user.tokenizer = Number(value);
3630 BIAS_CACHE.clear();
3631 saveSettingsDebounced();
3632
3633 // Trigger character editor re-tokenize
3634 forceCharacterEditorTokenize();
3635 });
3636
3637 $('#send_on_enter').on('change', function () {
3638 const value = $(this).find(':selected').val();
3639 power_user.send_on_enter = Number(value);
3640 saveSettingsDebounced();
3641 });
3642
3643 $('#confirm_message_delete').on('input', function () {
3644 power_user.confirm_message_delete = !!$(this).prop('checked');
3645 saveSettingsDebounced();
3646 });
3647
3648 $('#reload_chat').on('click', async function () {
3649 const currentChatId = getCurrentChatId();
3650 if (currentChatId !== undefined && currentChatId !== null) {
3651 await saveSettings();
3652 await saveChatConditional();
3653 await reloadCurrentChat();
3654 }
3655 });
3656
3657 $('#allow_name1_display').on('input', function () {
3658 power_user.allow_name1_display = !!$(this).prop('checked');
3659 reloadCurrentChat();
3660 saveSettingsDebounced();
3661 });
3662
3663 $('#allow_name2_display').on('input', function () {
3664 power_user.allow_name2_display = !!$(this).prop('checked');
3665 reloadCurrentChat();
3666 saveSettingsDebounced();
3667 });
3668
3669 $('#token_padding').on('input', function () {
3670 power_user.token_padding = Number($(this).val());
3671 saveSettingsDebounced();
3672 });
3673
3674 $('#messageTimerEnabled').on('input', function () {
3675 const value = !!$(this).prop('checked');
3676 power_user.timer_enabled = value;
3677 switchTimer();
3678 saveSettingsDebounced();
3679 });
3680
3681 $('#messageTimestampsEnabled').on('input', function () {
3682 const value = !!$(this).prop('checked');
3683 power_user.timestamps_enabled = value;
3684 switchTimestamps();
3685 saveSettingsDebounced();
3686 });
3687
3688 $('#messageModelIconEnabled').on('input', function () {
3689 const value = !!$(this).prop('checked');
3690 power_user.timestamp_model_icon = value;
3691 switchIcons();
3692 saveSettingsDebounced();
3693 });
3694
3695 $('#messageTokensEnabled').on('input', function () {
3696 const value = !!$(this).prop('checked');
3697 power_user.message_token_count_enabled = value;
3698 switchTokenCount();
3699 saveSettingsDebounced();
3700 });
3701
3702 $('#expandMessageActions').on('input', function () {
3703 const value = !!$(this).prop('checked');
3704 power_user.expand_message_actions = value;
3705 switchMessageActions();
3706 saveSettingsDebounced();
3707 });
3708
3709 $('#enableZenSliders').on('input', function () {
3710 const value = !!$(this).prop('checked');
3711 if (power_user.enableLabMode === true && value === true) {
3712 //disallow zenSliders while Lab Mode is active
3713 toastr.warning('Disable Mad Lab Mode before enabling Zen Sliders');
3714 $(this).prop('checked', false).trigger('input');
3715 return;
3716 }
3717 power_user.enableZenSliders = value;
3718 switchZenSliders();
3719 saveSettingsDebounced();
3720 });
3721
3722 $('#enableLabMode').on('input', function (event, { fromInit = false } = {}) {
3723 const value = !!$(this).prop('checked');
3724 if (power_user.enableZenSliders === true && value === true) {
3725 //disallow Lab Mode if ZenSliders are active
3726 toastr.warning('Disable Zen Sliders before enabling Mad Lab Mode');
3727 $(this).prop('checked', false).trigger('input');
3728 return;
3729 }
3730
3731 power_user.enableLabMode = value;
3732 switchLabMode({ noReset: fromInit });
3733 saveSettingsDebounced();
3734 });
3735
3736 $('#mesIDDisplayEnabled').on('input', function () {
3737 const value = !!$(this).prop('checked');
3738 power_user.mesIDDisplay_enabled = value;
3739 switchMesIDDisplay();
3740 saveSettingsDebounced();
3741 });
3742
3743 $('#hideChatAvatarsEnabled').on('input', function () {
3744 const value = !!$(this).prop('checked');
3745 power_user.hideChatAvatars_enabled = value;
3746 switchHideChatAvatars();
3747 saveSettingsDebounced();
3748 });
3749
3750 $('#hotswapEnabled').on('input', function () {
3751 const value = !!$(this).prop('checked');
3752 power_user.hotswap_enabled = value;
3753 switchHotswap();
3754 saveSettingsDebounced();
3755 });
3756
3757 $('#prefer_character_prompt').on('input', function () {
3758 const value = !!$(this).prop('checked');
3759 power_user.prefer_character_prompt = value;
3760 saveSettingsDebounced();
3761 });
3762
3763 $('#prefer_character_jailbreak').on('input', function () {
3764 const value = !!$(this).prop('checked');
3765 power_user.prefer_character_jailbreak = value;
3766 saveSettingsDebounced();
3767 });
3768
3769 $('#continue_on_send').on('input', function () {
3770 const value = !!$(this).prop('checked');
3771 power_user.continue_on_send = value;
3772 saveSettingsDebounced();
3773 });
3774
3775 $('#quick_continue').on('input', function () {
3776 const value = !!$(this).prop('checked');
3777 power_user.quick_continue = value;
3778 $('#mes_continue').css('display', value ? '' : 'none');
3779 saveSettingsDebounced();
3780 });
3781
3782 $('#quick_impersonate').on('input', function () {
3783 const value = !!$(this).prop('checked');
3784 power_user.quick_impersonate = value;
3785 $('#mes_impersonate').css('display', value ? '' : 'none');
3786 saveSettingsDebounced();
3787 });
3788
3789 $('#trim_spaces').on('input', function () {
3790 const value = !!$(this).prop('checked');
3791 power_user.trim_spaces = value;
3792 saveSettingsDebounced();
3793 });
3794
3795 $('#relaxed_api_urls').on('input', function () {
3796 const value = !!$(this).prop('checked');
3797 power_user.relaxed_api_urls = value;
3798 saveSettingsDebounced();
3799 });
3800
3801 $('#world_import_dialog').on('input', function () {
3802 const value = !!$(this).prop('checked');
3803 power_user.world_import_dialog = value;
3804 saveSettingsDebounced();
3805 });
3806
3807 $('#enable_auto_select_input').on('input', function () {
3808 const value = !!$(this).prop('checked');
3809 power_user.enable_auto_select_input = value;
3810 saveSettingsDebounced();
3811 });
3812
3813 $('#enable_md_hotkeys').on('input', function () {
3814 const value = !!$(this).prop('checked');
3815 power_user.enable_md_hotkeys = value;
3816 toggleMDHotkeyIconDisplay();
3817 saveSettingsDebounced();
3818 });
3819
3820 $('#spoiler_free_mode').on('input', function () {
3821 power_user.spoiler_free_mode = !!$(this).prop('checked');
3822 switchSpoilerMode();
3823 saveSettingsDebounced();
3824 });
3825
3826 $('#spoiler_free_desc_button').on('click', function (e) {
3827 e.stopPropagation();
3828 peekSpoilerMode();
3829 $(this).toggleClass('fa-eye fa-eye-slash');
3830 });
3831
3832 $('#custom_stopping_strings').on('input', function () {
3833 power_user.custom_stopping_strings = String($(this).val()).trim();
3834 saveSettingsDebounced();
3835 });
3836
3837 $('#custom_stopping_strings_macro').on('change', function () {
3838 power_user.custom_stopping_strings_macro = !!$(this).prop('checked');
3839 saveSettingsDebounced();
3840 });
3841
3842 $('#fuzzy_search_checkbox').on('input', function () {
3843 power_user.fuzzy_search = !!$(this).prop('checked');
3844 saveSettingsDebounced();
3845 });
3846
3847 $('#persona_show_notifications').on('input', function () {
3848 power_user.persona_show_notifications = !!$(this).prop('checked');
3849 saveSettingsDebounced();
3850 });
3851
3852 $('#persona_allow_multi_connections').on('input', function () {
3853 power_user.persona_allow_multi_connections = !!$(this).prop('checked');
3854 saveSettingsDebounced();
3855 });
3856
3857 $('#persona_auto_lock').on('input', function () {
3858 power_user.persona_auto_lock = !!$(this).prop('checked');
3859 saveSettingsDebounced();
3860 });
3861
3862 $('#encode_tags').on('input', async function () {
3863 power_user.encode_tags = !!$(this).prop('checked');
3864 await reloadCurrentChat();
3865 saveSettingsDebounced();
3866 });
3867
3868 $('#experimental_macro_engine').on('input', function () {
3869 power_user.experimental_macro_engine = !!$(this).prop('checked');
3870 saveSettingsDebounced();
3871
3872 // Check if the app is ready before showing the toast
3873 if (!settingsReady) {
3874 return;
3875 }
3876
3877 eventSource.once(event_types.SETTINGS_UPDATED, function () {
3878 toastr.warning(
3879 t`Click here to reload.`,
3880 t`Toggling the Experimental Macro Engine requires a reload.`,
3881 {
3882 onclick: () => window.location.reload(),
3883 timeOut: 10000,
3884 preventDuplicates: true,
3885 },
3886 );
3887 });
3888 });
3889
3890 $('#disable_group_trimming').on('input', function () {
3891 power_user.disable_group_trimming = !!$(this).prop('checked');
3892 saveSettingsDebounced();
3893 });
3894
3895 $('#debug_menu').on('click', function () {
3896 showDebugMenu();
3897 });
3898
3899 $('#bogus_folders').on('input', function () {
3900 power_user.bogus_folders = !!$(this).prop('checked');
3901 printCharactersDebounced();
3902 saveSettingsDebounced();
3903 });
3904
3905 $('#zoomed_avatar_magnification').on('input', function () {
3906 power_user.zoomed_avatar_magnification = !!$(this).prop('checked');
3907 printCharactersDebounced();
3908 saveSettingsDebounced();
3909 });
3910
3911 $('#aux_field').on('change', function () {
3912 const value = $(this).find(':selected').val();
3913 power_user.aux_field = String(value);
3914 printCharactersDebounced();
3915 saveSettingsDebounced();
3916 });
3917
3918 $('#tag_import_setting').on('change', function () {
3919 const value = $(this).find(':selected').val();
3920 power_user.tag_import_setting = Number(value);
3921 saveSettingsDebounced();
3922 });
3923
3924 $('#stscript_autocomplete_state').on('input', function () {
3925 power_user.stscript.autocomplete.state = Number($(this).val());
3926 saveSettingsDebounced();
3927 });
3928
3929 $('#stscript_autocomplete_autoHide').on('input', function () {
3930 power_user.stscript.autocomplete.autoHide = !!$(this).prop('checked');
3931 saveSettingsDebounced();
3932 });
3933
3934 $('#stscript_autocomplete_showInAllMacroFields').on('input', function () {
3935 power_user.stscript.autocomplete.showInAllMacroFields = !!$(this).prop('checked');
3936 saveSettingsDebounced();
3937 });
3938
3939 $('#stscript_matching').on('change', function () {
3940 const value = $(this).find(':selected').val();
3941 power_user.stscript.matching = String(value);
3942 saveSettingsDebounced();
3943 });
3944
3945 $('#stscript_autocomplete_style').on('change', function () {
3946 const value = $(this).find(':selected').val();
3947 power_user.stscript.autocomplete.style = String(value);
3948 document.body.setAttribute('data-stscript-style', power_user.stscript.autocomplete.style);
3949 saveSettingsDebounced();
3950 });
3951
3952 $('#stscript_autocomplete_select').on('change', function () {
3953 const value = $(this).find(':selected').val();
3954 power_user.stscript.autocomplete.select = parseInt(String(value));
3955 saveSettingsDebounced();
3956 });
3957
3958 $('#stscript_autocomplete_font_scale').on('input', function () {
3959 const value = $(this).val();
3960 $('#stscript_autocomplete_font_scale_counter').val(value);
3961 power_user.stscript.autocomplete.font.scale = Number(value);
3962 document.body.style.setProperty('--ac-font-scale', value.toString());
3963 window.dispatchEvent(new Event('resize', { bubbles: true }));
3964 saveSettingsDebounced();
3965 });
3966 $('#stscript_autocomplete_font_scale_counter').on('input', function () {
3967 const value = $(this).val();
3968 $('#stscript_autocomplete_font_scale').val(value);
3969 power_user.stscript.autocomplete.font.scale = Number(value);
3970 document.body.style.setProperty('--ac-font-scale', value.toString());
3971 window.dispatchEvent(new Event('resize', { bubbles: true }));
3972 saveSettingsDebounced();
3973 });
3974
3975 $('#stscript_autocomplete_width_left').on('input', function () {
3976 const value = $(this).val();
3977 power_user.stscript.autocomplete.width.left = Number(value);
3978 /**@type {HTMLElement}*/(this.closest('.doubleRangeInputContainer')).style.setProperty('--value', value.toString());
3979 window.dispatchEvent(new Event('resize', { bubbles: true }));
3980 saveSettingsDebounced();
3981 });
3982
3983 $('#stscript_autocomplete_width_right').on('input', function () {
3984 const value = $(this).val();
3985 power_user.stscript.autocomplete.width.right = Number(value);
3986 /**@type {HTMLElement}*/(this.closest('.doubleRangeInputContainer')).style.setProperty('--value', value.toString());
3987 window.dispatchEvent(new Event('resize', { bubbles: true }));
3988 saveSettingsDebounced();
3989 });
3990
3991 $('#stscript_parser_flag_strict_escaping').on('click', function () {
3992 const value = $(this).prop('checked');
3993 power_user.stscript.parser.flags[PARSER_FLAG.STRICT_ESCAPING] = value;
3994 saveSettingsDebounced();
3995 });
3996
3997 $('#stscript_parser_flag_replace_getvar').on('click', function () {
3998 const value = $(this).prop('checked');
3999 power_user.stscript.parser.flags[PARSER_FLAG.REPLACE_GETVAR] = value;
4000 saveSettingsDebounced();
4001 });
4002
4003 $('#restore_user_input').on('input', function () {
4004 power_user.restore_user_input = !!$(this).prop('checked');
4005 saveSettingsDebounced();
4006 });
4007
4008 $('#reduced_motion').on('input', function () {
4009 power_user.reduced_motion = !!$(this).prop('checked');
4010 switchReducedMotion();
4011 saveSettingsDebounced();
4012 });
4013
4014 $('#compact_input_area').on('input', function () {
4015 power_user.compact_input_area = !!$(this).prop('checked');
4016 switchCompactInputArea();
4017 saveSettingsDebounced();
4018 });
4019
4020 $('#show_swipe_num_all_messages').on('input', function () {
4021 power_user.show_swipe_num_all_messages = !!$(this).prop('checked');
4022 switchSwipeNumAllMessages();
4023 saveSettingsDebounced();
4024 });
4025
4026 $('#auto-connect-checkbox').on('input', function () {
4027 power_user.auto_connect = !!$(this).prop('checked');
4028 saveSettingsDebounced();
4029 });
4030
4031 $('#auto-load-chat-checkbox').on('input', function () {
4032 power_user.auto_load_chat = !!$(this).prop('checked');
4033 saveSettingsDebounced();
4034 });
4035
4036 $('#forbid_external_media').on('input', function () {
4037 power_user.forbid_external_media = !!$(this).prop('checked');
4038 saveSettingsDebounced();
4039 reloadCurrentChat();
4040 });
4041
4042 $('#pin_styles').on('input', function () {
4043 power_user.pin_styles = !!$(this).prop('checked');
4044 saveSettingsDebounced();
4045 applyStylePins();
4046 });
4047
4048 $('#click_to_edit').on('input', function () {
4049 power_user.click_to_edit = !!$(this).prop('checked');
4050 saveSettingsDebounced();
4051 });
4052
4053 $('#ui_preset_import_button').on('click', function () {
4054 $('#ui_preset_import_file').trigger('click');
4055 });
4056
4057 $('#ui_preset_import_file').on('change', async function () {
4058 const inputElement = this instanceof HTMLInputElement && this;
4059
4060 try {
4061 const file = inputElement?.files?.[0];
4062 await importTheme(file);
4063 } catch (error) {
4064 console.error('Error importing UI theme', error);
4065 toastr.error(String(error), 'Failed to import UI theme');
4066 } finally {
4067 if (inputElement) {
4068 inputElement.value = null;
4069 }
4070 }
4071 });
4072
4073 $('#ui_preset_export_button').on('click', async function () {
4074 await exportTheme();
4075 });
4076
4077 $('#media_display').on('input', async function () {
4078 power_user.media_display = $(this).val().toString();
4079 saveSettingsDebounced();
4080 if (isMediaDisplayReloadNeeded()) {
4081 await reloadCurrentChat();
4082 }
4083 });
4084
4085 $('#image_overswipe').on('input', function () {
4086 power_user.image_overswipe = $(this).val().toString();
4087 saveSettingsDebounced();
4088 });
4089
4090 $(document).on('click', '#debug_table [data-debug-function]', function () {
4091 const functionId = $(this).data('debug-function');
4092 const functionRecord = debug_functions.find(f => f.functionId === functionId);
4093
4094 if (functionRecord) {
4095 functionRecord.func();
4096 } else {
4097 console.warn(`Debug function ${functionId} not found`);
4098 }
4099 });
4100
4101 $(window).on('focus', function () {
4102 browser_has_focus = true;
4103 });
4104
4105 $(window).on('blur', function () {
4106 browser_has_focus = false;
4107 });
4108
4109 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
4110 name: 'vn',
4111 callback: toggleWaifu,
4112 helpString: 'Swaps Visual Novel Mode On/Off',
4113 }));
4114 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
4115 name: 'newchat',
4116 /** @type {(args: { delete: string?}, string) => Promise<''>} */
4117 callback: async (args, _) => {
4118 await doNewChat({ deleteCurrentChat: isTrueBoolean(args.delete) });
4119 return '';
4120 },
4121 namedArgumentList: [
4122 SlashCommandNamedArgument.fromProps({
4123 name: 'delete',
4124 description: 'delete the current chat',
4125 typeList: [ARGUMENT_TYPE.BOOLEAN],
4126 defaultValue: 'false',
4127 enumList: commonEnumProviders.boolean('trueFalse')(),
4128 }),
4129 ],
4130 helpString: 'Start a new chat with the current character',
4131 }));
4132 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
4133 name: 'random',
4134 callback: doRandomChat,
4135 unnamedArgumentList: [
4136 SlashCommandArgument.fromProps({
4137 description: 'optional tag name',
4138 typeList: [ARGUMENT_TYPE.STRING],
4139 enumProvider: () => tags.filter(tag => Object.values(tag_map).some(x => x.includes(tag.id))).map(tag => new SlashCommandEnumValue(tag.name, null, enumTypes.enum, enumIcons.tag)),
4140 }),
4141 ],
4142 helpString: 'Start a new chat with a random character. If an argument is provided, only considers characters that have the specified tag.',
4143 }));
4144 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
4145 name: 'del',
4146 callback: doDelMode,
4147 aliases: ['delete', 'delmode'],
4148 unnamedArgumentList: [
4149 new SlashCommandArgument(
4150 'optional number', [ARGUMENT_TYPE.NUMBER], false,
4151 ),
4152 ],
4153 helpString: 'Enter message deletion mode, and auto-deletes last N messages if numeric argument is provided.',
4154 returns: 'The text of the deleted messages.',
4155 }));
4156 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
4157 name: 'cut',
4158 callback: doMesCut,
4159 returns: 'the text of cut messages separated by a newline',
4160 unnamedArgumentList: [
4161 SlashCommandArgument.fromProps({
4162 description: 'number or range',
4163 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE],
4164 isRequired: true,
4165 acceptsMultiple: true,
4166 enumProvider: commonEnumProviders.messages(),
4167 }),
4168 ],
4169 helpString: `
4170 <div>
4171 Cuts the specified message or continuous chunk from the chat.
4172 </div>
4173 <div>
4174 Ranges are inclusive!
4175 </div>
4176 <div>
4177 <strong>Example:</strong>
4178 <ul>
4179 <li>
4180 <pre><code>/cut 0-10</code></pre>
4181 </li>
4182 </ul>
4183 </div>
4184 `,
4185 aliases: [],
4186 }));
4187 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
4188 name: 'resetpanels',
4189 callback: doResetPanels,
4190 helpString: 'resets UI panels to original state',
4191 aliases: ['resetui'],
4192 }));
4193 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
4194 name: 'bgcol',
4195 callback: setAvgBG,
4196 namedArgumentList: [
4197 SlashCommandNamedArgument.fromProps({
4198 name: 'force',
4199 description: 'force generation even if a theme with the same name already exists',
4200 typeList: [ARGUMENT_TYPE.BOOLEAN],
4201 defaultValue: 'false',
4202 enumList: commonEnumProviders.boolean('trueFalse')(),
4203 }),
4204 SlashCommandNamedArgument.fromProps({
4205 name: 'name',
4206 description: 'override the generated theme name',
4207 typeList: [ARGUMENT_TYPE.STRING],
4208 }),
4209 SlashCommandNamedArgument.fromProps({
4210 name: 'bg',
4211 description: 'background image filename to use instead of the current one',
4212 typeList: [ARGUMENT_TYPE.STRING],
4213 enumProvider: commonEnumProviders.backgrounds,
4214 }),
4215 ],
4216 helpString: 'Generates a new theme based on a dominant color of the specified background image. Saves as "bgcol - background name".',
4217 }));
4218 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
4219 name: 'theme',
4220 callback: setThemeCallback,
4221 unnamedArgumentList: [
4222 SlashCommandArgument.fromProps({
4223 description: 'theme name',
4224 typeList: [ARGUMENT_TYPE.STRING],
4225 enumProvider: () => themes.map(theme => new SlashCommandEnumValue(theme.name)),
4226 }),
4227 ],
4228 helpString: `
4229 <div>
4230 Sets a UI theme by name.
4231 </div>
4232 <div>
4233 If no theme name is is provided, this will return the currently active theme.
4234 </div>
4235 <div>
4236 <strong>Example:</strong>
4237 <ul>
4238 <li>
4239 <pre><code>/theme Cappuccino</code></pre>
4240 </li>
4241 <li>
4242 <pre><code>/theme</code></pre>
4243 </li>
4244 </ul>
4245 </div>
4246 `,
4247 }));
4248 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
4249 name: 'css-var',
4250 /** @param {{to: string, varname: string }} args @param {string} value @returns {string} */
4251 callback: (args, value) => {
4252 // Map enum to target selector
4253 const targetSelector = {
4254 chat: '#chat',
4255 background: '#bg1',
4256 gallery: '#gallery',
4257 zoomedAvatar: 'div.zoomed_avatar',
4258 }[args.to || 'chat'];
4259
4260 if (!targetSelector) {
4261 toastr.error(`Invalid target: ${args.to}`);
4262 return;
4263 }
4264
4265 if (!args.varname) {
4266 toastr.error('CSS variable name is required');
4267 return;
4268 }
4269 if (!args.varname.startsWith('--')) {
4270 toastr.error('CSS variable names must start with "--"');
4271 return;
4272 }
4273
4274 const elements = document.querySelectorAll(targetSelector);
4275 if (elements.length === 0) {
4276 toastr.error(`No elements found for ${args.to ?? 'chat'} with selector "${targetSelector}"`);
4277 return;
4278 }
4279
4280 elements.forEach(element => {
4281 element.style.setProperty(args.varname, value);
4282 });
4283
4284 console.info(`Set CSS variable "${args.varname}" to "${value}" on "${targetSelector}"`);
4285 },
4286 namedArgumentList: [
4287 SlashCommandNamedArgument.fromProps({
4288 name: 'varname',
4289 description: 'CSS variable name (starting with double dashes)',
4290 typeList: [ARGUMENT_TYPE.STRING],
4291 isRequired: true,
4292 }),
4293 SlashCommandNamedArgument.fromProps({
4294 name: 'to',
4295 description: 'The target element to which the CSS variable will be applied',
4296 typeList: [ARGUMENT_TYPE.STRING],
4297 enumList: [
4298 new SlashCommandEnumValue('chat', null, enumTypes.enum, enumIcons.message),
4299 new SlashCommandEnumValue('background', null, enumTypes.enum, enumIcons.image),
4300 new SlashCommandEnumValue('zoomedAvatar', null, enumTypes.enum, enumIcons.character),
4301 new SlashCommandEnumValue('gallery', null, enumTypes.enum, enumIcons.image),
4302 ],
4303 defaultValue: 'chat',
4304 }),
4305 ],
4306 unnamedArgumentList: [
4307 SlashCommandArgument.fromProps({
4308 description: 'CSS variable value',
4309 typeList: [ARGUMENT_TYPE.STRING],
4310 isRequired: true,
4311 }),
4312 ],
4313 helpString: `
4314 <div>
4315 Sets a CSS variable to a specified value on a target element.
4316 <br />
4317 Only setting of variable names is supported. They have to be prefixed with double dashes ("--exampleVar").
4318 Setting actual CSS properties is not supported. Custom CSS in the theme settings can be used for that.
4319 <br /><br />
4320 <b>This value will be gone after a page reload!</b>
4321 </div>
4322 <div>
4323 <strong>Example:</strong>
4324 <ul>
4325 <li>
4326 <pre><code>/css-var varname="--SmartThemeBodyColor" #ff0000</code></pre>
4327 Sets the text color of the chat to red
4328 </li>
4329 <li>
4330 <pre><code>/css-var to=zoomedAvatar varname="--SmartThemeBlurStrength" 0</code></pre>
4331 Remove the blur from the zoomed avatar
4332 </li>
4333 </ul>
4334 </div>
4335 `,
4336 }));
4337 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
4338 name: 'movingui',
4339 callback: setmovingUIPreset,
4340 unnamedArgumentList: [
4341 SlashCommandArgument.fromProps({
4342 description: 'name',
4343 typeList: [ARGUMENT_TYPE.STRING],
4344 isRequired: true,
4345 enumProvider: () => movingUIPresets.map(preset => new SlashCommandEnumValue(preset.name)),
4346 }),
4347 ],
4348 helpString: 'activates a movingUI preset by name',
4349 }));
4350 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
4351 name: 'stop-strings',
4352 aliases: ['stopping-strings', 'custom-stopping-strings', 'custom-stop-strings'],
4353 helpString: `
4354 <div>
4355 Sets a list of custom stopping strings. Gets the list if no value is provided.
4356 Use a "force" argument to force set an empty value.
4357 </div>
4358 <div>
4359 <strong>Examples:</strong>
4360 </div>
4361 <ul>
4362 <li>Force set an empty value: <pre><code class="language-stscript">/stop-strings force="true" {{noop}}</code></pre></li>
4363 <li>Value must be a JSON-serialized array: <pre><code class="language-stscript">/stop-strings ["goodbye", "farewell"]</code></pre></li>
4364 <li>Pipe characters must be escaped with a backslash: <pre><code class="language-stscript">/stop-strings ["left\\|right"]</code></pre></li>
4365 </ul>
4366 `,
4367 returns: ARGUMENT_TYPE.LIST,
4368 namedArgumentList: [
4369 SlashCommandNamedArgument.fromProps({
4370 name: 'force',
4371 description: 'force set a value if empty',
4372 typeList: [ARGUMENT_TYPE.BOOLEAN],
4373 defaultValue: 'false',
4374 enumList: commonEnumProviders.boolean('trueFalse')(),
4375 }),
4376 ],
4377 unnamedArgumentList: [
4378 SlashCommandArgument.fromProps({
4379 description: 'list of strings',
4380 typeList: [ARGUMENT_TYPE.LIST],
4381 acceptsMultiple: false,
4382 isRequired: false,
4383 }),
4384 ],
4385 callback: (args, value) => {
4386 const force = isTrueBoolean(String(args?.force ?? false));
4387 value = String(value ?? '').trim();
4388
4389 // Skip processing if no value and not forced
4390 if (!force && !value) {
4391 return power_user.custom_stopping_strings;
4392 }
4393
4394 // Use empty array for forced empty value
4395 if (force && !value) {
4396 value = JSON.stringify([]);
4397 }
4398
4399 const parsedValue = ((x) => { try { return JSON.parse(x.toString()); } catch { return null; } })(value);
4400 if (!parsedValue || !Array.isArray(parsedValue)) {
4401 throw new Error('Invalid list format. The value must be a JSON-serialized array of strings.');
4402 }
4403 parsedValue.forEach((item, index) => {
4404 parsedValue[index] = String(item);
4405 });
4406 power_user.custom_stopping_strings = JSON.stringify(parsedValue);
4407 $('#custom_stopping_strings').val(power_user.custom_stopping_strings);
4408 saveSettingsDebounced();
4409
4410 return power_user.custom_stopping_strings;
4411 },
4412 }));
4413 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
4414 name: 'start-reply-with',
4415 helpString: `
4416 <div>
4417 Sets a "Start Reply With". Gets the current value if no value is provided.
4418 Use a "force" argument to force set an empty value.
4419 </div>
4420 <div>
4421 <strong>Examples:</strong>
4422 </div>
4423 <ul>
4424 <li>Set the field value: <pre><code class="language-stscript">/start-reply-with Sure!</code></pre></li>
4425 <li>Force set an empty value: <pre><code class="language-stscript">/start-reply-with force="true" {{noop}}</code></pre></li>
4426 </ul>
4427 `,
4428 namedArgumentList: [
4429 SlashCommandNamedArgument.fromProps({
4430 name: 'force',
4431 description: 'force set a value if empty',
4432 typeList: [ARGUMENT_TYPE.BOOLEAN],
4433 defaultValue: 'false',
4434 enumList: commonEnumProviders.boolean('trueFalse')(),
4435 }),
4436 ],
4437 unnamedArgumentList: [
4438 SlashCommandArgument.fromProps({
4439 description: 'value',
4440 typeList: [ARGUMENT_TYPE.STRING],
4441 acceptsMultiple: false,
4442 isRequired: false,
4443 }),
4444 ],
4445 callback: (args, value) => {
4446 const force = isTrueBoolean(String(args?.force ?? false));
4447
4448 // Skip processing if no value and not forced
4449 if (!force && !value) {
4450 return power_user.user_prompt_bias;
4451 }
4452
4453 power_user.user_prompt_bias = String(value ?? '');
4454 $('#start_reply_with').val(power_user.user_prompt_bias);
4455 saveSettingsDebounced();
4456
4457 return power_user.user_prompt_bias;
4458 },
4459 }));
4460});