Merge branch 'staging' into patch-2

392c98455ec3feeb78ef4ec48ef0243de77ebecb

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

14 files changed, +595 -256Ignore whitespace
public/index.html+37 -35
@@ -3248,25 +3248,26 @@
3248 </div>3248 </div>
3249 </div>3249 </div>
3250 <div id="InstructSettingsColumn" class="flex-container flexNoGap flexFlowColumn flex1">3250 <div id="InstructSettingsColumn" class="flex-container flexNoGap flexFlowColumn flex1">
3251 <h4 class="standoutHeader title_restorable justifySpaceBetween">
3252 <div class="flex-container">
3253 <span data-i18n="Instruct Template">Instruct Template</span>
3254 <a href="https://docs.sillytavern.app/usage/core-concepts/instructmode/" class="notes-link" target="_blank">
3255 <span class="fa-solid fa-circle-question note-link-span"></span>
3256 </a>
3257 </div>
3258 <div class="flex-container">
3259 <label for="instruct_bind_to_context" class="checkbox_label flex1" title="Bind to Context&#10If enabled, Context templates will be automatically selected based on selected Instruct template name or by preference." data-i18n="[title]instruct_bind_to_context">
3260 <input id="instruct_bind_to_context" type="checkbox" style="display:none;" />
3261 <small><i class="fa-solid fa-link menu_button margin0"></i></small>
3262 </label>
3263 <label id="instruct_enabled_label"for="instruct_enabled" class="checkbox_label flex1" title="Enable Instruct Mode" data-i18n="[title]instruct_enabled">
3264 <input id="instruct_enabled" type="checkbox" style="display:none;" />
3265 <small><i class="fa-solid fa-power-off menu_button margin0"></i></small>
3266 </label>
3267 </div>
3268 </h4>
3251 <div id="instructSettingsBlock">3269 <div id="instructSettingsBlock">
3252 <h4 class="standoutHeader title_restorable justifySpaceBetween">3270
3253 <div class="flex-container">
3254 <span data-i18n="Instruct Template">Instruct Template</span>
3255 <a href="https://docs.sillytavern.app/usage/core-concepts/instructmode/" class="notes-link" target="_blank">
3256 <span class="fa-solid fa-circle-question note-link-span"></span>
3257 </a>
3258 </div>
3259 <div class="flex-container">
3260 <label for="instruct_bind_to_context" class="checkbox_label flex1" title="Bind to Context&#10If enabled, Context templates will be automatically selected based on selected Instruct template name or by preference." data-i18n="[title]instruct_bind_to_context">
3261 <input id="instruct_bind_to_context" type="checkbox" style="display:none;" />
3262 <small><i class="fa-solid fa-link menu_button margin0"></i></small>
3263 </label>
3264 <label for="instruct_enabled" class="checkbox_label flex1" title="Enable Instruct Mode" data-i18n="[title]instruct_enabled">
3265 <input id="instruct_enabled" type="checkbox" style="display:none;" />
3266 <small><i class="fa-solid fa-power-off menu_button margin0"></i></small>
3267 </label>
3268 </div>
3269 </h4>
32703271
3271 <div class="flex-container" title="Select your current Instruct Template" data-i18n="[title]Select your current Instruct Template">3272 <div class="flex-container" title="Select your current Instruct Template" data-i18n="[title]Select your current Instruct Template">
3272 <select id="instruct_presets" data-preset-manager-for="instruct" class="flex1 text_pole"></select>3273 <select id="instruct_presets" data-preset-manager-for="instruct" class="flex1 text_pole"></select>
@@ -3431,21 +3432,22 @@
3431 </div>3432 </div>
3432 </div>3433 </div>
3433 <div id="SystemPromptColumn" class="flex-container flexNoGap flexFlowColumn flex1">3434 <div id="SystemPromptColumn" class="flex-container flexNoGap flexFlowColumn flex1">
3435 <h4 class="standoutHeader title_restorable justifySpaceBetween">
3436 <div class="flex-container">
3437 <span data-i18n="System Prompt">System Prompt</span>
3438 <a href="https://docs.sillytavern.app/usage/core-concepts/advancedformatting/#system-prompt" class="notes-link" target="_blank">
3439 <span class="fa-solid fa-circle-question note-link-span"></span>
3440 </a>
3441 </div>
3442 <div class="flex-container">
3443 <label id="sysprompt_enabled_label" for="sysprompt_enabled" class="checkbox_label flex1" title="Enable System Prompt" data-i18n="[title]sysprompt_enabled">
3444 <input id="sysprompt_enabled" type="checkbox" style="display:none;" />
3445 <small><i class="fa-solid fa-power-off menu_button margin0"></i></small>
3446 </label>
3447 </div>
3448 </h4>
3434 <div id="SystemPromptBlock">3449 <div id="SystemPromptBlock">
3435 <h4 class="standoutHeader title_restorable justifySpaceBetween">3450
3436 <div class="flex-container">
3437 <span data-i18n="System Prompt">System Prompt</span>
3438 <a href="https://docs.sillytavern.app/usage/core-concepts/advancedformatting/#system-prompt" class="notes-link" target="_blank">
3439 <span class="fa-solid fa-circle-question note-link-span"></span>
3440 </a>
3441 </div>
3442 <div class="flex-container">
3443 <label for="sysprompt_enabled" class="checkbox_label flex1" title="Enable System Prompt" data-i18n="[title]sysprompt_enabled">
3444 <input id="sysprompt_enabled" type="checkbox" style="display:none;" />
3445 <small><i class="fa-solid fa-power-off menu_button margin0"></i></small>
3446 </label>
3447 </div>
3448 </h4>
34493451
3450 <div class="flex-container" title="Select your current System Prompt" data-i18n="[title]Select your current System Prompt">3452 <div class="flex-container" title="Select your current System Prompt" data-i18n="[title]Select your current System Prompt">
3451 <select id="sysprompt_select" data-preset-manager-for="sysprompt" class="flex1 text_pole"></select>3453 <select id="sysprompt_select" data-preset-manager-for="sysprompt" class="flex1 text_pole"></select>
@@ -5939,9 +5941,9 @@
5939 </div>5941 </div>
5940 <div class="mes_bias"></div>5942 <div class="mes_bias"></div>
5941 </div>5943 </div>
5942 <div class="swipe_right fa-solid fa-chevron-right" style="display: none;">5944 <div class="swipe_right fa-solid fa-chevron-right" style="display: none;">
5943 <div class="swipes-counter"></div>5945 <div class="swipes-counter"></div>
5944 </div>5946 </div>
5945 </div>5947 </div>
5946 </div>5948 </div>
5947 <div id="group_avatars_template" class="template_element">5949 <div id="group_avatars_template" class="template_element">
public/script.js+14 -2
@@ -230,7 +230,7 @@ import { MacrosParser, evaluateMacros, getLastMessageId } from './scripts/macros
230import { currentUser, setUserControls } from './scripts/user.js';230import { currentUser, setUserControls } from './scripts/user.js';
231import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';231import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';
232import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';232import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';
233import { ScraperManager } from './scripts/scrapers.js';233import { initScrapers, ScraperManager } from './scripts/scrapers.js';
234import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js';234import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js';
235import { SlashCommand } from './scripts/slash-commands/SlashCommand.js';235import { SlashCommand } from './scripts/slash-commands/SlashCommand.js';
236import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './scripts/slash-commands/SlashCommandArgument.js';236import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './scripts/slash-commands/SlashCommandArgument.js';
@@ -959,6 +959,7 @@ async function firstLoadInit() {
959 initCfg();959 initCfg();
960 initLogprobs();960 initLogprobs();
961 initInputMarkdown();961 initInputMarkdown();
962 await initScrapers();
962 doDailyExtensionUpdatesCheck();963 doDailyExtensionUpdatesCheck();
963 await hideLoader();964 await hideLoader();
964 await fixViewport();965 await fixViewport();
@@ -5427,6 +5428,7 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
5427 getMessage = getMessage.substring(0, getMessage.indexOf('<|endoftext|>'));5428 getMessage = getMessage.substring(0, getMessage.indexOf('<|endoftext|>'));
5428 }5429 }
5429 const isInstruct = power_user.instruct.enabled && main_api !== 'openai';5430 const isInstruct = power_user.instruct.enabled && main_api !== 'openai';
5431 const isNotEmpty = (str) => str && str.trim() !== '';
5430 if (isInstruct && power_user.instruct.stop_sequence) {5432 if (isInstruct && power_user.instruct.stop_sequence) {
5431 if (getMessage.indexOf(power_user.instruct.stop_sequence) != -1) {5433 if (getMessage.indexOf(power_user.instruct.stop_sequence) != -1) {
5432 getMessage = getMessage.substring(0, getMessage.indexOf(power_user.instruct.stop_sequence));5434 getMessage = getMessage.substring(0, getMessage.indexOf(power_user.instruct.stop_sequence));
@@ -5434,7 +5436,7 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
5434 }5436 }
5435 // Hana: Only use the first sequence (should be <|model|>)5437 // Hana: Only use the first sequence (should be <|model|>)
5436 // of the prompt before <|user|> (as KoboldAI Lite does it).5438 // of the prompt before <|user|> (as KoboldAI Lite does it).
5437 if (isInstruct && power_user.instruct.input_sequence) {5439 if (isInstruct && isNotEmpty(power_user.instruct.input_sequence)) {
5438 if (getMessage.indexOf(power_user.instruct.input_sequence) != -1) {5440 if (getMessage.indexOf(power_user.instruct.input_sequence) != -1) {
5439 getMessage = getMessage.substring(0, getMessage.indexOf(power_user.instruct.input_sequence));5441 getMessage = getMessage.substring(0, getMessage.indexOf(power_user.instruct.input_sequence));
5440 }5442 }
@@ -8476,12 +8478,22 @@ const CONNECT_API_MAP = {
8476 selected: 'novel',8478 selected: 'novel',
8477 button: '#api_button_novel',8479 button: '#api_button_novel',
8478 },8480 },
8481 'koboldcpp': {
8482 selected: 'textgenerationwebui',
8483 button: '#api_button_textgenerationwebui',
8484 type: textgen_types.KOBOLDCPP,
8485 },
8479 // KoboldCpp alias8486 // KoboldCpp alias
8480 'kcpp': {8487 'kcpp': {
8481 selected: 'textgenerationwebui',8488 selected: 'textgenerationwebui',
8482 button: '#api_button_textgenerationwebui',8489 button: '#api_button_textgenerationwebui',
8483 type: textgen_types.KOBOLDCPP,8490 type: textgen_types.KOBOLDCPP,
8484 },8491 },
8492 'openai': {
8493 selected: 'openai',
8494 button: '#api_button_openai',
8495 source: chat_completion_sources.OPENAI,
8496 },
8485 // OpenAI alias8497 // OpenAI alias
8486 'oai': {8498 'oai': {
8487 selected: 'openai',8499 selected: 'openai',
public/scripts/RossAscends-mods.js+17 -16
@@ -56,22 +56,25 @@ let counterNonce = Date.now();
5656
57const observerConfig = { childList: true, subtree: true };57const observerConfig = { childList: true, subtree: true };
58const countTokensDebounced = debounce(RA_CountCharTokens, debounce_timeout.relaxed);58const countTokensDebounced = debounce(RA_CountCharTokens, debounce_timeout.relaxed);
59const countTokensShortDebounced = debounce(RA_CountCharTokens, debounce_timeout.short);
60const checkStatusDebounced = debounce(RA_checkOnlineStatus, debounce_timeout.short);
5961
60const observer = new MutationObserver(function (mutations) {62const observer = new MutationObserver(function (mutations) {
61 mutations.forEach(function (mutation) {63 mutations.forEach(function (mutation) {
64 if (!(mutation.target instanceof HTMLElement)) {
65 return;
66 }
62 if (mutation.target.classList.contains('online_status_text')) {67 if (mutation.target.classList.contains('online_status_text')) {
63 RA_checkOnlineStatus();68 checkStatusDebounced();
64 } else if (mutation.target.parentNode === SelectedCharacterTab) {69 } else if (mutation.target.parentNode === SelectedCharacterTab) {
65 setTimeout(RA_CountCharTokens, 200);70 countTokensShortDebounced();
66 } else if (mutation.target.classList.contains('mes_text')) {71 } else if (mutation.target.classList.contains('mes_text')) {
67 if (mutation.target instanceof HTMLElement) {72 for (const element of mutation.target.getElementsByTagName('math')) {
68 for (const element of mutation.target.getElementsByTagName('math')) {73 element.childNodes.forEach(function (child) {
69 element.childNodes.forEach(function (child) {74 if (child.nodeType === Node.TEXT_NODE) {
70 if (child.nodeType === Node.TEXT_NODE) {75 child.textContent = '';
71 child.textContent = '';76 }
72 }77 });
73 });
74 }
75 }78 }
76 }79 }
77 });80 });
@@ -159,8 +162,8 @@ export function shouldSendOnEnter() {
159export function humanizedDateTime() {162export function humanizedDateTime() {
160 const now = new Date(Date.now());163 const now = new Date(Date.now());
161 const dt = {164 const dt = {
162 year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate(),165 year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate(),
163 hour: now.getHours(), minute: now.getMinutes(), second: now.getSeconds(),166 hour: now.getHours(), minute: now.getMinutes(), second: now.getSeconds(),
164 };167 };
165 for (const key in dt) {168 for (const key in dt) {
166 dt[key] = dt[key].toString().padStart(2, '0');169 dt[key] = dt[key].toString().padStart(2, '0');
@@ -725,9 +728,7 @@ export function addSafariPatch() {
725728
726export function initRossMods() {729export function initRossMods() {
727 // initial status check730 // initial status check
728 setTimeout(() => {731 checkStatusDebounced();
729 RA_checkOnlineStatus();
730 }, 100);
731732
732 if (power_user.auto_load_chat) {733 if (power_user.auto_load_chat) {
733 RA_autoloadchat();734 RA_autoloadchat();
@@ -752,7 +753,7 @@ export function initRossMods() {
752 setTimeout(() => RA_autoconnect(PrevAPI), 100);753 setTimeout(() => RA_autoconnect(PrevAPI), 100);
753 });754 });
754755
755 $('#api_button').click(function () { setTimeout(RA_checkOnlineStatus, 100); });756 $('#api_button').on('click', () => checkStatusDebounced());
756757
757 //toggle pin class when lock toggle clicked758 //toggle pin class when lock toggle clicked
758 $(RPanelPin).on('click', function () {759 $(RPanelPin).on('click', function () {
public/scripts/extensions/expressions/index.js+9 -3
@@ -2,7 +2,7 @@ import { callPopup, eventSource, event_types, generateRaw, getRequestHeaders, ma
2import { dragElement, isMobile } from '../../RossAscends-mods.js';2import { dragElement, isMobile } from '../../RossAscends-mods.js';
3import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';3import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
4import { loadMovingUIState, power_user } from '../../power-user.js';4import { loadMovingUIState, power_user } from '../../power-user.js';
5import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition } from '../../utils.js';5import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition, findChar } from '../../utils.js';
6import { hideMutedSprites } from '../../group-chats.js';6import { hideMutedSprites } from '../../group-chats.js';
7import { isJsonSchemaSupported } from '../../textgen-settings.js';7import { isJsonSchemaSupported } from '../../textgen-settings.js';
8import { debounce_timeout } from '../../constants.js';8import { debounce_timeout } from '../../constants.js';
@@ -2105,14 +2105,20 @@ function migrateSettings() {
2105 }));2105 }));
2106 SlashCommandParser.addCommandObject(SlashCommand.fromProps({2106 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2107 name: 'lastsprite',2107 name: 'lastsprite',
2108 callback: (_, value) => lastExpression[String(value).trim()] ?? '',2108 callback: (_, name) => {
2109 if (typeof name !== 'string') throw new Error('name must be a string');
2110 const char = findChar({ name: name });
2111 const sprite = lastExpression[char?.name ?? name] ?? '';
2112 return sprite;
2113 },
2109 returns: 'the last set sprite / expression for the named character.',2114 returns: 'the last set sprite / expression for the named character.',
2110 unnamedArgumentList: [2115 unnamedArgumentList: [
2111 SlashCommandArgument.fromProps({2116 SlashCommandArgument.fromProps({
2112 description: 'character name',2117 description: 'Character name - or unique character identifier (avatar key)',
2113 typeList: [ARGUMENT_TYPE.STRING],2118 typeList: [ARGUMENT_TYPE.STRING],
2114 isRequired: true,2119 isRequired: true,
2115 enumProvider: commonEnumProviders.characters('character'),2120 enumProvider: commonEnumProviders.characters('character'),
2121 forceEnum: true,
2116 }),2122 }),
2117 ],2123 ],
2118 helpString: 'Returns the last set sprite / expression for the named character.',2124 helpString: 'Returns the last set sprite / expression for the named character.',
public/scripts/extensions/gallery/index.js+1 -0
@@ -441,6 +441,7 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
441 description: 'character name',441 description: 'character name',
442 typeList: [ARGUMENT_TYPE.STRING],442 typeList: [ARGUMENT_TYPE.STRING],
443 enumProvider: commonEnumProviders.characters('character'),443 enumProvider: commonEnumProviders.characters('character'),
444 forceEnum: true,
444 }),445 }),
445 SlashCommandNamedArgument.fromProps({446 SlashCommandNamedArgument.fromProps({
446 name: 'group',447 name: 'group',
public/scripts/scrapers.js+16 -7
@@ -13,6 +13,7 @@ import { isValidUrl } from './utils.js';
13 * @property {string} description13 * @property {string} description
14 * @property {string} iconClass14 * @property {string} iconClass
15 * @property {boolean} iconAvailable15 * @property {boolean} iconAvailable
16 * @property {() => Promise<void>} [init=null]
16 * @property {() => Promise<boolean>} isAvailable17 * @property {() => Promise<boolean>} isAvailable
17 * @property {() => Promise<File[]>} scrape18 * @property {() => Promise<File[]>} scrape
18 */19 */
@@ -36,12 +37,16 @@ export class ScraperManager {
36 * Register a scraper to be used by the Data Bank.37 * Register a scraper to be used by the Data Bank.
37 * @param {Scraper} scraper Instance of a scraper to register38 * @param {Scraper} scraper Instance of a scraper to register
38 */39 */
39 static registerDataBankScraper(scraper) {40 static async registerDataBankScraper(scraper) {
40 if (ScraperManager.#scrapers.some(s => s.id === scraper.id)) {41 if (ScraperManager.#scrapers.some(s => s.id === scraper.id)) {
41 console.warn(`Scraper with ID ${scraper.id} already registered`);42 console.warn(`Scraper with ID ${scraper.id} already registered`);
42 return;43 return;
43 }44 }
4445
46 if (scraper.init) {
47 await scraper.init();
48 }
49
45 ScraperManager.#scrapers.push(scraper);50 ScraperManager.#scrapers.push(scraper);
46 }51 }
4752
@@ -462,7 +467,9 @@ class YouTubeScraper {
462 this.description = 'Download a transcript from a YouTube video.';467 this.description = 'Download a transcript from a YouTube video.';
463 this.iconClass = 'fa-brands fa-youtube';468 this.iconClass = 'fa-brands fa-youtube';
464 this.iconAvailable = true;469 this.iconAvailable = true;
470 }
465471
472 async init() {
466 SlashCommandParser.addCommandObject(SlashCommand.fromProps({473 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
467 name: 'yt-script',474 name: 'yt-script',
468 callback: async (args, url) => {475 callback: async (args, url) => {
@@ -564,9 +571,11 @@ class YouTubeScraper {
564 }571 }
565}572}
566573
567ScraperManager.registerDataBankScraper(new FileScraper());574export async function initScrapers() {
568ScraperManager.registerDataBankScraper(new Notepad());575 await ScraperManager.registerDataBankScraper(new FileScraper());
569ScraperManager.registerDataBankScraper(new WebScraper());576 await ScraperManager.registerDataBankScraper(new Notepad());
570ScraperManager.registerDataBankScraper(new MediaWikiScraper());577 await ScraperManager.registerDataBankScraper(new WebScraper());
571ScraperManager.registerDataBankScraper(new FandomScraper());578 await ScraperManager.registerDataBankScraper(new MediaWikiScraper());
572ScraperManager.registerDataBankScraper(new YouTubeScraper());579 await ScraperManager.registerDataBankScraper(new FandomScraper());
580 await ScraperManager.registerDataBankScraper(new YouTubeScraper());
581}
public/scripts/slash-commands.js+225 -108
@@ -55,7 +55,7 @@ import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockStat
55import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';55import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
56import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';56import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
57import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';57import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';
58import { debounce, delay, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';58import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
59import { registerVariableCommands, resolveVariable } from './variables.js';59import { registerVariableCommands, resolveVariable } from './variables.js';
60import { background_settings } from './backgrounds.js';60import { background_settings } from './backgrounds.js';
61import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';61import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
@@ -68,7 +68,6 @@ import { SlashCommandNamedArgumentAssignment } from './slash-commands/SlashComma
68import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';68import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
69import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';69import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
70import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';70import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
71import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js';
72import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';71import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
73import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';72import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
74export {73export {
@@ -174,20 +173,96 @@ export function initDefaultSlashCommands() {
174 `,173 `,
175 }));174 }));
176 SlashCommandParser.addCommandObject(SlashCommand.fromProps({175 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
176 name: 'char-find',
177 aliases: ['findchar'],
178 callback: (args, name) => {
179 if (typeof name !== 'string') throw new Error('name must be a string');
180 if (args.preferCurrent instanceof SlashCommandClosure || Array.isArray(args.preferCurrent)) throw new Error('preferCurrent cannot be a closure or array');
181 if (args.quiet instanceof SlashCommandClosure || Array.isArray(args.quiet)) throw new Error('quiet cannot be a closure or array');
182
183 const char = findChar({ name: name, filteredByTags: validateArrayArgString(args.tag, 'tag'), preferCurrentChar: !isFalseBoolean(args.preferCurrent), quiet: isTrueBoolean(args.quiet) });
184 return char?.avatar ?? '';
185 },
186 returns: 'the avatar key (unique identifier) of the character',
187 namedArgumentList: [
188 SlashCommandNamedArgument.fromProps({
189 name: 'tag',
190 description: 'Supply one or more tags to filter down to the correct character for the provided name, if multiple characters have the same name.',
191 typeList: [ARGUMENT_TYPE.STRING],
192 enumProvider: commonEnumProviders.tags('assigned'),
193 acceptsMultiple: true,
194 }),
195 SlashCommandNamedArgument.fromProps({
196 name: 'preferCurrent',
197 description: 'Prefer current character or characters in a group, if multiple characters match',
198 typeList: [ARGUMENT_TYPE.BOOLEAN],
199 defaultValue: 'true',
200 }),
201 SlashCommandNamedArgument.fromProps({
202 name: 'quiet',
203 description: 'Do not show warning if multiple charactrers are found',
204 typeList: [ARGUMENT_TYPE.BOOLEAN],
205 defaultValue: 'false',
206 enumProvider: commonEnumProviders.boolean('trueFalse'),
207 }),
208 ],
209 unnamedArgumentList: [
210 SlashCommandArgument.fromProps({
211 description: 'Character name - or unique character identifier (avatar key)',
212 typeList: [ARGUMENT_TYPE.STRING],
213 enumProvider: commonEnumProviders.characters('character'),
214 forceEnum: false,
215 }),
216 ],
217 helpString: `
218 <div>
219 Searches for a character and returns its avatar key.
220 </div>
221 <div>
222 This can be used to choose the correct character for something like <code>/sendas</code> or other commands in need of a character name
223 if you have multiple characters with the same name.
224 </div>
225 <div>
226 <strong>Example:</strong>
227 <ul>
228 <li>
229 <pre><code>/char-find name="Chloe"</code></pre>
230 Returns the avatar key for "Chloe".
231 </li>
232 <li>
233 <pre><code>/search name="Chloe" tag="friend"</code></pre>
234 Returns the avatar key for the character "Chloe" that is tagged with "friend".
235 This is useful if you for example have multiple characters named "Chloe", and the others are "foe", "goddess", or anything else,
236 so you can actually select the character you are looking for.
237 </li>
238 </ul>
239 </div>
240 `,
241 }));
242 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
177 name: 'sendas',243 name: 'sendas',
178 callback: sendMessageAs,244 callback: sendMessageAs,
179 namedArgumentList: [245 namedArgumentList: [
180 SlashCommandNamedArgument.fromProps({246 SlashCommandNamedArgument.fromProps({
181 name: 'name',247 name: 'name',
182 description: 'Character name',248 description: 'Character name - or unique character identifier (avatar key)',
183 typeList: [ARGUMENT_TYPE.STRING],249 typeList: [ARGUMENT_TYPE.STRING],
184 isRequired: true,250 isRequired: true,
185 enumProvider: commonEnumProviders.characters('character'),251 enumProvider: commonEnumProviders.characters('character'),
186 forceEnum: false,252 forceEnum: false,
187 }),253 }),
188 new SlashCommandNamedArgument(254 SlashCommandNamedArgument.fromProps({
189 'compact', 'Use compact layout', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',255 name: 'avatar',
190 ),256 description: 'Character avatar override (Can be either avatar key or just the character name to pull the avatar from)',
257 typeList: [ARGUMENT_TYPE.STRING],
258 enumProvider: commonEnumProviders.characters('character'),
259 }),
260 SlashCommandNamedArgument.fromProps({
261 name: 'compact',
262 description: 'Use compact layout',
263 typeList: [ARGUMENT_TYPE.BOOLEAN],
264 defaultValue: 'false',
265 }),
191 SlashCommandNamedArgument.fromProps({266 SlashCommandNamedArgument.fromProps({
192 name: 'at',267 name: 'at',
193 description: 'position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values are accepted and will work similarly to how \'depth\' usually works. For example, -1 will insert the message right before the last message in chat.',268 description: 'position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values are accepted and will work similarly to how \'depth\' usually works. For example, -1 will insert the message right before the last message in chat.',
@@ -211,6 +286,10 @@ export function initDefaultSlashCommands() {
211 <pre><code>/sendas name="Chloe" Hello, guys!</code></pre>286 <pre><code>/sendas name="Chloe" Hello, guys!</code></pre>
212 will send "Hello, guys!" from "Chloe".287 will send "Hello, guys!" from "Chloe".
213 </li>288 </li>
289 <li>
290 <pre><code>/sendas name="Chloe" avatar="BigBadBoss" Hehehe, I am the big bad evil, fear me.</code></pre>
291 will send a message as the character "Chloe", but utilizing the avatar from a character named "BigBadBoss".
292 </li>
214 </ul>293 </ul>
215 </div>294 </div>
216 <div>295 <div>
@@ -381,12 +460,14 @@ export function initDefaultSlashCommands() {
381 SlashCommandParser.addCommandObject(SlashCommand.fromProps({460 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
382 name: 'go',461 name: 'go',
383 callback: goToCharacterCallback,462 callback: goToCharacterCallback,
463 returns: 'The character/group name',
384 unnamedArgumentList: [464 unnamedArgumentList: [
385 SlashCommandArgument.fromProps({465 SlashCommandArgument.fromProps({
386 description: 'name',466 description: 'Character name - or unique character identifier (avatar key)',
387 typeList: [ARGUMENT_TYPE.STRING],467 typeList: [ARGUMENT_TYPE.STRING],
388 isRequired: true,468 isRequired: true,
389 enumProvider: commonEnumProviders.characters('all'),469 enumProvider: commonEnumProviders.characters('all'),
470 forceEnum: true,
390 }),471 }),
391 ],472 ],
392 helpString: 'Opens up a chat with the character or group by its name',473 helpString: 'Opens up a chat with the character or group by its name',
@@ -432,7 +513,7 @@ export function initDefaultSlashCommands() {
432 namedArgumentList: [513 namedArgumentList: [
433 SlashCommandNamedArgument.fromProps({514 SlashCommandNamedArgument.fromProps({
434 name: 'name',515 name: 'name',
435 description: 'character name',516 description: 'Character name - or unique character identifier (avatar key)',
436 typeList: [ARGUMENT_TYPE.STRING],517 typeList: [ARGUMENT_TYPE.STRING],
437 isRequired: true,518 isRequired: true,
438 enumProvider: commonEnumProviders.characters('character'),519 enumProvider: commonEnumProviders.characters('character'),
@@ -451,7 +532,7 @@ export function initDefaultSlashCommands() {
451 namedArgumentList: [],532 namedArgumentList: [],
452 unnamedArgumentList: [533 unnamedArgumentList: [
453 SlashCommandArgument.fromProps({534 SlashCommandArgument.fromProps({
454 description: 'name',535 description: 'Character name - or unique character identifier (avatar key)',
455 typeList: [ARGUMENT_TYPE.STRING],536 typeList: [ARGUMENT_TYPE.STRING],
456 isRequired: true,537 isRequired: true,
457 enumProvider: commonEnumProviders.characters('character'),538 enumProvider: commonEnumProviders.characters('character'),
@@ -618,7 +699,7 @@ export function initDefaultSlashCommands() {
618 aliases: ['addmember', 'memberadd'],699 aliases: ['addmember', 'memberadd'],
619 unnamedArgumentList: [700 unnamedArgumentList: [
620 SlashCommandArgument.fromProps({701 SlashCommandArgument.fromProps({
621 description: 'character name',702 description: 'Character name - or unique character identifier (avatar key)',
622 typeList: [ARGUMENT_TYPE.STRING],703 typeList: [ARGUMENT_TYPE.STRING],
623 isRequired: true,704 isRequired: true,
624 enumProvider: () => selected_group ? commonEnumProviders.characters('character')() : [],705 enumProvider: () => selected_group ? commonEnumProviders.characters('character')() : [],
@@ -856,7 +937,7 @@ export function initDefaultSlashCommands() {
856 ),937 ),
857 SlashCommandNamedArgument.fromProps({938 SlashCommandNamedArgument.fromProps({
858 name: 'name',939 name: 'name',
859 description: 'in-prompt name for instruct mode',940 description: 'in-prompt character name for instruct mode (or unique character identifier (avatar key), which will be used as name)',
860 typeList: [ARGUMENT_TYPE.STRING],941 typeList: [ARGUMENT_TYPE.STRING],
861 defaultValue: 'System',942 defaultValue: 'System',
862 enumProvider: () => [...commonEnumProviders.characters('character')(), new SlashCommandEnumValue('System', null, enumTypes.enum, enumIcons.assistant)],943 enumProvider: () => [...commonEnumProviders.characters('character')(), new SlashCommandEnumValue('System', null, enumTypes.enum, enumIcons.assistant)],
@@ -2293,7 +2374,8 @@ async function generateCallback(args, value) {
22932374
2294 setEphemeralStopStrings(resolveVariable(args?.stop));2375 setEphemeralStopStrings(resolveVariable(args?.stop));
2295 const name = args?.name;2376 const name = args?.name;
2296 const result = await generateQuietPrompt(value, quietToLoud, false, '', name, length);2377 const char = findChar({ name: name });
2378 const result = await generateQuietPrompt(value, quietToLoud, false, '', char?.name ?? name, length);
2297 return result;2379 return result;
2298 } catch (err) {2380 } catch (err) {
2299 console.error('Error on /gen generation', err);2381 console.error('Error on /gen generation', err);
@@ -2481,26 +2563,22 @@ async function askCharacter(args, text) {
2481 return '';2563 return '';
2482 }2564 }
24832565
2484 let name = '';2566 if (!args.name) {
24852567 toastr.warning('You must specify a name of the character to ask.');
2486 if (args?.name) {2568 return '';
2487 name = args.name.trim();
2488
2489 if (!name) {
2490 toastr.warning('You must specify a name of the character to ask.');
2491 return '';
2492 }
2493 }2569 }
24942570
2495 const prevChId = this_chid;2571 const prevChId = this_chid;
24962572
2497 // Find the character2573 // Find the character
2498 const chId = characters.findIndex((e) => e.name === name || e.avatar === name);2574 const character = findChar({ name: args?.name });
2499 if (!characters[chId] || chId === -1) {2575 if (!character) {
2500 toastr.error('Character not found.');2576 toastr.error('Character not found.');
2501 return '';2577 return '';
2502 }2578 }
25032579
2580 const chId = getCharIndex(character);
2581
2504 if (text) {2582 if (text) {
2505 const mesText = getRegexedString(text.trim(), regex_placement.SLASH_COMMAND);2583 const mesText = getRegexedString(text.trim(), regex_placement.SLASH_COMMAND);
2506 // Sending a message implicitly saves the chat, so this needs to be done before changing the character2584 // Sending a message implicitly saves the chat, so this needs to be done before changing the character
@@ -2511,32 +2589,27 @@ async function askCharacter(args, text) {
2511 // Override character and send a user message2589 // Override character and send a user message
2512 setCharacterId(String(chId));2590 setCharacterId(String(chId));
25132591
2514 const character = characters[chId];2592 const { name, force_avatar, original_avatar } = getNameAndAvatarForMessage(character, args?.name);
2515 let force_avatar, original_avatar;
25162593
2517 if (character && character.avatar !== 'none') {2594 setCharacterName(name);
2518 force_avatar = getThumbnailUrl('avatar', character.avatar);
2519 original_avatar = character.avatar;
2520 }
2521 else {
2522 force_avatar = default_avatar;
2523 original_avatar = default_avatar;
2524 }
2525
2526 setCharacterName(character.name);
25272595
2528 const restoreCharacter = () => {2596 const restoreCharacter = () => {
2529 if (String(this_chid) !== String(chId)) {2597 if (String(this_chid) !== String(chId)) {
2530 return;2598 return;
2531 }2599 }
25322600
2533 setCharacterId(prevChId);2601 if (prevChId !== undefined) {
2534 setCharacterName(characters[prevChId].name);2602 setCharacterId(prevChId);
2603 setCharacterName(characters[prevChId].name);
2604 } else {
2605 setCharacterId(undefined);
2606 setCharacterName(neutralCharacterName);
2607 }
25352608
2536 // Only force the new avatar if the character name is the same2609 // Only force the new avatar if the character name is the same
2537 // This skips if an error was fired2610 // This skips if an error was fired
2538 const lastMessage = chat[chat.length - 1];2611 const lastMessage = chat[chat.length - 1];
2539 if (lastMessage && lastMessage?.name === character.name) {2612 if (lastMessage && lastMessage?.name === name) {
2540 lastMessage.force_avatar = force_avatar;2613 lastMessage.force_avatar = force_avatar;
2541 lastMessage.original_avatar = original_avatar;2614 lastMessage.original_avatar = original_avatar;
2542 }2615 }
@@ -2547,7 +2620,7 @@ async function askCharacter(args, text) {
2547 // Run generate and restore previous character2620 // Run generate and restore previous character
2548 try {2621 try {
2549 eventSource.once(event_types.MESSAGE_RECEIVED, restoreCharacter);2622 eventSource.once(event_types.MESSAGE_RECEIVED, restoreCharacter);
2550 toastr.info(`Asking ${character.name} something...`);2623 toastr.info(`Asking ${name} something...`);
2551 askResult = await Generate('ask_command');2624 askResult = await Generate('ask_command');
2552 } catch (error) {2625 } catch (error) {
2553 restoreCharacter();2626 restoreCharacter();
@@ -2741,26 +2814,23 @@ async function removeGroupMemberCallback(_, arg) {
2741 return '';2814 return '';
2742}2815}
27432816
2744async function addGroupMemberCallback(_, arg) {2817async function addGroupMemberCallback(_, name) {
2745 if (!selected_group) {2818 if (!selected_group) {
2746 toastr.warning('Cannot run /memberadd command outside of a group chat.');2819 toastr.warning('Cannot run /memberadd command outside of a group chat.');
2747 return '';2820 return '';
2748 }2821 }
27492822
2750 if (!arg) {2823 if (!name) {
2751 console.warn('WARN: No argument provided for /memberadd command');2824 console.warn('WARN: No argument provided for /memberadd command');
2752 return '';2825 return '';
2753 }2826 }
27542827
2755 arg = arg.trim();2828 const character = findChar({ name: name, preferCurrentChar: false });
2756 const chid = findCharacterIndex(arg);2829 if (!character) {
27572830 console.warn(`WARN: No character found for argument ${name}`);
2758 if (chid === -1) {
2759 console.warn(`WARN: No character found for argument ${arg}`);
2760 return '';2831 return '';
2761 }2832 }
27622833
2763 const character = characters[chid];
2764 const group = groups.find(x => x.id === selected_group);2834 const group = groups.find(x => x.id === selected_group);
27652835
2766 if (!group || !Array.isArray(group.members)) {2836 if (!group || !Array.isArray(group.members)) {
@@ -2829,7 +2899,7 @@ function findPersonaByName(name) {
2829 }2899 }
28302900
2831 for (const persona of Object.entries(power_user.personas)) {2901 for (const persona of Object.entries(power_user.personas)) {
2832 if (persona[1].toLowerCase() === name.toLowerCase()) {2902 if (equalsIgnoreCaseAndAccents(persona[1], name)) {
2833 return persona[0];2903 return persona[0];
2834 }2904 }
2835 }2905 }
@@ -2872,7 +2942,9 @@ async function deleteMessagesByNameCallback(_, name) {
2872 return;2942 return;
2873 }2943 }
28742944
2875 name = name.trim();2945 // Search for a matching character to get the real name, or take the name provided
2946 const character = findChar({ name: name });
2947 name = character?.name || name;
28762948
2877 const messagesToDelete = [];2949 const messagesToDelete = [];
2878 chat.forEach((value) => {2950 chat.forEach((value) => {
@@ -2901,60 +2973,34 @@ async function deleteMessagesByNameCallback(_, name) {
2901 return '';2973 return '';
2902}2974}
29032975
2904function findCharacterIndex(name) {
2905 const matchTypes = [
2906 (a, b) => a === b,
2907 (a, b) => a.startsWith(b),
2908 (a, b) => a.includes(b),
2909 ];
2910
2911 const exactAvatarMatch = characters.findIndex(x => x.avatar === name);
2912
2913 if (exactAvatarMatch !== -1) {
2914 return exactAvatarMatch;
2915 }
2916
2917 for (const matchType of matchTypes) {
2918 const index = characters.findIndex(x => matchType(x.name.toLowerCase(), name.toLowerCase()));
2919 if (index !== -1) {
2920 return index;
2921 }
2922 }
2923
2924 return -1;
2925}
2926
2927async function goToCharacterCallback(_, name) {2976async function goToCharacterCallback(_, name) {
2928 if (!name) {2977 if (!name) {
2929 console.warn('WARN: No character name provided for /go command');2978 console.warn('WARN: No character name provided for /go command');
2930 return;2979 return;
2931 }2980 }
29322981
2933 name = name.trim();2982 const character = findChar({ name: name });
2934 const characterIndex = findCharacterIndex(name);2983 if (character) {
29352984 const chid = getCharIndex(character);
2936 if (characterIndex !== -1) {2985 await openChat(new String(chid));
2937 await openChat(new String(characterIndex));2986 setActiveCharacter(character.avatar);
2938 setActiveCharacter(characters[characterIndex]?.avatar);
2939 setActiveGroup(null);2987 setActiveGroup(null);
2940 return characters[characterIndex]?.name;2988 return character.name;
2941 } else {
2942 const group = groups.find(it => it.name.toLowerCase() == name.toLowerCase());
2943 if (group) {
2944 await openGroupById(group.id);
2945 setActiveCharacter(null);
2946 setActiveGroup(group.id);
2947 return group.name;
2948 } else {
2949 console.warn(`No matches found for name "${name}"`);
2950 return '';
2951 }
2952 }2989 }
2990 const group = groups.find(it => equalsIgnoreCaseAndAccents(it.name, name));
2991 if (group) {
2992 await openGroupById(group.id);
2993 setActiveCharacter(null);
2994 setActiveGroup(group.id);
2995 return group.name;
2996 }
2997 console.warn(`No matches found for name "${name}"`);
2998 return '';
2953}2999}
29543000
2955async function openChat(id) {3001async function openChat(chid) {
2956 resetSelectedGroup();3002 resetSelectedGroup();
2957 setCharacterId(id);3003 setCharacterId(chid);
2958 await delay(1);3004 await delay(1);
2959 await reloadCurrentChat();3005 await reloadCurrentChat();
2960}3006}
@@ -3100,6 +3146,79 @@ async function setNarratorName(_, text) {
3100 return '';3146 return '';
3101}3147}
31023148
3149/**
3150 * Checks if an argument is a string array (or undefined), and if not, throws an error
3151 * @param {string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined} arg The named argument to check
3152 * @param {string} name The name of the argument for the error message
3153 * @param {object} [options={}] - The optional arguments
3154 * @param {boolean} [options.allowUndefined=false] - Whether the argument can be undefined
3155 * @throws {Error} If the argument is not an array
3156 * @returns {string[]}
3157 */
3158export function validateArrayArgString(arg, name, { allowUndefined = true } = {}) {
3159 if (arg === undefined) {
3160 if (allowUndefined) return undefined;
3161 throw new Error(`Argument "${name}" is undefined, but must be a string array`);
3162 }
3163 if (!Array.isArray(arg)) throw new Error(`Argument "${name}" must be an array`);
3164 if (!arg.every(x => typeof x === 'string')) throw new Error(`Argument "${name}" must be an array of strings`);
3165 return arg;
3166}
3167
3168/**
3169 * Checks if an argument is a string or closure array (or undefined), and if not, throws an error
3170 * @param {string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined} arg The named argument to check
3171 * @param {string} name The name of the argument for the error message
3172 * @param {object} [options={}] - The optional arguments
3173 * @param {boolean} [options.allowUndefined=false] - Whether the argument can be undefined
3174 * @throws {Error} If the argument is not an array of strings or closures
3175 * @returns {(string|SlashCommandClosure)[]}
3176 */
3177export function validateArrayArg(arg, name, { allowUndefined = true } = {}) {
3178 if (arg === undefined) {
3179 if (allowUndefined) return [];
3180 throw new Error(`Argument "${name}" is undefined, but must be an array of strings or closures`);
3181 }
3182 if (!Array.isArray(arg)) throw new Error(`Argument "${name}" must be an array`);
3183 if (!arg.every(x => typeof x === 'string' || x instanceof SlashCommandClosure)) throw new Error(`Argument "${name}" must be an array of strings or closures`);
3184 return arg;
3185}
3186
3187
3188/**
3189 * Retrieves the name and avatar information for a message
3190 *
3191 * The name of the character will always have precendence over the one given as argument. If you want to specify a different name for the message,
3192 * explicitly implement this in the code using this.
3193 *
3194 * @param {object?} character - The character object to get the avatar data for
3195 * @param {string?} name - The name to get the avatar data for
3196 * @returns {{name: string, force_avatar: string, original_avatar: string}} An object containing the name for the message, forced avatar URL, and original avatar
3197 */
3198export function getNameAndAvatarForMessage(character, name = null) {
3199 const isNeutralCharacter = !character && name2 === neutralCharacterName && name === neutralCharacterName;
3200 const currentChar = characters[this_chid];
3201
3202 let force_avatar, original_avatar;
3203 if (character?.avatar === currentChar?.avatar || isNeutralCharacter) {
3204 // If the targeted character is the currently selected one in a solo chat, we don't need to force any avatars
3205 }
3206 else if (character && character.avatar !== 'none') {
3207 force_avatar = getThumbnailUrl('avatar', character.avatar);
3208 original_avatar = character.avatar;
3209 }
3210 else {
3211 force_avatar = default_avatar;
3212 original_avatar = default_avatar;
3213 }
3214
3215 return {
3216 name: character?.name || name,
3217 force_avatar: force_avatar,
3218 original_avatar: original_avatar,
3219 };
3220}
3221
3103export async function sendMessageAs(args, text) {3222export async function sendMessageAs(args, text) {
3104 if (!text) {3223 if (!text) {
3105 return '';3224 return '';
@@ -3138,26 +3257,18 @@ export async function sendMessageAs(args, text) {
3138 const isSystem = bias && !removeMacros(mesText).length;3257 const isSystem = bias && !removeMacros(mesText).length;
3139 const compact = isTrueBoolean(args?.compact);3258 const compact = isTrueBoolean(args?.compact);
31403259
3141 const character = characters.find(x => x.avatar === name) ?? characters.find(x => x.name === name);3260 const character = findChar({ name: name });
3142 let force_avatar, original_avatar;
3143
3144 const chatCharacter = this_chid !== undefined ? characters[this_chid] : null;
3145 const isNeutralCharacter = !chatCharacter && name2 === neutralCharacterName && name === neutralCharacterName;
31463261
3147 if (chatCharacter === character || isNeutralCharacter) {3262 const avatarCharacter = args.avatar ? findChar({ name: args.avatar }) : character;
3148 // If the targeted character is the currently selected one in a solo chat, we don't need to force any avatars3263 if (args.avatar && !avatarCharacter) {
3149 }3264 toastr.warning(`Character for avatar ${args.avatar} not found`);
3150 else if (character && character.avatar !== 'none') {3265 return '';
3151 force_avatar = getThumbnailUrl('avatar', character.avatar);
3152 original_avatar = character.avatar;
3153 }
3154 else {
3155 force_avatar = default_avatar;
3156 original_avatar = default_avatar;
3157 }3266 }
31583267
3268 const { name: avatarCharName, force_avatar, original_avatar } = getNameAndAvatarForMessage(avatarCharacter, name);
3269
3159 const message = {3270 const message = {
3160 name: name,3271 name: character?.name || name || avatarCharName,
3161 is_user: false,3272 is_user: false,
3162 is_system: isSystem,3273 is_system: isSystem,
3163 send_date: getMessageTimeStamp(),3274 send_date: getMessageTimeStamp(),
@@ -3561,11 +3672,17 @@ function setPromptEntryCallback(args, targetState) {
3561 const prompts = promptManager.serviceSettings.prompts;3672 const prompts = promptManager.serviceSettings.prompts;
35623673
3563 function parseArgs(arg) {3674 function parseArgs(arg) {
3675 // Arg is already an array
3676 if (Array.isArray(arg)) {
3677 return arg;
3678 }
3564 const list = [];3679 const list = [];
3565 try {3680 try {
3681 // Arg is a JSON-stringified array
3566 const parsedArg = JSON.parse(arg);3682 const parsedArg = JSON.parse(arg);
3567 list.push(...Array.isArray(parsedArg) ? parsedArg : [arg]);3683 list.push(...Array.isArray(parsedArg) ? parsedArg : [arg]);
3568 } catch {3684 } catch {
3685 // Arg is a string
3569 list.push(arg);3686 list.push(arg);
3570 }3687 }
3571 return list;3688 return list;
public/scripts/slash-commands/SlashCommand.js+2 -2
@@ -15,13 +15,13 @@ import { SlashCommandScope } from './SlashCommandScope.js';
15 * _abortController:SlashCommandAbortController,15 * _abortController:SlashCommandAbortController,
16 * _debugController:SlashCommandDebugController,16 * _debugController:SlashCommandDebugController,
17 * _hasUnnamedArgument:boolean,17 * _hasUnnamedArgument:boolean,
18 * [id:string]:string|SlashCommandClosure,18 * [id:string]:string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined,
19 * }} NamedArguments19 * }} NamedArguments
20 */20 */
2121
22/**22/**
23 * Alternative object for local JSDocs, where you don't need existing pipe, scope, etc. arguments23 * Alternative object for local JSDocs, where you don't need existing pipe, scope, etc. arguments
24 * @typedef {{[id:string]:string|SlashCommandClosure}} NamedArgumentsCapture24 * @typedef {{[id:string]:string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined}} NamedArgumentsCapture
25 */25 */
2626
27/**27/**
public/scripts/slash-commands/SlashCommandClosure.js+60 -11
@@ -2,6 +2,7 @@ import { substituteParams } from '../../script.js';
2import { delay, escapeRegex, uuidv4 } from '../utils.js';2import { delay, escapeRegex, uuidv4 } from '../utils.js';
3import { SlashCommand } from './SlashCommand.js';3import { SlashCommand } from './SlashCommand.js';
4import { SlashCommandAbortController } from './SlashCommandAbortController.js';4import { SlashCommandAbortController } from './SlashCommandAbortController.js';
5import { SlashCommandNamedArgument } from './SlashCommandArgument.js';
5import { SlashCommandBreak } from './SlashCommandBreak.js';6import { SlashCommandBreak } from './SlashCommandBreak.js';
6import { SlashCommandBreakController } from './SlashCommandBreakController.js';7import { SlashCommandBreakController } from './SlashCommandBreakController.js';
7import { SlashCommandBreakPoint } from './SlashCommandBreakPoint.js';8import { SlashCommandBreakPoint } from './SlashCommandBreakPoint.js';
@@ -53,7 +54,7 @@ export class SlashCommandClosure {
53 *54 *
54 * @param {string} text55 * @param {string} text
55 * @param {SlashCommandScope} scope56 * @param {SlashCommandScope} scope
56 * @returns57 * @returns {string|SlashCommandClosure|(string|SlashCommandClosure)[]}
57 */58 */
58 substituteParams(text, scope = null) {59 substituteParams(text, scope = null) {
59 let isList = false;60 let isList = false;
@@ -379,6 +380,52 @@ export class SlashCommandClosure {
379 * @param {import('./SlashCommand.js').NamedArguments} args380 * @param {import('./SlashCommand.js').NamedArguments} args
380 */381 */
381 async substituteNamedArguments(executor, args) {382 async substituteNamedArguments(executor, args) {
383 /**
384 * Handles the assignment of named arguments, considering if they accept multiple values
385 * @param {string} name The name of the argument, as defined for the command execution
386 * @param {string|SlashCommandClosure|(string|SlashCommandClosure)[]} value The value to be assigned
387 */
388 const assign = (name, value) => {
389 // If an array is supposed to be assigned, assign it one by one
390 if (Array.isArray(value)) {
391 for (const val of value) {
392 assign(name, val);
393 }
394 return;
395 }
396
397 const definition = executor.command.namedArgumentList.find(x => x.name == name);
398
399 // Prefer definition name if a valid named args defintion is found
400 name = definition?.name ?? name;
401
402 // Unescape named argument
403 if (value && typeof value == 'string') {
404 value = value
405 .replace(/\\\{/g, '{')
406 .replace(/\\\}/g, '}');
407 }
408
409 // If the named argument accepts multiple values, we have to make sure to build an array correctly
410 if (definition?.acceptsMultiple) {
411 if (args[name] !== undefined) {
412 // If there already is something for that named arg, make the value is an array and add to it
413 let currentValue = args[name];
414 if (!Array.isArray(currentValue)) {
415 currentValue = [currentValue];
416 }
417 currentValue.push(value);
418 args[name] = currentValue;
419 } else {
420 // If there is nothing in there, we create an array with that singular value
421 args[name] = [value];
422 }
423 } else {
424 args[name] !== undefined && console.debug(`Named argument assigned multiple times: ${name}`);
425 args[name] = value;
426 }
427 };
428
382 // substitute named arguments429 // substitute named arguments
383 for (const arg of executor.namedArgumentList) {430 for (const arg of executor.namedArgumentList) {
384 if (arg.value instanceof SlashCommandClosure) {431 if (arg.value instanceof SlashCommandClosure) {
@@ -390,19 +437,12 @@ export class SlashCommandClosure {
390 closure.debugController = this.debugController;437 closure.debugController = this.debugController;
391 }438 }
392 if (closure.executeNow) {439 if (closure.executeNow) {
393 args[arg.name] = (await closure.execute())?.pipe;440 assign(arg.name, (await closure.execute())?.pipe);
394 } else {441 } else {
395 args[arg.name] = closure;442 assign(arg.name, closure);
396 }443 }
397 } else {444 } else {
398 args[arg.name] = this.substituteParams(arg.value);445 assign(arg.name, this.substituteParams(arg.value));
399 }
400 // unescape named argument
401 if (typeof args[arg.name] == 'string') {
402 args[arg.name] = args[arg.name]
403 ?.replace(/\\\{/g, '{')
404 ?.replace(/\\\}/g, '}')
405 ;
406 }446 }
407 }447 }
408 }448 }
@@ -424,6 +464,7 @@ export class SlashCommandClosure {
424 } else {464 } else {
425 value = [];465 value = [];
426 for (let i = 0; i < executor.unnamedArgumentList.length; i++) {466 for (let i = 0; i < executor.unnamedArgumentList.length; i++) {
467 /** @type {string|SlashCommandClosure|(string|SlashCommandClosure)[]} */
427 let v = executor.unnamedArgumentList[i].value;468 let v = executor.unnamedArgumentList[i].value;
428 if (v instanceof SlashCommandClosure) {469 if (v instanceof SlashCommandClosure) {
429 /**@type {SlashCommandClosure}*/470 /**@type {SlashCommandClosure}*/
@@ -467,6 +508,14 @@ export class SlashCommandClosure {
467 return v;508 return v;
468 });509 });
469 }510 }
511
512 value ??= '';
513
514 // Make sure that if unnamed args are split, it should always return an array
515 if (executor.command.splitUnnamedArgument && !Array.isArray(value)) {
516 value = [value];
517 }
518
470 return value;519 return value;
471 }520 }
472521
public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js+43 -2
@@ -2,7 +2,7 @@ import { chat_metadata, characters, substituteParams, chat, extension_prompt_rol
2import { extension_settings } from '../extensions.js';2import { extension_settings } from '../extensions.js';
3import { getGroupMembers, groups } from '../group-chats.js';3import { getGroupMembers, groups } from '../group-chats.js';
4import { power_user } from '../power-user.js';4import { power_user } from '../power-user.js';
5import { searchCharByName, getTagsList, tags } from '../tags.js';5import { searchCharByName, getTagsList, tags, tag_map } from '../tags.js';
6import { world_names } from '../world-info.js';6import { world_names } from '../world-info.js';
7import { SlashCommandClosure } from './SlashCommandClosure.js';7import { SlashCommandClosure } from './SlashCommandClosure.js';
8import { SlashCommandEnumValue, enumTypes } from './SlashCommandEnumValue.js';8import { SlashCommandEnumValue, enumTypes } from './SlashCommandEnumValue.js';
@@ -153,6 +153,35 @@ export const commonEnumProviders = {
153 },153 },
154154
155 /**155 /**
156 * Enum values for numbers and variable names
157 *
158 * Includes all variable names and the ability to specify any number
159 *
160 * @param {SlashCommandExecutor} executor - The executor of the slash command
161 * @param {SlashCommandScope} scope - The scope of the slash command
162 * @returns {SlashCommandEnumValue[]} The enum values
163 */
164 numbersAndVariables: (executor, scope) => [
165 ...commonEnumProviders.variables('all')(executor, scope),
166 new SlashCommandEnumValue(
167 'any variable name',
168 null,
169 enumTypes.variable,
170 enumIcons.variable,
171 (input) => /^\w*$/.test(input),
172 (input) => input,
173 ),
174 new SlashCommandEnumValue(
175 'any number',
176 null,
177 enumTypes.number,
178 enumIcons.number,
179 (input) => input == '' || !Number.isNaN(Number(input)),
180 (input) => input,
181 ),
182 ],
183
184 /**
156 * All possible char entities, like characters and groups. Can be filtered down to just one type.185 * All possible char entities, like characters and groups. Can be filtered down to just one type.
157 *186 *
158 * @param {('all' | 'character' | 'group')?} [mode='all'] - Which type to return187 * @param {('all' | 'character' | 'group')?} [mode='all'] - Which type to return
@@ -182,6 +211,18 @@ export const commonEnumProviders = {
182 personas: () => Object.values(power_user.personas).map(persona => new SlashCommandEnumValue(persona, null, enumTypes.name, enumIcons.persona)),211 personas: () => Object.values(power_user.personas).map(persona => new SlashCommandEnumValue(persona, null, enumTypes.name, enumIcons.persona)),
183212
184 /**213 /**
214 * All possible tags, or only those that have been assigned
215 *
216 * @param {('all' | 'assigned')} [mode='all'] - Which types of tags to show
217 * @returns {() => SlashCommandEnumValue[]}
218 */
219 tags: (mode = 'all') => () => {
220 let assignedTags = mode === 'assigned' ? new Set(Object.values(tag_map).flat()) : new Set();
221 return tags.filter(tag => mode === 'all' || (mode === 'assigned' && assignedTags.has(tag.id)))
222 .map(tag => new SlashCommandEnumValue(tag.name, null, enumTypes.command, enumIcons.tag));
223 },
224
225 /**
185 * All possible tags for a given char/group entity226 * All possible tags for a given char/group entity
186 *227 *
187 * @param {('all' | 'existing' | 'not-existing')?} [mode='all'] - Which types of tags to show228 * @param {('all' | 'existing' | 'not-existing')?} [mode='all'] - Which types of tags to show
@@ -193,7 +234,7 @@ export const commonEnumProviders = {
193 if (charName instanceof SlashCommandClosure) throw new Error('Argument \'name\' does not support closures');234 if (charName instanceof SlashCommandClosure) throw new Error('Argument \'name\' does not support closures');
194 const key = searchCharByName(substituteParams(charName), { suppressLogging: true });235 const key = searchCharByName(substituteParams(charName), { suppressLogging: true });
195 const assigned = key ? getTagsList(key) : [];236 const assigned = key ? getTagsList(key) : [];
196 return tags.filter(it => !key || mode === 'all' || mode === 'existing' && assigned.includes(it) || mode === 'not-existing' && !assigned.includes(it))237 return tags.filter(it => mode === 'all' || mode === 'existing' && assigned.includes(it) || mode === 'not-existing' && !assigned.includes(it))
197 .map(tag => new SlashCommandEnumValue(tag.name, null, enumTypes.command, enumIcons.tag));238 .map(tag => new SlashCommandEnumValue(tag.name, null, enumTypes.command, enumIcons.tag));
198 },239 },
199240
public/scripts/tags.js+8 -8
@@ -15,7 +15,7 @@ import {
15import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, FilterHelper } from './filters.js';15import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, FilterHelper } from './filters.js';
1616
17import { groupCandidatesFilter, groups, selected_group } from './group-chats.js';17import { groupCandidatesFilter, groups, selected_group } from './group-chats.js';
18import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, flashHighlight, equalsIgnoreCaseAndAccents, includesIgnoreCaseAndAccents, removeFromArray, getFreeName, debounce } from './utils.js';18import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, flashHighlight, equalsIgnoreCaseAndAccents, includesIgnoreCaseAndAccents, removeFromArray, getFreeName, debounce, findChar } from './utils.js';
19import { power_user } from './power-user.js';19import { power_user } from './power-user.js';
20import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';20import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
21import { SlashCommand } from './slash-commands/SlashCommand.js';21import { SlashCommand } from './slash-commands/SlashCommand.js';
@@ -50,7 +50,6 @@ export {
50 removeTagFromMap,50 removeTagFromMap,
51};51};
5252
53/** @typedef {import('../scripts/popup.js').Popup} Popup */
54/** @typedef {import('../script.js').Character} Character */53/** @typedef {import('../script.js').Character} Character */
5554
56const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter';55const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter';
@@ -507,7 +506,7 @@ export function getTagKeyForEntityElement(element) {
507 */506 */
508export function searchCharByName(charName, { suppressLogging = false } = {}) {507export function searchCharByName(charName, { suppressLogging = false } = {}) {
509 const entity = charName508 const entity = charName
510 ? (characters.find(x => x.name === charName) || groups.find(x => x.name == charName))509 ? (findChar({ name: charName }) || groups.find(x => equalsIgnoreCaseAndAccents(x.name, charName)))
511 : (selected_group ? groups.find(x => x.id == selected_group) : characters[this_chid]);510 : (selected_group ? groups.find(x => x.id == selected_group) : characters[this_chid]);
512 const key = getTagKeyForEntity(entity);511 const key = getTagKeyForEntity(entity);
513 if (!key) {512 if (!key) {
@@ -1861,8 +1860,9 @@ function registerTagsSlashCommands() {
1861 return String(result);1860 return String(result);
1862 },1861 },
1863 namedArgumentList: [1862 namedArgumentList: [
1864 SlashCommandNamedArgument.fromProps({ name: 'name',1863 SlashCommandNamedArgument.fromProps({
1865 description: 'Character name',1864 name: 'name',
1865 description: 'Character name - or unique character identifier (avatar key)',
1866 typeList: [ARGUMENT_TYPE.STRING],1866 typeList: [ARGUMENT_TYPE.STRING],
1867 defaultValue: '{{char}}',1867 defaultValue: '{{char}}',
1868 enumProvider: commonEnumProviders.characters(),1868 enumProvider: commonEnumProviders.characters(),
@@ -1907,7 +1907,7 @@ function registerTagsSlashCommands() {
1907 },1907 },
1908 namedArgumentList: [1908 namedArgumentList: [
1909 SlashCommandNamedArgument.fromProps({ name: 'name',1909 SlashCommandNamedArgument.fromProps({ name: 'name',
1910 description: 'Character name',1910 description: 'Character name - or unique character identifier (avatar key)',
1911 typeList: [ARGUMENT_TYPE.STRING],1911 typeList: [ARGUMENT_TYPE.STRING],
1912 defaultValue: '{{char}}',1912 defaultValue: '{{char}}',
1913 enumProvider: commonEnumProviders.characters(),1913 enumProvider: commonEnumProviders.characters(),
@@ -1950,7 +1950,7 @@ function registerTagsSlashCommands() {
1950 namedArgumentList: [1950 namedArgumentList: [
1951 SlashCommandNamedArgument.fromProps({1951 SlashCommandNamedArgument.fromProps({
1952 name: 'name',1952 name: 'name',
1953 description: 'Character name',1953 description: 'Character name - or unique character identifier (avatar key)',
1954 typeList: [ARGUMENT_TYPE.STRING],1954 typeList: [ARGUMENT_TYPE.STRING],
1955 defaultValue: '{{char}}',1955 defaultValue: '{{char}}',
1956 enumProvider: commonEnumProviders.characters(),1956 enumProvider: commonEnumProviders.characters(),
@@ -1993,7 +1993,7 @@ function registerTagsSlashCommands() {
1993 namedArgumentList: [1993 namedArgumentList: [
1994 SlashCommandNamedArgument.fromProps({1994 SlashCommandNamedArgument.fromProps({
1995 name: 'name',1995 name: 'name',
1996 description: 'Character name',1996 description: 'Character name - or unique character identifier (avatar key)',
1997 typeList: [ARGUMENT_TYPE.STRING],1997 typeList: [ARGUMENT_TYPE.STRING],
1998 defaultValue: '{{char}}',1998 defaultValue: '{{char}}',
1999 enumProvider: commonEnumProviders.characters(),1999 enumProvider: commonEnumProviders.characters(),
public/scripts/utils.js+74 -1
@@ -1,10 +1,12 @@
1import { getContext } from './extensions.js';1import { getContext } from './extensions.js';
2import { getRequestHeaders } from '../script.js';2import { characters, getRequestHeaders, this_chid } from '../script.js';
3import { isMobile } from './RossAscends-mods.js';3import { isMobile } from './RossAscends-mods.js';
4import { collapseNewlines } from './power-user.js';4import { collapseNewlines } from './power-user.js';
5import { debounce_timeout } from './constants.js';5import { debounce_timeout } from './constants.js';
6import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';6import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
7import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';7import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
8import { getTagsList } from './tags.js';
9import { groups, selected_group } from './group-chats.js';
810
9/**11/**
10 * Pagination status string template.12 * Pagination status string template.
@@ -2110,3 +2112,74 @@ export async function showFontAwesomePicker(customList = null) {
2110 }2112 }
2111 return null;2113 return null;
2112}2114}
2115
2116/**
2117 * Finds a character by name, with optional filtering and precedence for avatars
2118 * @param {object} [options={}] - The options for the search
2119 * @param {string?} [options.name=null] - The name to search for
2120 * @param {boolean} [options.allowAvatar=true] - Whether to allow searching by avatar
2121 * @param {boolean} [options.insensitive=true] - Whether the search should be case insensitive
2122 * @param {string[]?} [options.filteredByTags=null] - Tags to filter characters by
2123 * @param {boolean} [options.preferCurrentChar=true] - Whether to prefer the current character(s)
2124 * @param {boolean} [options.quiet=false] - Whether to suppress warnings
2125 * @returns {any?} - The found character or null if not found
2126 */
2127export function findChar({ name = null, allowAvatar = true, insensitive = true, filteredByTags = null, preferCurrentChar = true, quiet = false } = {}) {
2128 const matches = (char) => !name || (allowAvatar && char.avatar === name) || (insensitive ? equalsIgnoreCaseAndAccents(char.name, name) : char.name === name);
2129
2130 // Filter characters by tags if provided
2131 let filteredCharacters = characters;
2132 if (filteredByTags) {
2133 filteredCharacters = characters.filter(char => {
2134 const charTags = getTagsList(char.avatar, false);
2135 return filteredByTags.every(tagName => charTags.some(x => x.name == tagName));
2136 });
2137 }
2138
2139 // Get the current character(s)
2140 /** @type {any[]} */
2141 const currentChars = selected_group ? groups.find(group => group.id === selected_group)?.members.map(member => filteredCharacters.find(char => char.avatar === member))
2142 : filteredCharacters.filter(char => characters[this_chid]?.avatar === char.avatar);
2143
2144 // If we have a current char and prefer it, return that if it matches
2145 if (preferCurrentChar) {
2146 const preferredCharSearch = currentChars.filter(matches);
2147 if (preferredCharSearch.length > 1) {
2148 if (!quiet) toastr.warning('Multiple characters found for given conditions.');
2149 else console.warn('Multiple characters found for given conditions. Returning the first match.');
2150 }
2151 if (preferredCharSearch.length) {
2152 return preferredCharSearch[0];
2153 }
2154 }
2155
2156 // If allowAvatar is true, search by avatar first
2157 if (allowAvatar && name) {
2158 const characterByAvatar = filteredCharacters.find(char => char.avatar === name);
2159 if (characterByAvatar) {
2160 return characterByAvatar;
2161 }
2162 }
2163
2164 // Search for matching characters by name
2165 const matchingCharacters = name ? filteredCharacters.filter(matches) : filteredCharacters;
2166 if (matchingCharacters.length > 1) {
2167 if (!quiet) toastr.warning('Multiple characters found for given conditions.');
2168 else console.warn('Multiple characters found for given conditions. Returning the first match.');
2169 }
2170
2171 return matchingCharacters[0] || null;
2172}
2173
2174/**
2175 * Gets the index of a character based on the character object
2176 * @param {object} char - The character object to find the index for
2177 * @throws {Error} If the character is not found
2178 * @returns {number} The index of the character in the characters array
2179 */
2180export function getCharIndex(char) {
2181 if (!char) throw new Error('Character is undefined');
2182 const index = characters.findIndex(c => c.avatar === char.avatar);
2183 if (index === -1) throw new Error(`Character not found: ${char.avatar}`);
2184 return index;
2185}
public/scripts/variables.js+77 -58
@@ -669,8 +669,8 @@ function deleteGlobalVariable(name) {
669}669}
670670
671/**671/**
672 * Parses a series of numeric values from a string.672 * Parses a series of numeric values from a string or a string array.
673 * @param {string} value A space-separated list of numeric values or variable names673 * @param {string|string[]} value A space-separated list of numeric values or variable names
674 * @param {SlashCommandScope} scope Scope674 * @param {SlashCommandScope} scope Scope
675 * @returns {number[]} An array of numeric values675 * @returns {number[]} An array of numeric values
676 */676 */
@@ -679,11 +679,17 @@ function parseNumericSeries(value, scope = null) {
679 return [value];679 return [value];
680 }680 }
681681
682 const array = value682 /** @type {(string|number)[]} */
683 .split(' ')683 let values = Array.isArray(value) ? value : value.split(' ');
684 .map(i => i.trim())684
685 // If a JSON array was provided as the only value, convert it to an array
686 if (values.length === 1 && typeof values[0] === 'string' && values[0].startsWith('[')) {
687 values = convertValueType(values[0], 'array');
688 }
689
690 const array = values.map(i => typeof i === 'string' ? i.trim() : i)
685 .filter(i => i !== '')691 .filter(i => i !== '')
686 .map(i => isNaN(Number(i)) ? Number(resolveVariable(i, scope)) : Number(i))692 .map(i => isNaN(Number(i)) ? Number(resolveVariable(String(i), scope)) : Number(i))
687 .filter(i => !isNaN(i));693 .filter(i => !isNaN(i));
688694
689 return array;695 return array;
@@ -703,7 +709,7 @@ function performOperation(value, operation, singleOperand = false, scope = null)
703709
704 const result = singleOperand ? operation(array[0]) : operation(array);710 const result = singleOperand ? operation(array[0]) : operation(array);
705711
706 if (isNaN(result) || !isFinite(result)) {712 if (isNaN(result)) {
707 return 0;713 return 0;
708 }714 }
709715
@@ -731,7 +737,7 @@ function maxValuesCallback(args, value) {
731}737}
732738
733function subValuesCallback(args, value) {739function subValuesCallback(args, value) {
734 return performOperation(value, (array) => array[0] - array[1], false, args._scope);740 return performOperation(value, (array) => array.reduce((a, b) => a - b, array.shift() ?? 0), false, args._scope);
735}741}
736742
737function divValuesCallback(args, value) {743function divValuesCallback(args, value) {
@@ -1595,36 +1601,15 @@ export function registerVariableCommands() {
1595 }));1601 }));
1596 SlashCommandParser.addCommandObject(SlashCommand.fromProps({1602 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1597 name: 'add',1603 name: 'add',
1598 callback: (args, /**@type {string[]}*/value) => addValuesCallback(args, value.join(' ')),1604 callback: (args, value) => addValuesCallback(args, value),
1599 returns: 'sum of the provided values',1605 returns: 'sum of the provided values',
1600 unnamedArgumentList: [1606 unnamedArgumentList: [
1601 SlashCommandArgument.fromProps({1607 SlashCommandArgument.fromProps({
1602 description: 'values to sum',1608 description: 'values to sum',
1603 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1609 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
1604 isRequired: true,1610 isRequired: true,
1605 acceptsMultiple: true,1611 acceptsMultiple: true,
1606 enumProvider: (executor, scope) => {1612 enumProvider: commonEnumProviders.numbersAndVariables,
1607 const vars = commonEnumProviders.variables('all')(executor, scope);
1608 vars.push(
1609 new SlashCommandEnumValue(
1610 'any variable name',
1611 null,
1612 enumTypes.variable,
1613 enumIcons.variable,
1614 (input) => /^\w*$/.test(input),
1615 (input) => input,
1616 ),
1617 new SlashCommandEnumValue(
1618 'any number',
1619 null,
1620 enumTypes.number,
1621 enumIcons.number,
1622 (input) => input == '' || !Number.isNaN(Number(input)),
1623 (input) => input,
1624 ),
1625 );
1626 return vars;
1627 },
1628 forceEnum: false,1613 forceEnum: false,
1629 }),1614 }),
1630 ],1615 ],
@@ -1632,7 +1617,9 @@ export function registerVariableCommands() {
1632 helpString: `1617 helpString: `
1633 <div>1618 <div>
1634 Performs an addition of the set of values and passes the result down the pipe.1619 Performs an addition of the set of values and passes the result down the pipe.
1635 Can use variable names.1620 </div>
1621 <div>
1622 Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
1636 </div>1623 </div>
1637 <div>1624 <div>
1638 <strong>Example:</strong>1625 <strong>Example:</strong>
@@ -1640,6 +1627,9 @@ export function registerVariableCommands() {
1640 <li>1627 <li>
1641 <pre><code class="language-stscript">/add 10 i 30 j</code></pre>1628 <pre><code class="language-stscript">/add 10 i 30 j</code></pre>
1642 </li>1629 </li>
1630 <li>
1631 <pre><code class="language-stscript">/add ["count", 15, 2, "i"]</code></pre>
1632 </li>
1643 </ul>1633 </ul>
1644 </div>1634 </div>
1645 `,1635 `,
@@ -1651,16 +1641,20 @@ export function registerVariableCommands() {
1651 unnamedArgumentList: [1641 unnamedArgumentList: [
1652 SlashCommandArgument.fromProps({1642 SlashCommandArgument.fromProps({
1653 description: 'values to multiply',1643 description: 'values to multiply',
1654 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1644 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
1655 isRequired: true,1645 isRequired: true,
1656 acceptsMultiple: true,1646 acceptsMultiple: true,
1657 enumProvider: commonEnumProviders.variables('all'),1647 enumProvider: commonEnumProviders.numbersAndVariables,
1658 forceEnum: false,1648 forceEnum: false,
1659 }),1649 }),
1660 ],1650 ],
1651 splitUnnamedArgument: true,
1661 helpString: `1652 helpString: `
1662 <div>1653 <div>
1663 Performs a multiplication of the set of values and passes the result down the pipe. Can use variable names.1654 Performs a multiplication of the set of values and passes the result down the pipe.
1655 </div>
1656 <div>
1657 Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
1664 </div>1658 </div>
1665 <div>1659 <div>
1666 <strong>Examples:</strong>1660 <strong>Examples:</strong>
@@ -1668,6 +1662,9 @@ export function registerVariableCommands() {
1668 <li>1662 <li>
1669 <pre><code class="language-stscript">/mul 10 i 30 j</code></pre>1663 <pre><code class="language-stscript">/mul 10 i 30 j</code></pre>
1670 </li>1664 </li>
1665 <li>
1666 <pre><code class="language-stscript">/mul ["count", 15, 2, "i"]</code></pre>
1667 </li>
1671 </ul>1668 </ul>
1672 </div>1669 </div>
1673 `,1670 `,
@@ -1679,16 +1676,20 @@ export function registerVariableCommands() {
1679 unnamedArgumentList: [1676 unnamedArgumentList: [
1680 SlashCommandArgument.fromProps({1677 SlashCommandArgument.fromProps({
1681 description: 'values to find the max',1678 description: 'values to find the max',
1682 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1679 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
1683 isRequired: true,1680 isRequired: true,
1684 acceptsMultiple: true,1681 acceptsMultiple: true,
1685 enumProvider: commonEnumProviders.variables('all'),1682 enumProvider: commonEnumProviders.numbersAndVariables,
1686 forceEnum: false,1683 forceEnum: false,
1687 }),1684 }),
1688 ],1685 ],
1686 splitUnnamedArgument: true,
1689 helpString: `1687 helpString: `
1690 <div>1688 <div>
1691 Returns the maximum value of the set of values and passes the result down the pipe. Can use variable names.1689 Returns the maximum value of the set of values and passes the result down the pipe.
1690 </div>
1691 <div>
1692 Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
1692 </div>1693 </div>
1693 <div>1694 <div>
1694 <strong>Examples:</strong>1695 <strong>Examples:</strong>
@@ -1696,6 +1697,9 @@ export function registerVariableCommands() {
1696 <li>1697 <li>
1697 <pre><code class="language-stscript">/max 10 i 30 j</code></pre>1698 <pre><code class="language-stscript">/max 10 i 30 j</code></pre>
1698 </li>1699 </li>
1700 <li>
1701 <pre><code class="language-stscript">/max ["count", 15, 2, "i"]</code></pre>
1702 </li>
1699 </ul>1703 </ul>
1700 </div>1704 </div>
1701 `,1705 `,
@@ -1707,17 +1711,20 @@ export function registerVariableCommands() {
1707 unnamedArgumentList: [1711 unnamedArgumentList: [
1708 SlashCommandArgument.fromProps({1712 SlashCommandArgument.fromProps({
1709 description: 'values to find the min',1713 description: 'values to find the min',
1710 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1714 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
1711 isRequired: true,1715 isRequired: true,
1712 acceptsMultiple: true,1716 acceptsMultiple: true,
1713 enumProvider: commonEnumProviders.variables('all'),1717 enumProvider: commonEnumProviders.numbersAndVariables,
1714 forceEnum: false,1718 forceEnum: false,
1715 }),1719 }),
1716 ],1720 ],
1721 splitUnnamedArgument: true,
1717 helpString: `1722 helpString: `
1718 <div>1723 <div>
1719 Returns the minimum value of the set of values and passes the result down the pipe.1724 Returns the minimum value of the set of values and passes the result down the pipe.
1720 Can use variable names.1725 </div>
1726 <div>
1727 Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
1721 </div>1728 </div>
1722 <div>1729 <div>
1723 <strong>Example:</strong>1730 <strong>Example:</strong>
@@ -1725,6 +1732,9 @@ export function registerVariableCommands() {
1725 <li>1732 <li>
1726 <pre><code class="language-stscript">/min 10 i 30 j</code></pre>1733 <pre><code class="language-stscript">/min 10 i 30 j</code></pre>
1727 </li>1734 </li>
1735 <li>
1736 <pre><code class="language-stscript">/min ["count", 15, 2, "i"]</code></pre>
1737 </li>
1728 </ul>1738 </ul>
1729 </div>1739 </div>
1730 `,1740 `,
@@ -1735,18 +1745,21 @@ export function registerVariableCommands() {
1735 returns: 'difference of the provided values',1745 returns: 'difference of the provided values',
1736 unnamedArgumentList: [1746 unnamedArgumentList: [
1737 SlashCommandArgument.fromProps({1747 SlashCommandArgument.fromProps({
1738 description: 'values to find the difference',1748 description: 'values to subtract, starting form the first provided value',
1739 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1749 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
1740 isRequired: true,1750 isRequired: true,
1741 acceptsMultiple: true,1751 acceptsMultiple: true,
1742 enumProvider: commonEnumProviders.variables('all'),1752 enumProvider: commonEnumProviders.numbersAndVariables,
1743 forceEnum: false,1753 forceEnum: false,
1744 }),1754 }),
1745 ],1755 ],
1756 splitUnnamedArgument: true,
1746 helpString: `1757 helpString: `
1747 <div>1758 <div>
1748 Performs a subtraction of the set of values and passes the result down the pipe.1759 Performs a subtraction of the set of values and passes the result down the pipe.
1749 Can use variable names.1760 </div>
1761 <div>
1762 Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
1750 </div>1763 </div>
1751 <div>1764 <div>
1752 <strong>Example:</strong>1765 <strong>Example:</strong>
@@ -1754,6 +1767,9 @@ export function registerVariableCommands() {
1754 <li>1767 <li>
1755 <pre><code class="language-stscript">/sub i 5</code></pre>1768 <pre><code class="language-stscript">/sub i 5</code></pre>
1756 </li>1769 </li>
1770 <li>
1771 <pre><code class="language-stscript">/sub ["count", 4, "i"]</code></pre>
1772 </li>
1757 </ul>1773 </ul>
1758 </div>1774 </div>
1759 `,1775 `,
@@ -1767,17 +1783,18 @@ export function registerVariableCommands() {
1767 description: 'dividend',1783 description: 'dividend',
1768 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1784 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1769 isRequired: true,1785 isRequired: true,
1770 enumProvider: commonEnumProviders.variables('all'),1786 enumProvider: commonEnumProviders.numbersAndVariables,
1771 forceEnum: false,1787 forceEnum: false,
1772 }),1788 }),
1773 SlashCommandArgument.fromProps({1789 SlashCommandArgument.fromProps({
1774 description: 'divisor',1790 description: 'divisor',
1775 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1791 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1776 isRequired: true,1792 isRequired: true,
1777 enumProvider: commonEnumProviders.variables('all'),1793 enumProvider: commonEnumProviders.numbersAndVariables,
1778 forceEnum: false,1794 forceEnum: false,
1779 }),1795 }),
1780 ],1796 ],
1797 splitUnnamedArgument: true,
1781 helpString: `1798 helpString: `
1782 <div>1799 <div>
1783 Performs a division of two values and passes the result down the pipe.1800 Performs a division of two values and passes the result down the pipe.
@@ -1802,17 +1819,18 @@ export function registerVariableCommands() {
1802 description: 'dividend',1819 description: 'dividend',
1803 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1820 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1804 isRequired: true,1821 isRequired: true,
1805 enumProvider: commonEnumProviders.variables('all'),1822 enumProvider: commonEnumProviders.numbersAndVariables,
1806 forceEnum: false,1823 forceEnum: false,
1807 }),1824 }),
1808 SlashCommandArgument.fromProps({1825 SlashCommandArgument.fromProps({
1809 description: 'divisor',1826 description: 'divisor',
1810 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1827 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1811 isRequired: true,1828 isRequired: true,
1812 enumProvider: commonEnumProviders.variables('all'),1829 enumProvider: commonEnumProviders.numbersAndVariables,
1813 forceEnum: false,1830 forceEnum: false,
1814 }),1831 }),
1815 ],1832 ],
1833 splitUnnamedArgument: true,
1816 helpString: `1834 helpString: `
1817 <div>1835 <div>
1818 Performs a modulo operation of two values and passes the result down the pipe.1836 Performs a modulo operation of two values and passes the result down the pipe.
@@ -1837,17 +1855,18 @@ export function registerVariableCommands() {
1837 description: 'base',1855 description: 'base',
1838 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1856 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1839 isRequired: true,1857 isRequired: true,
1840 enumProvider: commonEnumProviders.variables('all'),1858 enumProvider: commonEnumProviders.numbersAndVariables,
1841 forceEnum: false,1859 forceEnum: false,
1842 }),1860 }),
1843 SlashCommandArgument.fromProps({1861 SlashCommandArgument.fromProps({
1844 description: 'exponent',1862 description: 'exponent',
1845 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1863 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1846 isRequired: true,1864 isRequired: true,
1847 enumProvider: commonEnumProviders.variables('all'),1865 enumProvider: commonEnumProviders.numbersAndVariables,
1848 forceEnum: false,1866 forceEnum: false,
1849 }),1867 }),
1850 ],1868 ],
1869 splitUnnamedArgument: true,
1851 helpString: `1870 helpString: `
1852 <div>1871 <div>
1853 Performs a power operation of two values and passes the result down the pipe.1872 Performs a power operation of two values and passes the result down the pipe.
@@ -1872,7 +1891,7 @@ export function registerVariableCommands() {
1872 description: 'value',1891 description: 'value',
1873 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1892 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1874 isRequired: true,1893 isRequired: true,
1875 enumProvider: commonEnumProviders.variables('all'),1894 enumProvider: commonEnumProviders.numbersAndVariables,
1876 forceEnum: false,1895 forceEnum: false,
1877 }),1896 }),
1878 ],1897 ],
@@ -1900,7 +1919,7 @@ export function registerVariableCommands() {
1900 description: 'value',1919 description: 'value',
1901 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1920 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1902 isRequired: true,1921 isRequired: true,
1903 enumProvider: commonEnumProviders.variables('all'),1922 enumProvider: commonEnumProviders.numbersAndVariables,
1904 forceEnum: false,1923 forceEnum: false,
1905 }),1924 }),
1906 ],1925 ],
@@ -1929,7 +1948,7 @@ export function registerVariableCommands() {
1929 description: 'value',1948 description: 'value',
1930 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1949 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1931 isRequired: true,1950 isRequired: true,
1932 enumProvider: commonEnumProviders.variables('all'),1951 enumProvider: commonEnumProviders.numbersAndVariables,
1933 forceEnum: false,1952 forceEnum: false,
1934 }),1953 }),
1935 ],1954 ],
@@ -1957,7 +1976,7 @@ export function registerVariableCommands() {
1957 description: 'value',1976 description: 'value',
1958 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1977 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1959 isRequired: true,1978 isRequired: true,
1960 enumProvider: commonEnumProviders.variables('all'),1979 enumProvider: commonEnumProviders.numbersAndVariables,
1961 forceEnum: false,1980 forceEnum: false,
1962 }),1981 }),
1963 ],1982 ],
@@ -1985,7 +2004,7 @@ export function registerVariableCommands() {
1985 description: 'value',2004 description: 'value',
1986 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],2005 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1987 isRequired: true,2006 isRequired: true,
1988 enumProvider: commonEnumProviders.variables('all'),2007 enumProvider: commonEnumProviders.numbersAndVariables,
1989 forceEnum: false,2008 forceEnum: false,
1990 }),2009 }),
1991 ],2010 ],
@@ -2013,7 +2032,7 @@ export function registerVariableCommands() {
2013 description: 'value',2032 description: 'value',
2014 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],2033 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
2015 isRequired: true,2034 isRequired: true,
2016 enumProvider: commonEnumProviders.variables('all'),2035 enumProvider: commonEnumProviders.numbersAndVariables,
2017 forceEnum: false,2036 forceEnum: false,
2018 }),2037 }),
2019 ],2038 ],
public/style.css+12 -3
@@ -978,15 +978,15 @@ body .panelControlBar {
978 justify-content: center;978 justify-content: center;
979 z-index: 9999;979 z-index: 9999;
980 grid-row-start: 2;980 grid-row-start: 2;
981 grid-column-start: 4;
982 flex-flow: column;
983 font-size: 30px;981 font-size: 30px;
984 cursor: pointer;982 cursor: pointer;
985 align-self: center;983 align-self: center;
986 position: absolute;984 position: absolute;
987 bottom: 15px;985bottom: 15px;
986flex-flow: column;
988}987}
989988
989
990.swipes-counter {990.swipes-counter {
991 color: var(--SmartThemeBodyColor);991 color: var(--SmartThemeBodyColor);
992 font-size: 12px;992 font-size: 12px;
@@ -994,6 +994,9 @@ body .panelControlBar {
994 font-family: var(--mainFontFamily);994 font-family: var(--mainFontFamily);
995 font-weight: 400;995 font-weight: 400;
996 align-self: center;996 align-self: center;
997 min-width: 40px;
998 display: flex;
999 justify-content: center;
997}1000}
9981001
999.swipe_left {1002.swipe_left {
@@ -1003,6 +1006,7 @@ body .panelControlBar {
10031006
1004.swipe_right {1007.swipe_right {
1005 right: 5px;1008 right: 5px;
1009 align-self:end;
1006}1010}
10071011
1008.ui-settings {1012.ui-settings {
@@ -2634,6 +2638,11 @@ select option:not(:checked) {
2634 color: var(--active) !important;2638 color: var(--active) !important;
2635}2639}
26362640
2641#instruct_enabled_label .menu_button:not(.toggleEnabled),
2642#sysprompt_enabled_label .menu_button:not(.toggleEnabled) {
2643 color: Red;
2644}
2645
2637.displayBlock {2646.displayBlock {
2638 display: block !important;2647 display: block !important;
2639}2648}