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 @@
32483248 </div>
32493249 </div>
32503250 <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>
32513269 <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
32713272 <div class="flex-container" title="Select your current Instruct Template" data-i18n="[title]Select your current Instruct Template">
32723273 <select id="instruct_presets" data-preset-manager-for="instruct" class="flex1 text_pole"></select>
@@ -3431,21 +3432,22 @@
34313432 </div>
34323433 </div>
34333434 <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>
34343449 <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
34503452 <div class="flex-container" title="Select your current System Prompt" data-i18n="[title]Select your current System Prompt">
34513453 <select id="sysprompt_select" data-preset-manager-for="sysprompt" class="flex1 text_pole"></select>
@@ -5939,9 +5941,9 @@
59395941 </div>
59405942 <div class="mes_bias"></div>
59415943 </div>
59425944 <div class="swipe_right fa-solid fa-chevron-right" style="display: none;">
59435945 <div class="swipes-counter"></div>
59445946 </div>
59455947 </div>
59465948 </div>
59475949 <div id="group_avatars_template" class="template_element">
public/script.js+14 -2
@@ -230,7 +230,7 @@ import { MacrosParser, evaluateMacros, getLastMessageId } from './scripts/macros
230230import { currentUser, setUserControls } from './scripts/user.js';
231231import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';
232232import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';
233233import { initScrapers, ScraperManager } from './scripts/scrapers.js';
234234import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js';
235235import { SlashCommand } from './scripts/slash-commands/SlashCommand.js';
236236import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './scripts/slash-commands/SlashCommandArgument.js';
@@ -959,6 +959,7 @@ async function firstLoadInit() {
959959 initCfg();
960960 initLogprobs();
961961 initInputMarkdown();
962+ await initScrapers();
962963 doDailyExtensionUpdatesCheck();
963964 await hideLoader();
964965 await fixViewport();
@@ -5427,6 +5428,7 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
54275428 getMessage = getMessage.substring(0, getMessage.indexOf('<|endoftext|>'));
54285429 }
54295430 const isInstruct = power_user.instruct.enabled && main_api !== 'openai';
5431+ const isNotEmpty = (str) => str && str.trim() !== '';
54305432 if (isInstruct && power_user.instruct.stop_sequence) {
54315433 if (getMessage.indexOf(power_user.instruct.stop_sequence) != -1) {
54325434 getMessage = getMessage.substring(0, getMessage.indexOf(power_user.instruct.stop_sequence));
@@ -5434,7 +5436,7 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
54345436 }
54355437 // Hana: Only use the first sequence (should be <|model|>)
54365438 // of the prompt before <|user|> (as KoboldAI Lite does it).
54375439 if (isInstruct && isNotEmpty(power_user.instruct.input_sequence)) {
54385440 if (getMessage.indexOf(power_user.instruct.input_sequence) != -1) {
54395441 getMessage = getMessage.substring(0, getMessage.indexOf(power_user.instruct.input_sequence));
54405442 }
@@ -8476,12 +8478,22 @@ const CONNECT_API_MAP = {
84768478 selected: 'novel',
84778479 button: '#api_button_novel',
84788480 },
8481+ 'koboldcpp': {
8482+ selected: 'textgenerationwebui',
8483+ button: '#api_button_textgenerationwebui',
8484+ type: textgen_types.KOBOLDCPP,
8485+ },
84798486 // KoboldCpp alias
84808487 'kcpp': {
84818488 selected: 'textgenerationwebui',
84828489 button: '#api_button_textgenerationwebui',
84838490 type: textgen_types.KOBOLDCPP,
84848491 },
8492+ 'openai': {
8493+ selected: 'openai',
8494+ button: '#api_button_openai',
8495+ source: chat_completion_sources.OPENAI,
8496+ },
84858497 // OpenAI alias
84868498 'oai': {
84878499 selected: 'openai',
public/scripts/RossAscends-mods.js+17 -16
@@ -56,22 +56,25 @@ let counterNonce = Date.now();
5656
5757const observerConfig = { childList: true, subtree: true };
5858const countTokensDebounced = debounce(RA_CountCharTokens, debounce_timeout.relaxed);
59+const countTokensShortDebounced = debounce(RA_CountCharTokens, debounce_timeout.short);
60+const checkStatusDebounced = debounce(RA_checkOnlineStatus, debounce_timeout.short);
5961
6062const observer = new MutationObserver(function (mutations) {
6163 mutations.forEach(function (mutation) {
64+ if (!(mutation.target instanceof HTMLElement)) {
65+ return;
66+ }
6267 if (mutation.target.classList.contains('online_status_text')) {
6368 RA_checkOnlineStatuscheckStatusDebounced();
6469 } else if (mutation.target.parentNode === SelectedCharacterTab) {
6570 setTimeoutcountTokensShortDebounced(RA_CountCharTokens, 200);
6671 } 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- }
7578 }
7679 }
7780 });
@@ -159,8 +162,8 @@ export function shouldSendOnEnter() {
159162export function humanizedDateTime() {
160163 const now = new Date(Date.now());
161164 const dt = {
162165 year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate(),
163166 hour: now.getHours(), minute: now.getMinutes(), second: now.getSeconds(),
164167 };
165168 for (const key in dt) {
166169 dt[key] = dt[key].toString().padStart(2, '0');
@@ -725,9 +728,7 @@ export function addSafariPatch() {
725728
726729export function initRossMods() {
727730 // initial status check
728- setTimeout(() => {
731+ checkStatusDebounced();
729- RA_checkOnlineStatus();
730- }, 100);
731732
732733 if (power_user.auto_load_chat) {
733734 RA_autoloadchat();
@@ -752,7 +753,7 @@ export function initRossMods() {
752753 setTimeout(() => RA_autoconnect(PrevAPI), 100);
753754 });
754755
755756 $('#api_button').clickon(function'click', () {=> setTimeoutcheckStatusDebounced(RA_checkOnlineStatus, 100); });
756757
757758 //toggle pin class when lock toggle clicked
758759 $(RPanelPin).on('click', function () {
public/scripts/extensions/expressions/index.js+9 -3
@@ -2,7 +2,7 @@ import { callPopup, eventSource, event_types, generateRaw, getRequestHeaders, ma
22import { dragElement, isMobile } from '../../RossAscends-mods.js';
33import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
44import { loadMovingUIState, power_user } from '../../power-user.js';
55import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition, findChar } from '../../utils.js';
66import { hideMutedSprites } from '../../group-chats.js';
77import { isJsonSchemaSupported } from '../../textgen-settings.js';
88import { debounce_timeout } from '../../constants.js';
@@ -2105,14 +2105,20 @@ function migrateSettings() {
21052105 }));
21062106 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
21072107 name: 'lastsprite',
21082108 callback: (_, valuename) => lastExpression[String(value).trim()] ?? '',{
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+ },
21092114 returns: 'the last set sprite / expression for the named character.',
21102115 unnamedArgumentList: [
21112116 SlashCommandArgument.fromProps({
21122117 description: 'characterCharacter name - or unique character identifier (avatar key)',
21132118 typeList: [ARGUMENT_TYPE.STRING],
21142119 isRequired: true,
21152120 enumProvider: commonEnumProviders.characters('character'),
2121+ forceEnum: true,
21162122 }),
21172123 ],
21182124 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({
441441 description: 'character name',
442442 typeList: [ARGUMENT_TYPE.STRING],
443443 enumProvider: commonEnumProviders.characters('character'),
444+ forceEnum: true,
444445 }),
445446 SlashCommandNamedArgument.fromProps({
446447 name: 'group',
public/scripts/scrapers.js+16 -7
@@ -13,6 +13,7 @@ import { isValidUrl } from './utils.js';
1313 * @property {string} description
1414 * @property {string} iconClass
1515 * @property {boolean} iconAvailable
16+ * @property {() => Promise<void>} [init=null]
1617 * @property {() => Promise<boolean>} isAvailable
1718 * @property {() => Promise<File[]>} scrape
1819 */
@@ -36,12 +37,16 @@ export class ScraperManager {
3637 * Register a scraper to be used by the Data Bank.
3738 * @param {Scraper} scraper Instance of a scraper to register
3839 */
3940 static async registerDataBankScraper(scraper) {
4041 if (ScraperManager.#scrapers.some(s => s.id === scraper.id)) {
4142 console.warn(`Scraper with ID ${scraper.id} already registered`);
4243 return;
4344 }
4445
46+ if (scraper.init) {
47+ await scraper.init();
48+ }
49+
4550 ScraperManager.#scrapers.push(scraper);
4651 }
4752
@@ -462,7 +467,9 @@ class YouTubeScraper {
462467 this.description = 'Download a transcript from a YouTube video.';
463468 this.iconClass = 'fa-brands fa-youtube';
464469 this.iconAvailable = true;
470+ }
465471
472+ async init() {
466473 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
467474 name: 'yt-script',
468475 callback: async (args, url) => {
@@ -564,9 +571,11 @@ class YouTubeScraper {
564571 }
565572}
566573
567-ScraperManager.registerDataBankScraper(new FileScraper());
574+export async function initScrapers() {
568575 await ScraperManager.registerDataBankScraper(new NotepadFileScraper());
569576 await ScraperManager.registerDataBankScraper(new WebScraperNotepad());
570577 await ScraperManager.registerDataBankScraper(new MediaWikiScraperWebScraper());
571578 await ScraperManager.registerDataBankScraper(new FandomScraperMediaWikiScraper());
572579 await ScraperManager.registerDataBankScraper(new YouTubeScraperFandomScraper());
580+ await ScraperManager.registerDataBankScraper(new YouTubeScraper());
581+}
public/scripts/slash-commands.js+225 -108
@@ -55,7 +55,7 @@ import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockStat
5555import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
5656import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
5757import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';
5858import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
5959import { registerVariableCommands, resolveVariable } from './variables.js';
6060import { background_settings } from './backgrounds.js';
6161import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
@@ -68,7 +68,6 @@ import { SlashCommandNamedArgumentAssignment } from './slash-commands/SlashComma
6868import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
6969import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
7070import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
71-import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js';
7271import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
7372import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
7473export {
@@ -174,20 +173,96 @@ export function initDefaultSlashCommands() {
174173 `,
175174 }));
176175 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({
177243 name: 'sendas',
178244 callback: sendMessageAs,
179245 namedArgumentList: [
180246 SlashCommandNamedArgument.fromProps({
181247 name: 'name',
182248 description: 'Character name - or unique character identifier (avatar key)',
183249 typeList: [ARGUMENT_TYPE.STRING],
184250 isRequired: true,
185251 enumProvider: commonEnumProviders.characters('character'),
186252 forceEnum: false,
187253 }),
188254 new 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+ }),
191266 SlashCommandNamedArgument.fromProps({
192267 name: 'at',
193268 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() {
211286 <pre><code>/sendas name="Chloe" Hello, guys!</code></pre>
212287 will send "Hello, guys!" from "Chloe".
213288 </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>
214293 </ul>
215294 </div>
216295 <div>
@@ -381,12 +460,14 @@ export function initDefaultSlashCommands() {
381460 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
382461 name: 'go',
383462 callback: goToCharacterCallback,
463+ returns: 'The character/group name',
384464 unnamedArgumentList: [
385465 SlashCommandArgument.fromProps({
386- description: 'name',
466+ description: 'Character name - or unique character identifier (avatar key)',
387467 typeList: [ARGUMENT_TYPE.STRING],
388468 isRequired: true,
389469 enumProvider: commonEnumProviders.characters('all'),
470+ forceEnum: true,
390471 }),
391472 ],
392473 helpString: 'Opens up a chat with the character or group by its name',
@@ -432,7 +513,7 @@ export function initDefaultSlashCommands() {
432513 namedArgumentList: [
433514 SlashCommandNamedArgument.fromProps({
434515 name: 'name',
435516 description: 'characterCharacter name - or unique character identifier (avatar key)',
436517 typeList: [ARGUMENT_TYPE.STRING],
437518 isRequired: true,
438519 enumProvider: commonEnumProviders.characters('character'),
@@ -451,7 +532,7 @@ export function initDefaultSlashCommands() {
451532 namedArgumentList: [],
452533 unnamedArgumentList: [
453534 SlashCommandArgument.fromProps({
454- description: 'name',
535+ description: 'Character name - or unique character identifier (avatar key)',
455536 typeList: [ARGUMENT_TYPE.STRING],
456537 isRequired: true,
457538 enumProvider: commonEnumProviders.characters('character'),
@@ -618,7 +699,7 @@ export function initDefaultSlashCommands() {
618699 aliases: ['addmember', 'memberadd'],
619700 unnamedArgumentList: [
620701 SlashCommandArgument.fromProps({
621702 description: 'characterCharacter name - or unique character identifier (avatar key)',
622703 typeList: [ARGUMENT_TYPE.STRING],
623704 isRequired: true,
624705 enumProvider: () => selected_group ? commonEnumProviders.characters('character')() : [],
@@ -856,7 +937,7 @@ export function initDefaultSlashCommands() {
856937 ),
857938 SlashCommandNamedArgument.fromProps({
858939 name: 'name',
859940 description: 'in-prompt character name for instruct mode (or unique character identifier (avatar key), which will be used as name)',
860941 typeList: [ARGUMENT_TYPE.STRING],
861942 defaultValue: 'System',
862943 enumProvider: () => [...commonEnumProviders.characters('character')(), new SlashCommandEnumValue('System', null, enumTypes.enum, enumIcons.assistant)],
@@ -2293,7 +2374,8 @@ async function generateCallback(args, value) {
22932374
22942375 setEphemeralStopStrings(resolveVariable(args?.stop));
22952376 const name = args?.name;
22962377 const resultchar = await generateQuietPromptfindChar(value, quietToLoud, false,{ '',name: name, length});
2378+ const result = await generateQuietPrompt(value, quietToLoud, false, '', char?.name ?? name, length);
22972379 return result;
22982380 } catch (err) {
22992381 console.error('Error on /gen generation', err);
@@ -2481,26 +2563,22 @@ async function askCharacter(args, text) {
24812563 return '';
24822564 }
24832565
2484- let name = '';
2566+ if (!args.name) {
2485-
2567+ 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- }
24932569 }
24942570
24952571 const prevChId = this_chid;
24962572
24972573 // 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) {
25002576 toastr.error('Character not found.');
25012577 return '';
25022578 }
25032579
2580+ const chId = getCharIndex(character);
2581+
25042582 if (text) {
25052583 const mesText = getRegexedString(text.trim(), regex_placement.SLASH_COMMAND);
25062584 // 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) {
25112589 // Override character and send a user message
25122590 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
25282596 const restoreCharacter = () => {
25292597 if (String(this_chid) !== String(chId)) {
25302598 return;
25312599 }
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
25362609 // Only force the new avatar if the character name is the same
25372610 // This skips if an error was fired
25382611 const lastMessage = chat[chat.length - 1];
25392612 if (lastMessage && lastMessage?.name === character.name) {
25402613 lastMessage.force_avatar = force_avatar;
25412614 lastMessage.original_avatar = original_avatar;
25422615 }
@@ -2547,7 +2620,7 @@ async function askCharacter(args, text) {
25472620 // Run generate and restore previous character
25482621 try {
25492622 eventSource.once(event_types.MESSAGE_RECEIVED, restoreCharacter);
25502623 toastr.info(`Asking ${character.name} something...`);
25512624 askResult = await Generate('ask_command');
25522625 } catch (error) {
25532626 restoreCharacter();
@@ -2741,26 +2814,23 @@ async function removeGroupMemberCallback(_, arg) {
27412814 return '';
27422815}
27432816
27442817async function addGroupMemberCallback(_, argname) {
27452818 if (!selected_group) {
27462819 toastr.warning('Cannot run /memberadd command outside of a group chat.');
27472820 return '';
27482821 }
27492822
27502823 if (!argname) {
27512824 console.warn('WARN: No argument provided for /memberadd command');
27522825 return '';
27532826 }
27542827
2755- arg = arg.trim();
2828+ const character = findChar({ name: name, preferCurrentChar: false });
2756- const chid = findCharacterIndex(arg);
2829+ if (!character) {
2757-
2830+ console.warn(`WARN: No character found for argument ${name}`);
2758- if (chid === -1) {
2759- console.warn(`WARN: No character found for argument ${arg}`);
27602831 return '';
27612832 }
27622833
2763- const character = characters[chid];
27642834 const group = groups.find(x => x.id === selected_group);
27652835
27662836 if (!group || !Array.isArray(group.members)) {
@@ -2829,7 +2899,7 @@ function findPersonaByName(name) {
28292899 }
28302900
28312901 for (const persona of Object.entries(power_user.personas)) {
28322902 if (equalsIgnoreCaseAndAccents(persona[1].toLowerCase() ===, name.toLowerCase()) {
28332903 return persona[0];
28342904 }
28352905 }
@@ -2872,7 +2942,9 @@ async function deleteMessagesByNameCallback(_, name) {
28722942 return;
28732943 }
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
28772949 const messagesToDelete = [];
28782950 chat.forEach((value) => {
@@ -2901,60 +2973,34 @@ async function deleteMessagesByNameCallback(_, name) {
29012973 return '';
29022974}
29032975
2904-function 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-
29272976async function goToCharacterCallback(_, name) {
29282977 if (!name) {
29292978 console.warn('WARN: No character name provided for /go command');
29302979 return;
29312980 }
29322981
2933- name = name.trim();
2982+ const character = findChar({ name: name });
2934- const characterIndex = findCharacterIndex(name);
2983+ if (character) {
2935-
2984+ 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);
29392987 setActiveGroup(null);
29402988 return characters[characterIndex]?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- }
29522989 }
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 '';
29532999}
29543000
29553001async function openChat(idchid) {
29563002 resetSelectedGroup();
29573003 setCharacterId(idchid);
29583004 await delay(1);
29593005 await reloadCurrentChat();
29603006}
@@ -3100,6 +3146,79 @@ async function setNarratorName(_, text) {
31003146 return '';
31013147}
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+ */
3158+export 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+ */
3177+export 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+ */
3198+export 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+
31033222export async function sendMessageAs(args, text) {
31043223 if (!text) {
31053224 return '';
@@ -3138,26 +3257,18 @@ export async function sendMessageAs(args, text) {
31383257 const isSystem = bias && !removeMacros(mesText).length;
31393258 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 avatars
3263+ 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;
31573266 }
31583267
3268+ const { name: avatarCharName, force_avatar, original_avatar } = getNameAndAvatarForMessage(avatarCharacter, name);
3269+
31593270 const message = {
3160- name: name,
3271+ name: character?.name || name || avatarCharName,
31613272 is_user: false,
31623273 is_system: isSystem,
31633274 send_date: getMessageTimeStamp(),
@@ -3561,11 +3672,17 @@ function setPromptEntryCallback(args, targetState) {
35613672 const prompts = promptManager.serviceSettings.prompts;
35623673
35633674 function parseArgs(arg) {
3675+ // Arg is already an array
3676+ if (Array.isArray(arg)) {
3677+ return arg;
3678+ }
35643679 const list = [];
35653680 try {
3681+ // Arg is a JSON-stringified array
35663682 const parsedArg = JSON.parse(arg);
35673683 list.push(...Array.isArray(parsedArg) ? parsedArg : [arg]);
35683684 } catch {
3685+ // Arg is a string
35693686 list.push(arg);
35703687 }
35713688 return list;
public/scripts/slash-commands/SlashCommand.js+2 -2
@@ -15,13 +15,13 @@ import { SlashCommandScope } from './SlashCommandScope.js';
1515 * _abortController:SlashCommandAbortController,
1616 * _debugController:SlashCommandDebugController,
1717 * _hasUnnamedArgument:boolean,
1818 * [id:string]:string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined,
1919 * }} NamedArguments
2020 */
2121
2222/**
2323 * Alternative object for local JSDocs, where you don't need existing pipe, scope, etc. arguments
2424 * @typedef {{[id:string]:string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined}} NamedArgumentsCapture
2525 */
2626
2727/**
public/scripts/slash-commands/SlashCommandClosure.js+60 -11
@@ -2,6 +2,7 @@ import { substituteParams } from '../../script.js';
22import { delay, escapeRegex, uuidv4 } from '../utils.js';
33import { SlashCommand } from './SlashCommand.js';
44import { SlashCommandAbortController } from './SlashCommandAbortController.js';
5+import { SlashCommandNamedArgument } from './SlashCommandArgument.js';
56import { SlashCommandBreak } from './SlashCommandBreak.js';
67import { SlashCommandBreakController } from './SlashCommandBreakController.js';
78import { SlashCommandBreakPoint } from './SlashCommandBreakPoint.js';
@@ -53,7 +54,7 @@ export class SlashCommandClosure {
5354 *
5455 * @param {string} text
5556 * @param {SlashCommandScope} scope
56- * @returns
57+ * @returns {string|SlashCommandClosure|(string|SlashCommandClosure)[]}
5758 */
5859 substituteParams(text, scope = null) {
5960 let isList = false;
@@ -379,6 +380,52 @@ export class SlashCommandClosure {
379380 * @param {import('./SlashCommand.js').NamedArguments} args
380381 */
381382 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+
382429 // substitute named arguments
383430 for (const arg of executor.namedArgumentList) {
384431 if (arg.value instanceof SlashCommandClosure) {
@@ -390,19 +437,12 @@ export class SlashCommandClosure {
390437 closure.debugController = this.debugController;
391438 }
392439 if (closure.executeNow) {
393440 args[assign(arg.name] =, (await closure.execute())?.pipe);
394441 } else {
395442 args[assign(arg.name] =, closure);
396443 }
397444 } else {
398445 args[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- ;
406446 }
407447 }
408448 }
@@ -424,6 +464,7 @@ export class SlashCommandClosure {
424464 } else {
425465 value = [];
426466 for (let i = 0; i < executor.unnamedArgumentList.length; i++) {
467+ /** @type {string|SlashCommandClosure|(string|SlashCommandClosure)[]} */
427468 let v = executor.unnamedArgumentList[i].value;
428469 if (v instanceof SlashCommandClosure) {
429470 /**@type {SlashCommandClosure}*/
@@ -467,6 +508,14 @@ export class SlashCommandClosure {
467508 return v;
468509 });
469510 }
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+
470519 return value;
471520 }
472521
public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js+43 -2
@@ -2,7 +2,7 @@ import { chat_metadata, characters, substituteParams, chat, extension_prompt_rol
22import { extension_settings } from '../extensions.js';
33import { getGroupMembers, groups } from '../group-chats.js';
44import { power_user } from '../power-user.js';
55import { searchCharByName, getTagsList, tags, tag_map } from '../tags.js';
66import { world_names } from '../world-info.js';
77import { SlashCommandClosure } from './SlashCommandClosure.js';
88import { SlashCommandEnumValue, enumTypes } from './SlashCommandEnumValue.js';
@@ -153,6 +153,35 @@ export const commonEnumProviders = {
153153 },
154154
155155 /**
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+ /**
156185 * All possible char entities, like characters and groups. Can be filtered down to just one type.
157186 *
158187 * @param {('all' | 'character' | 'group')?} [mode='all'] - Which type to return
@@ -182,6 +211,18 @@ export const commonEnumProviders = {
182211 personas: () => Object.values(power_user.personas).map(persona => new SlashCommandEnumValue(persona, null, enumTypes.name, enumIcons.persona)),
183212
184213 /**
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+ /**
185226 * All possible tags for a given char/group entity
186227 *
187228 * @param {('all' | 'existing' | 'not-existing')?} [mode='all'] - Which types of tags to show
@@ -193,7 +234,7 @@ export const commonEnumProviders = {
193234 if (charName instanceof SlashCommandClosure) throw new Error('Argument \'name\' does not support closures');
194235 const key = searchCharByName(substituteParams(charName), { suppressLogging: true });
195236 const assigned = key ? getTagsList(key) : [];
196237 return tags.filter(it => !key || mode === 'all' || mode === 'existing' && assigned.includes(it) || mode === 'not-existing' && !assigned.includes(it))
197238 .map(tag => new SlashCommandEnumValue(tag.name, null, enumTypes.command, enumIcons.tag));
198239 },
199240
public/scripts/tags.js+8 -8
@@ -15,7 +15,7 @@ import {
1515import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, FilterHelper } from './filters.js';
1616
1717import { groupCandidatesFilter, groups, selected_group } from './group-chats.js';
1818import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, flashHighlight, equalsIgnoreCaseAndAccents, includesIgnoreCaseAndAccents, removeFromArray, getFreeName, debounce, findChar } from './utils.js';
1919import { power_user } from './power-user.js';
2020import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
2121import { SlashCommand } from './slash-commands/SlashCommand.js';
@@ -50,7 +50,6 @@ export {
5050 removeTagFromMap,
5151};
5252
53-/** @typedef {import('../scripts/popup.js').Popup} Popup */
5453/** @typedef {import('../script.js').Character} Character */
5554
5655const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter';
@@ -507,7 +506,7 @@ export function getTagKeyForEntityElement(element) {
507506 */
508507export function searchCharByName(charName, { suppressLogging = false } = {}) {
509508 const entity = charName
510509 ? (characters.findfindChar(x =>{ x.name ===: charName }) || groups.find(x => equalsIgnoreCaseAndAccents(x.name ==, charName)))
511510 : (selected_group ? groups.find(x => x.id == selected_group) : characters[this_chid]);
512511 const key = getTagKeyForEntity(entity);
513512 if (!key) {
@@ -1861,8 +1860,9 @@ function registerTagsSlashCommands() {
18611860 return String(result);
18621861 },
18631862 namedArgumentList: [
18641863 SlashCommandNamedArgument.fromProps({ name: 'name',
18651864 descriptionname: 'Character name',
1865+ description: 'Character name - or unique character identifier (avatar key)',
18661866 typeList: [ARGUMENT_TYPE.STRING],
18671867 defaultValue: '{{char}}',
18681868 enumProvider: commonEnumProviders.characters(),
@@ -1907,7 +1907,7 @@ function registerTagsSlashCommands() {
19071907 },
19081908 namedArgumentList: [
19091909 SlashCommandNamedArgument.fromProps({ name: 'name',
19101910 description: 'Character name - or unique character identifier (avatar key)',
19111911 typeList: [ARGUMENT_TYPE.STRING],
19121912 defaultValue: '{{char}}',
19131913 enumProvider: commonEnumProviders.characters(),
@@ -1950,7 +1950,7 @@ function registerTagsSlashCommands() {
19501950 namedArgumentList: [
19511951 SlashCommandNamedArgument.fromProps({
19521952 name: 'name',
19531953 description: 'Character name - or unique character identifier (avatar key)',
19541954 typeList: [ARGUMENT_TYPE.STRING],
19551955 defaultValue: '{{char}}',
19561956 enumProvider: commonEnumProviders.characters(),
@@ -1993,7 +1993,7 @@ function registerTagsSlashCommands() {
19931993 namedArgumentList: [
19941994 SlashCommandNamedArgument.fromProps({
19951995 name: 'name',
19961996 description: 'Character name - or unique character identifier (avatar key)',
19971997 typeList: [ARGUMENT_TYPE.STRING],
19981998 defaultValue: '{{char}}',
19991999 enumProvider: commonEnumProviders.characters(),
public/scripts/utils.js+74 -1
@@ -1,10 +1,12 @@
11import { getContext } from './extensions.js';
22import { characters, getRequestHeaders, this_chid } from '../script.js';
33import { isMobile } from './RossAscends-mods.js';
44import { collapseNewlines } from './power-user.js';
55import { debounce_timeout } from './constants.js';
66import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
77import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
8+import { getTagsList } from './tags.js';
9+import { groups, selected_group } from './group-chats.js';
810
911/**
1012 * Pagination status string template.
@@ -2110,3 +2112,74 @@ export async function showFontAwesomePicker(customList = null) {
21102112 }
21112113 return null;
21122114}
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+ */
2127+export 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+ */
2180+export 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) {
669669}
670670
671671/**
672672 * Parses a series of numeric values from a string or a string array.
673673 * @param {string|string[]} value A space-separated list of numeric values or variable names
674674 * @param {SlashCommandScope} scope Scope
675675 * @returns {number[]} An array of numeric values
676676 */
@@ -679,11 +679,17 @@ function parseNumericSeries(value, scope = null) {
679679 return [value];
680680 }
681681
682- const array = value
682+ /** @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)
685691 .filter(i => i !== '')
686692 .map(i => isNaN(Number(i)) ? Number(resolveVariable(String(i), scope)) : Number(i))
687693 .filter(i => !isNaN(i));
688694
689695 return array;
@@ -703,7 +709,7 @@ function performOperation(value, operation, singleOperand = false, scope = null)
703709
704710 const result = singleOperand ? operation(array[0]) : operation(array);
705711
706712 if (isNaN(result) || !isFinite(result)) {
707713 return 0;
708714 }
709715
@@ -731,7 +737,7 @@ function maxValuesCallback(args, value) {
731737}
732738
733739function subValuesCallback(args, value) {
734740 return performOperation(value, (array) => array[0].reduce((a, b) => a - b, array[1].shift() ?? 0), false, args._scope);
735741}
736742
737743function divValuesCallback(args, value) {
@@ -1595,36 +1601,15 @@ export function registerVariableCommands() {
15951601 }));
15961602 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
15971603 name: 'add',
15981604 callback: (args, /**@type {string[]}*/value) => addValuesCallback(args, value.join(' ')),
15991605 returns: 'sum of the provided values',
16001606 unnamedArgumentList: [
16011607 SlashCommandArgument.fromProps({
16021608 description: 'values to sum',
16031609 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
16041610 isRequired: true,
16051611 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- },
16281613 forceEnum: false,
16291614 }),
16301615 ],
@@ -1632,7 +1617,9 @@ export function registerVariableCommands() {
16321617 helpString: `
16331618 <div>
16341619 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).
16361623 </div>
16371624 <div>
16381625 <strong>Example:</strong>
@@ -1640,6 +1627,9 @@ export function registerVariableCommands() {
16401627 <li>
16411628 <pre><code class="language-stscript">/add 10 i 30 j</code></pre>
16421629 </li>
1630+ <li>
1631+ <pre><code class="language-stscript">/add ["count", 15, 2, "i"]</code></pre>
1632+ </li>
16431633 </ul>
16441634 </div>
16451635 `,
@@ -1651,16 +1641,20 @@ export function registerVariableCommands() {
16511641 unnamedArgumentList: [
16521642 SlashCommandArgument.fromProps({
16531643 description: 'values to multiply',
16541644 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
16551645 isRequired: true,
16561646 acceptsMultiple: true,
16571647 enumProvider: commonEnumProviders.variables('all')numbersAndVariables,
16581648 forceEnum: false,
16591649 }),
16601650 ],
1651+ splitUnnamedArgument: true,
16611652 helpString: `
16621653 <div>
16631654 Performs a multiplication of the set of values and passes the result down the pipe. Can use variable names.
1655+ </div>
1656+ <div>
1657+ Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
16641658 </div>
16651659 <div>
16661660 <strong>Examples:</strong>
@@ -1668,6 +1662,9 @@ export function registerVariableCommands() {
16681662 <li>
16691663 <pre><code class="language-stscript">/mul 10 i 30 j</code></pre>
16701664 </li>
1665+ <li>
1666+ <pre><code class="language-stscript">/mul ["count", 15, 2, "i"]</code></pre>
1667+ </li>
16711668 </ul>
16721669 </div>
16731670 `,
@@ -1679,16 +1676,20 @@ export function registerVariableCommands() {
16791676 unnamedArgumentList: [
16801677 SlashCommandArgument.fromProps({
16811678 description: 'values to find the max',
16821679 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
16831680 isRequired: true,
16841681 acceptsMultiple: true,
16851682 enumProvider: commonEnumProviders.variables('all')numbersAndVariables,
16861683 forceEnum: false,
16871684 }),
16881685 ],
1686+ splitUnnamedArgument: true,
16891687 helpString: `
16901688 <div>
16911689 Returns the maximum value of the set of values and passes the result down the pipe. Can use variable names.
1690+ </div>
1691+ <div>
1692+ Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
16921693 </div>
16931694 <div>
16941695 <strong>Examples:</strong>
@@ -1696,6 +1697,9 @@ export function registerVariableCommands() {
16961697 <li>
16971698 <pre><code class="language-stscript">/max 10 i 30 j</code></pre>
16981699 </li>
1700+ <li>
1701+ <pre><code class="language-stscript">/max ["count", 15, 2, "i"]</code></pre>
1702+ </li>
16991703 </ul>
17001704 </div>
17011705 `,
@@ -1707,17 +1711,20 @@ export function registerVariableCommands() {
17071711 unnamedArgumentList: [
17081712 SlashCommandArgument.fromProps({
17091713 description: 'values to find the min',
17101714 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
17111715 isRequired: true,
17121716 acceptsMultiple: true,
17131717 enumProvider: commonEnumProviders.variables('all')numbersAndVariables,
17141718 forceEnum: false,
17151719 }),
17161720 ],
1721+ splitUnnamedArgument: true,
17171722 helpString: `
17181723 <div>
17191724 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).
17211728 </div>
17221729 <div>
17231730 <strong>Example:</strong>
@@ -1725,6 +1732,9 @@ export function registerVariableCommands() {
17251732 <li>
17261733 <pre><code class="language-stscript">/min 10 i 30 j</code></pre>
17271734 </li>
1735+ <li>
1736+ <pre><code class="language-stscript">/min ["count", 15, 2, "i"]</code></pre>
1737+ </li>
17281738 </ul>
17291739 </div>
17301740 `,
@@ -1735,18 +1745,21 @@ export function registerVariableCommands() {
17351745 returns: 'difference of the provided values',
17361746 unnamedArgumentList: [
17371747 SlashCommandArgument.fromProps({
17381748 description: 'values to findsubtract, starting form the differencefirst provided value',
17391749 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
17401750 isRequired: true,
17411751 acceptsMultiple: true,
17421752 enumProvider: commonEnumProviders.variables('all')numbersAndVariables,
17431753 forceEnum: false,
17441754 }),
17451755 ],
1756+ splitUnnamedArgument: true,
17461757 helpString: `
17471758 <div>
17481759 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).
17501763 </div>
17511764 <div>
17521765 <strong>Example:</strong>
@@ -1754,6 +1767,9 @@ export function registerVariableCommands() {
17541767 <li>
17551768 <pre><code class="language-stscript">/sub i 5</code></pre>
17561769 </li>
1770+ <li>
1771+ <pre><code class="language-stscript">/sub ["count", 4, "i"]</code></pre>
1772+ </li>
17571773 </ul>
17581774 </div>
17591775 `,
@@ -1767,17 +1783,18 @@ export function registerVariableCommands() {
17671783 description: 'dividend',
17681784 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
17691785 isRequired: true,
17701786 enumProvider: commonEnumProviders.variables('all')numbersAndVariables,
17711787 forceEnum: false,
17721788 }),
17731789 SlashCommandArgument.fromProps({
17741790 description: 'divisor',
17751791 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
17761792 isRequired: true,
17771793 enumProvider: commonEnumProviders.variables('all')numbersAndVariables,
17781794 forceEnum: false,
17791795 }),
17801796 ],
1797+ splitUnnamedArgument: true,
17811798 helpString: `
17821799 <div>
17831800 Performs a division of two values and passes the result down the pipe.
@@ -1802,17 +1819,18 @@ export function registerVariableCommands() {
18021819 description: 'dividend',
18031820 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
18041821 isRequired: true,
18051822 enumProvider: commonEnumProviders.variables('all')numbersAndVariables,
18061823 forceEnum: false,
18071824 }),
18081825 SlashCommandArgument.fromProps({
18091826 description: 'divisor',
18101827 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
18111828 isRequired: true,
18121829 enumProvider: commonEnumProviders.variables('all')numbersAndVariables,
18131830 forceEnum: false,
18141831 }),
18151832 ],
1833+ splitUnnamedArgument: true,
18161834 helpString: `
18171835 <div>
18181836 Performs a modulo operation of two values and passes the result down the pipe.
@@ -1837,17 +1855,18 @@ export function registerVariableCommands() {
18371855 description: 'base',
18381856 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
18391857 isRequired: true,
18401858 enumProvider: commonEnumProviders.variables('all')numbersAndVariables,
18411859 forceEnum: false,
18421860 }),
18431861 SlashCommandArgument.fromProps({
18441862 description: 'exponent',
18451863 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
18461864 isRequired: true,
18471865 enumProvider: commonEnumProviders.variables('all')numbersAndVariables,
18481866 forceEnum: false,
18491867 }),
18501868 ],
1869+ splitUnnamedArgument: true,
18511870 helpString: `
18521871 <div>
18531872 Performs a power operation of two values and passes the result down the pipe.
@@ -1872,7 +1891,7 @@ export function registerVariableCommands() {
18721891 description: 'value',
18731892 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
18741893 isRequired: true,
18751894 enumProvider: commonEnumProviders.variables('all')numbersAndVariables,
18761895 forceEnum: false,
18771896 }),
18781897 ],
@@ -1900,7 +1919,7 @@ export function registerVariableCommands() {
19001919 description: 'value',
19011920 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
19021921 isRequired: true,
19031922 enumProvider: commonEnumProviders.variables('all')numbersAndVariables,
19041923 forceEnum: false,
19051924 }),
19061925 ],
@@ -1929,7 +1948,7 @@ export function registerVariableCommands() {
19291948 description: 'value',
19301949 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
19311950 isRequired: true,
19321951 enumProvider: commonEnumProviders.variables('all')numbersAndVariables,
19331952 forceEnum: false,
19341953 }),
19351954 ],
@@ -1957,7 +1976,7 @@ export function registerVariableCommands() {
19571976 description: 'value',
19581977 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
19591978 isRequired: true,
19601979 enumProvider: commonEnumProviders.variables('all')numbersAndVariables,
19611980 forceEnum: false,
19621981 }),
19631982 ],
@@ -1985,7 +2004,7 @@ export function registerVariableCommands() {
19852004 description: 'value',
19862005 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
19872006 isRequired: true,
19882007 enumProvider: commonEnumProviders.variables('all')numbersAndVariables,
19892008 forceEnum: false,
19902009 }),
19912010 ],
@@ -2013,7 +2032,7 @@ export function registerVariableCommands() {
20132032 description: 'value',
20142033 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
20152034 isRequired: true,
20162035 enumProvider: commonEnumProviders.variables('all')numbersAndVariables,
20172036 forceEnum: false,
20182037 }),
20192038 ],
public/style.css+12 -3
@@ -978,15 +978,15 @@ body .panelControlBar {
978978 justify-content: center;
979979 z-index: 9999;
980980 grid-row-start: 2;
981- grid-column-start: 4;
982- flex-flow: column;
983981 font-size: 30px;
984982 cursor: pointer;
985983 align-self: center;
986984 position: absolute;
987985 bottom: 15px;
986+flex-flow: column;
988987}
989988
989+
990990.swipes-counter {
991991 color: var(--SmartThemeBodyColor);
992992 font-size: 12px;
@@ -994,6 +994,9 @@ body .panelControlBar {
994994 font-family: var(--mainFontFamily);
995995 font-weight: 400;
996996 align-self: center;
997+ min-width: 40px;
998+ display: flex;
999+ justify-content: center;
9971000}
9981001
9991002.swipe_left {
@@ -1003,6 +1006,7 @@ body .panelControlBar {
10031006
10041007.swipe_right {
10051008 right: 5px;
1009+ align-self:end;
10061010}
10071011
10081012.ui-settings {
@@ -2634,6 +2638,11 @@ select option:not(:checked) {
26342638 color: var(--active) !important;
26352639}
26362640
2641+#instruct_enabled_label .menu_button:not(.toggleEnabled),
2642+#sysprompt_enabled_label .menu_button:not(.toggleEnabled) {
2643+ color: Red;
2644+}
2645+
26372646.displayBlock {
26382647 display: block !important;
26392648}