diff --git a/public/index.html b/public/index.html
index 13f59da05..741871edd 100644
--- a/public/index.html
+++ b/public/index.html
@@ -3248,25 +3248,26 @@
diff --git a/public/script.js b/public/script.js
index 6d93bc03b..8d085b6d2 100644
--- a/public/script.js
+++ b/public/script.js
@@ -230,7 +230,7 @@ import { MacrosParser, evaluateMacros, getLastMessageId } from './scripts/macros
import { currentUser, setUserControls } from './scripts/user.js';
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';
import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';
-import { ScraperManager } from './scripts/scrapers.js';
+import { initScrapers, ScraperManager } from './scripts/scrapers.js';
import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js';
import { SlashCommand } from './scripts/slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './scripts/slash-commands/SlashCommandArgument.js';
@@ -959,6 +959,7 @@ async function firstLoadInit() {
initCfg();
initLogprobs();
initInputMarkdown();
+ await initScrapers();
doDailyExtensionUpdatesCheck();
await hideLoader();
await fixViewport();
@@ -5427,6 +5428,7 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
getMessage = getMessage.substring(0, getMessage.indexOf('<|endoftext|>'));
}
const isInstruct = power_user.instruct.enabled && main_api !== 'openai';
+ const isNotEmpty = (str) => str && str.trim() !== '';
if (isInstruct && power_user.instruct.stop_sequence) {
if (getMessage.indexOf(power_user.instruct.stop_sequence) != -1) {
getMessage = getMessage.substring(0, getMessage.indexOf(power_user.instruct.stop_sequence));
@@ -5434,7 +5436,7 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
}
// Hana: Only use the first sequence (should be <|model|>)
// of the prompt before <|user|> (as KoboldAI Lite does it).
- if (isInstruct && power_user.instruct.input_sequence) {
+ if (isInstruct && isNotEmpty(power_user.instruct.input_sequence)) {
if (getMessage.indexOf(power_user.instruct.input_sequence) != -1) {
getMessage = getMessage.substring(0, getMessage.indexOf(power_user.instruct.input_sequence));
}
@@ -8476,12 +8478,22 @@ const CONNECT_API_MAP = {
selected: 'novel',
button: '#api_button_novel',
},
+ 'koboldcpp': {
+ selected: 'textgenerationwebui',
+ button: '#api_button_textgenerationwebui',
+ type: textgen_types.KOBOLDCPP,
+ },
// KoboldCpp alias
'kcpp': {
selected: 'textgenerationwebui',
button: '#api_button_textgenerationwebui',
type: textgen_types.KOBOLDCPP,
},
+ 'openai': {
+ selected: 'openai',
+ button: '#api_button_openai',
+ source: chat_completion_sources.OPENAI,
+ },
// OpenAI alias
'oai': {
selected: 'openai',
diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js
index 92278f35c..1a9e2d7d2 100644
--- a/public/scripts/RossAscends-mods.js
+++ b/public/scripts/RossAscends-mods.js
@@ -56,22 +56,25 @@ let counterNonce = Date.now();
const observerConfig = { childList: true, subtree: true };
const countTokensDebounced = debounce(RA_CountCharTokens, debounce_timeout.relaxed);
+const countTokensShortDebounced = debounce(RA_CountCharTokens, debounce_timeout.short);
+const checkStatusDebounced = debounce(RA_checkOnlineStatus, debounce_timeout.short);
const observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
+ if (!(mutation.target instanceof HTMLElement)) {
+ return;
+ }
if (mutation.target.classList.contains('online_status_text')) {
- RA_checkOnlineStatus();
+ checkStatusDebounced();
} else if (mutation.target.parentNode === SelectedCharacterTab) {
- setTimeout(RA_CountCharTokens, 200);
+ countTokensShortDebounced();
} else if (mutation.target.classList.contains('mes_text')) {
- if (mutation.target instanceof HTMLElement) {
- for (const element of mutation.target.getElementsByTagName('math')) {
- element.childNodes.forEach(function (child) {
- if (child.nodeType === Node.TEXT_NODE) {
- child.textContent = '';
- }
- });
- }
+ for (const element of mutation.target.getElementsByTagName('math')) {
+ element.childNodes.forEach(function (child) {
+ if (child.nodeType === Node.TEXT_NODE) {
+ child.textContent = '';
+ }
+ });
}
}
});
@@ -159,8 +162,8 @@ export function shouldSendOnEnter() {
export function humanizedDateTime() {
const now = new Date(Date.now());
const dt = {
- year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate(),
- hour: now.getHours(), minute: now.getMinutes(), second: now.getSeconds(),
+ year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate(),
+ hour: now.getHours(), minute: now.getMinutes(), second: now.getSeconds(),
};
for (const key in dt) {
dt[key] = dt[key].toString().padStart(2, '0');
@@ -725,9 +728,7 @@ export function addSafariPatch() {
export function initRossMods() {
// initial status check
- setTimeout(() => {
- RA_checkOnlineStatus();
- }, 100);
+ checkStatusDebounced();
if (power_user.auto_load_chat) {
RA_autoloadchat();
@@ -752,7 +753,7 @@ export function initRossMods() {
setTimeout(() => RA_autoconnect(PrevAPI), 100);
});
- $('#api_button').click(function () { setTimeout(RA_checkOnlineStatus, 100); });
+ $('#api_button').on('click', () => checkStatusDebounced());
//toggle pin class when lock toggle clicked
$(RPanelPin).on('click', function () {
diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js
index 71be1e422..edb19ff05 100644
--- a/public/scripts/extensions/expressions/index.js
+++ b/public/scripts/extensions/expressions/index.js
@@ -2,7 +2,7 @@ import { callPopup, eventSource, event_types, generateRaw, getRequestHeaders, ma
import { dragElement, isMobile } from '../../RossAscends-mods.js';
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
import { loadMovingUIState, power_user } from '../../power-user.js';
-import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition } from '../../utils.js';
+import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition, findChar } from '../../utils.js';
import { hideMutedSprites } from '../../group-chats.js';
import { isJsonSchemaSupported } from '../../textgen-settings.js';
import { debounce_timeout } from '../../constants.js';
@@ -2105,14 +2105,20 @@ function migrateSettings() {
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'lastsprite',
- callback: (_, value) => lastExpression[String(value).trim()] ?? '',
+ callback: (_, name) => {
+ if (typeof name !== 'string') throw new Error('name must be a string');
+ const char = findChar({ name: name });
+ const sprite = lastExpression[char?.name ?? name] ?? '';
+ return sprite;
+ },
returns: 'the last set sprite / expression for the named character.',
unnamedArgumentList: [
SlashCommandArgument.fromProps({
- description: 'character name',
+ description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: commonEnumProviders.characters('character'),
+ forceEnum: true,
}),
],
helpString: 'Returns the last set sprite / expression for the named character.',
diff --git a/public/scripts/extensions/gallery/index.js b/public/scripts/extensions/gallery/index.js
index da617c683..a39634603 100644
--- a/public/scripts/extensions/gallery/index.js
+++ b/public/scripts/extensions/gallery/index.js
@@ -441,6 +441,7 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
description: 'character name',
typeList: [ARGUMENT_TYPE.STRING],
enumProvider: commonEnumProviders.characters('character'),
+ forceEnum: true,
}),
SlashCommandNamedArgument.fromProps({
name: 'group',
diff --git a/public/scripts/scrapers.js b/public/scripts/scrapers.js
index 86d2ba567..5a21dedd0 100644
--- a/public/scripts/scrapers.js
+++ b/public/scripts/scrapers.js
@@ -13,6 +13,7 @@ import { isValidUrl } from './utils.js';
* @property {string} description
* @property {string} iconClass
* @property {boolean} iconAvailable
+ * @property {() => Promise} [init=null]
* @property {() => Promise} isAvailable
* @property {() => Promise} scrape
*/
@@ -36,12 +37,16 @@ export class ScraperManager {
* Register a scraper to be used by the Data Bank.
* @param {Scraper} scraper Instance of a scraper to register
*/
- static registerDataBankScraper(scraper) {
+ static async registerDataBankScraper(scraper) {
if (ScraperManager.#scrapers.some(s => s.id === scraper.id)) {
console.warn(`Scraper with ID ${scraper.id} already registered`);
return;
}
+ if (scraper.init) {
+ await scraper.init();
+ }
+
ScraperManager.#scrapers.push(scraper);
}
@@ -462,7 +467,9 @@ class YouTubeScraper {
this.description = 'Download a transcript from a YouTube video.';
this.iconClass = 'fa-brands fa-youtube';
this.iconAvailable = true;
+ }
+ async init() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'yt-script',
callback: async (args, url) => {
@@ -564,9 +571,11 @@ class YouTubeScraper {
}
}
-ScraperManager.registerDataBankScraper(new FileScraper());
-ScraperManager.registerDataBankScraper(new Notepad());
-ScraperManager.registerDataBankScraper(new WebScraper());
-ScraperManager.registerDataBankScraper(new MediaWikiScraper());
-ScraperManager.registerDataBankScraper(new FandomScraper());
-ScraperManager.registerDataBankScraper(new YouTubeScraper());
+export async function initScrapers() {
+ await ScraperManager.registerDataBankScraper(new FileScraper());
+ await ScraperManager.registerDataBankScraper(new Notepad());
+ await ScraperManager.registerDataBankScraper(new WebScraper());
+ await ScraperManager.registerDataBankScraper(new MediaWikiScraper());
+ await ScraperManager.registerDataBankScraper(new FandomScraper());
+ await ScraperManager.registerDataBankScraper(new YouTubeScraper());
+}
diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index 37e99615d..22531eb15 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -55,7 +55,7 @@ import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockStat
import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';
-import { debounce, delay, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
+import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
import { registerVariableCommands, resolveVariable } from './variables.js';
import { background_settings } from './backgrounds.js';
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
@@ -68,7 +68,6 @@ import { SlashCommandNamedArgumentAssignment } from './slash-commands/SlashComma
import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
-import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js';
import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
export {
@@ -173,21 +172,97 @@ export function initDefaultSlashCommands() {
`,
}));
+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
+ name: 'char-find',
+ aliases: ['findchar'],
+ callback: (args, name) => {
+ if (typeof name !== 'string') throw new Error('name must be a string');
+ if (args.preferCurrent instanceof SlashCommandClosure || Array.isArray(args.preferCurrent)) throw new Error('preferCurrent cannot be a closure or array');
+ if (args.quiet instanceof SlashCommandClosure || Array.isArray(args.quiet)) throw new Error('quiet cannot be a closure or array');
+
+ const char = findChar({ name: name, filteredByTags: validateArrayArgString(args.tag, 'tag'), preferCurrentChar: !isFalseBoolean(args.preferCurrent), quiet: isTrueBoolean(args.quiet) });
+ return char?.avatar ?? '';
+ },
+ returns: 'the avatar key (unique identifier) of the character',
+ namedArgumentList: [
+ SlashCommandNamedArgument.fromProps({
+ name: 'tag',
+ description: 'Supply one or more tags to filter down to the correct character for the provided name, if multiple characters have the same name.',
+ typeList: [ARGUMENT_TYPE.STRING],
+ enumProvider: commonEnumProviders.tags('assigned'),
+ acceptsMultiple: true,
+ }),
+ SlashCommandNamedArgument.fromProps({
+ name: 'preferCurrent',
+ description: 'Prefer current character or characters in a group, if multiple characters match',
+ typeList: [ARGUMENT_TYPE.BOOLEAN],
+ defaultValue: 'true',
+ }),
+ SlashCommandNamedArgument.fromProps({
+ name: 'quiet',
+ description: 'Do not show warning if multiple charactrers are found',
+ typeList: [ARGUMENT_TYPE.BOOLEAN],
+ defaultValue: 'false',
+ enumProvider: commonEnumProviders.boolean('trueFalse'),
+ }),
+ ],
+ unnamedArgumentList: [
+ SlashCommandArgument.fromProps({
+ description: 'Character name - or unique character identifier (avatar key)',
+ typeList: [ARGUMENT_TYPE.STRING],
+ enumProvider: commonEnumProviders.characters('character'),
+ forceEnum: false,
+ }),
+ ],
+ helpString: `
+
+ This can be used to choose the correct character for something like /sendas
or other commands in need of a character name
+ if you have multiple characters with the same name.
+
+
@@ -381,12 +460,14 @@ export function initDefaultSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'go',
callback: goToCharacterCallback,
+ returns: 'The character/group name',
unnamedArgumentList: [
SlashCommandArgument.fromProps({
- description: 'name',
+ description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: commonEnumProviders.characters('all'),
+ forceEnum: true,
}),
],
helpString: 'Opens up a chat with the character or group by its name',
@@ -432,7 +513,7 @@ export function initDefaultSlashCommands() {
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'name',
- description: 'character name',
+ description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: commonEnumProviders.characters('character'),
@@ -451,7 +532,7 @@ export function initDefaultSlashCommands() {
namedArgumentList: [],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
- description: 'name',
+ description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: commonEnumProviders.characters('character'),
@@ -618,7 +699,7 @@ export function initDefaultSlashCommands() {
aliases: ['addmember', 'memberadd'],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
- description: 'character name',
+ description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: () => selected_group ? commonEnumProviders.characters('character')() : [],
@@ -856,7 +937,7 @@ export function initDefaultSlashCommands() {
),
SlashCommandNamedArgument.fromProps({
name: 'name',
- description: 'in-prompt name for instruct mode',
+ description: 'in-prompt character name for instruct mode (or unique character identifier (avatar key), which will be used as name)',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'System',
enumProvider: () => [...commonEnumProviders.characters('character')(), new SlashCommandEnumValue('System', null, enumTypes.enum, enumIcons.assistant)],
@@ -2293,7 +2374,8 @@ async function generateCallback(args, value) {
setEphemeralStopStrings(resolveVariable(args?.stop));
const name = args?.name;
- const result = await generateQuietPrompt(value, quietToLoud, false, '', name, length);
+ const char = findChar({ name: name });
+ const result = await generateQuietPrompt(value, quietToLoud, false, '', char?.name ?? name, length);
return result;
} catch (err) {
console.error('Error on /gen generation', err);
@@ -2481,26 +2563,22 @@ async function askCharacter(args, text) {
return '';
}
- let name = '';
-
- if (args?.name) {
- name = args.name.trim();
-
- if (!name) {
- toastr.warning('You must specify a name of the character to ask.');
- return '';
- }
+ if (!args.name) {
+ toastr.warning('You must specify a name of the character to ask.');
+ return '';
}
const prevChId = this_chid;
// Find the character
- const chId = characters.findIndex((e) => e.name === name || e.avatar === name);
- if (!characters[chId] || chId === -1) {
+ const character = findChar({ name: args?.name });
+ if (!character) {
toastr.error('Character not found.');
return '';
}
+ const chId = getCharIndex(character);
+
if (text) {
const mesText = getRegexedString(text.trim(), regex_placement.SLASH_COMMAND);
// 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) {
// Override character and send a user message
setCharacterId(String(chId));
- const character = characters[chId];
- let force_avatar, original_avatar;
+ const { name, force_avatar, original_avatar } = getNameAndAvatarForMessage(character, args?.name);
- if (character && character.avatar !== 'none') {
- force_avatar = getThumbnailUrl('avatar', character.avatar);
- original_avatar = character.avatar;
- }
- else {
- force_avatar = default_avatar;
- original_avatar = default_avatar;
- }
-
- setCharacterName(character.name);
+ setCharacterName(name);
const restoreCharacter = () => {
if (String(this_chid) !== String(chId)) {
return;
}
- setCharacterId(prevChId);
- setCharacterName(characters[prevChId].name);
+ if (prevChId !== undefined) {
+ setCharacterId(prevChId);
+ setCharacterName(characters[prevChId].name);
+ } else {
+ setCharacterId(undefined);
+ setCharacterName(neutralCharacterName);
+ }
// Only force the new avatar if the character name is the same
// This skips if an error was fired
const lastMessage = chat[chat.length - 1];
- if (lastMessage && lastMessage?.name === character.name) {
+ if (lastMessage && lastMessage?.name === name) {
lastMessage.force_avatar = force_avatar;
lastMessage.original_avatar = original_avatar;
}
@@ -2547,7 +2620,7 @@ async function askCharacter(args, text) {
// Run generate and restore previous character
try {
eventSource.once(event_types.MESSAGE_RECEIVED, restoreCharacter);
- toastr.info(`Asking ${character.name} something...`);
+ toastr.info(`Asking ${name} something...`);
askResult = await Generate('ask_command');
} catch (error) {
restoreCharacter();
@@ -2741,26 +2814,23 @@ async function removeGroupMemberCallback(_, arg) {
return '';
}
-async function addGroupMemberCallback(_, arg) {
+async function addGroupMemberCallback(_, name) {
if (!selected_group) {
toastr.warning('Cannot run /memberadd command outside of a group chat.');
return '';
}
- if (!arg) {
+ if (!name) {
console.warn('WARN: No argument provided for /memberadd command');
return '';
}
- arg = arg.trim();
- const chid = findCharacterIndex(arg);
-
- if (chid === -1) {
- console.warn(`WARN: No character found for argument ${arg}`);
+ const character = findChar({ name: name, preferCurrentChar: false });
+ if (!character) {
+ console.warn(`WARN: No character found for argument ${name}`);
return '';
}
- const character = characters[chid];
const group = groups.find(x => x.id === selected_group);
if (!group || !Array.isArray(group.members)) {
@@ -2829,7 +2899,7 @@ function findPersonaByName(name) {
}
for (const persona of Object.entries(power_user.personas)) {
- if (persona[1].toLowerCase() === name.toLowerCase()) {
+ if (equalsIgnoreCaseAndAccents(persona[1], name)) {
return persona[0];
}
}
@@ -2872,7 +2942,9 @@ async function deleteMessagesByNameCallback(_, name) {
return;
}
- name = name.trim();
+ // Search for a matching character to get the real name, or take the name provided
+ const character = findChar({ name: name });
+ name = character?.name || name;
const messagesToDelete = [];
chat.forEach((value) => {
@@ -2901,60 +2973,34 @@ async function deleteMessagesByNameCallback(_, name) {
return '';
}
-function findCharacterIndex(name) {
- const matchTypes = [
- (a, b) => a === b,
- (a, b) => a.startsWith(b),
- (a, b) => a.includes(b),
- ];
-
- const exactAvatarMatch = characters.findIndex(x => x.avatar === name);
-
- if (exactAvatarMatch !== -1) {
- return exactAvatarMatch;
- }
-
- for (const matchType of matchTypes) {
- const index = characters.findIndex(x => matchType(x.name.toLowerCase(), name.toLowerCase()));
- if (index !== -1) {
- return index;
- }
- }
-
- return -1;
-}
-
async function goToCharacterCallback(_, name) {
if (!name) {
console.warn('WARN: No character name provided for /go command');
return;
}
- name = name.trim();
- const characterIndex = findCharacterIndex(name);
-
- if (characterIndex !== -1) {
- await openChat(new String(characterIndex));
- setActiveCharacter(characters[characterIndex]?.avatar);
+ const character = findChar({ name: name });
+ if (character) {
+ const chid = getCharIndex(character);
+ await openChat(new String(chid));
+ setActiveCharacter(character.avatar);
setActiveGroup(null);
- return characters[characterIndex]?.name;
- } else {
- const group = groups.find(it => it.name.toLowerCase() == name.toLowerCase());
- if (group) {
- await openGroupById(group.id);
- setActiveCharacter(null);
- setActiveGroup(group.id);
- return group.name;
- } else {
- console.warn(`No matches found for name "${name}"`);
- return '';
- }
+ return character.name;
}
+ const group = groups.find(it => equalsIgnoreCaseAndAccents(it.name, name));
+ if (group) {
+ await openGroupById(group.id);
+ setActiveCharacter(null);
+ setActiveGroup(group.id);
+ return group.name;
+ }
+ console.warn(`No matches found for name "${name}"`);
+ return '';
}
-async function openChat(id) {
+async function openChat(chid) {
resetSelectedGroup();
- setCharacterId(id);
+ setCharacterId(chid);
await delay(1);
await reloadCurrentChat();
}
@@ -3100,6 +3146,79 @@ async function setNarratorName(_, text) {
return '';
}
+/**
+ * Checks if an argument is a string array (or undefined), and if not, throws an error
+ * @param {string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined} arg The named argument to check
+ * @param {string} name The name of the argument for the error message
+ * @param {object} [options={}] - The optional arguments
+ * @param {boolean} [options.allowUndefined=false] - Whether the argument can be undefined
+ * @throws {Error} If the argument is not an array
+ * @returns {string[]}
+ */
+export function validateArrayArgString(arg, name, { allowUndefined = true } = {}) {
+ if (arg === undefined) {
+ if (allowUndefined) return undefined;
+ throw new Error(`Argument "${name}" is undefined, but must be a string array`);
+ }
+ if (!Array.isArray(arg)) throw new Error(`Argument "${name}" must be an array`);
+ if (!arg.every(x => typeof x === 'string')) throw new Error(`Argument "${name}" must be an array of strings`);
+ return arg;
+}
+
+/**
+ * Checks if an argument is a string or closure array (or undefined), and if not, throws an error
+ * @param {string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined} arg The named argument to check
+ * @param {string} name The name of the argument for the error message
+ * @param {object} [options={}] - The optional arguments
+ * @param {boolean} [options.allowUndefined=false] - Whether the argument can be undefined
+ * @throws {Error} If the argument is not an array of strings or closures
+ * @returns {(string|SlashCommandClosure)[]}
+ */
+export function validateArrayArg(arg, name, { allowUndefined = true } = {}) {
+ if (arg === undefined) {
+ if (allowUndefined) return [];
+ throw new Error(`Argument "${name}" is undefined, but must be an array of strings or closures`);
+ }
+ if (!Array.isArray(arg)) throw new Error(`Argument "${name}" must be an array`);
+ if (!arg.every(x => typeof x === 'string' || x instanceof SlashCommandClosure)) throw new Error(`Argument "${name}" must be an array of strings or closures`);
+ return arg;
+}
+
+
+/**
+ * Retrieves the name and avatar information for a message
+ *
+ * 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,
+ * explicitly implement this in the code using this.
+ *
+ * @param {object?} character - The character object to get the avatar data for
+ * @param {string?} name - The name to get the avatar data for
+ * @returns {{name: string, force_avatar: string, original_avatar: string}} An object containing the name for the message, forced avatar URL, and original avatar
+ */
+export function getNameAndAvatarForMessage(character, name = null) {
+ const isNeutralCharacter = !character && name2 === neutralCharacterName && name === neutralCharacterName;
+ const currentChar = characters[this_chid];
+
+ let force_avatar, original_avatar;
+ if (character?.avatar === currentChar?.avatar || isNeutralCharacter) {
+ // If the targeted character is the currently selected one in a solo chat, we don't need to force any avatars
+ }
+ else if (character && character.avatar !== 'none') {
+ force_avatar = getThumbnailUrl('avatar', character.avatar);
+ original_avatar = character.avatar;
+ }
+ else {
+ force_avatar = default_avatar;
+ original_avatar = default_avatar;
+ }
+
+ return {
+ name: character?.name || name,
+ force_avatar: force_avatar,
+ original_avatar: original_avatar,
+ };
+}
+
export async function sendMessageAs(args, text) {
if (!text) {
return '';
@@ -3138,26 +3257,18 @@ export async function sendMessageAs(args, text) {
const isSystem = bias && !removeMacros(mesText).length;
const compact = isTrueBoolean(args?.compact);
- const character = characters.find(x => x.avatar === name) ?? characters.find(x => x.name === name);
- let force_avatar, original_avatar;
+ const character = findChar({ name: name });
- const chatCharacter = this_chid !== undefined ? characters[this_chid] : null;
- const isNeutralCharacter = !chatCharacter && name2 === neutralCharacterName && name === neutralCharacterName;
+ const avatarCharacter = args.avatar ? findChar({ name: args.avatar }) : character;
+ if (args.avatar && !avatarCharacter) {
+ toastr.warning(`Character for avatar ${args.avatar} not found`);
+ return '';
+ }
- if (chatCharacter === character || isNeutralCharacter) {
- // If the targeted character is the currently selected one in a solo chat, we don't need to force any avatars
- }
- else if (character && character.avatar !== 'none') {
- force_avatar = getThumbnailUrl('avatar', character.avatar);
- original_avatar = character.avatar;
- }
- else {
- force_avatar = default_avatar;
- original_avatar = default_avatar;
- }
+ const { name: avatarCharName, force_avatar, original_avatar } = getNameAndAvatarForMessage(avatarCharacter, name);
const message = {
- name: name,
+ name: character?.name || name || avatarCharName,
is_user: false,
is_system: isSystem,
send_date: getMessageTimeStamp(),
@@ -3561,11 +3672,17 @@ function setPromptEntryCallback(args, targetState) {
const prompts = promptManager.serviceSettings.prompts;
function parseArgs(arg) {
+ // Arg is already an array
+ if (Array.isArray(arg)) {
+ return arg;
+ }
const list = [];
try {
+ // Arg is a JSON-stringified array
const parsedArg = JSON.parse(arg);
list.push(...Array.isArray(parsedArg) ? parsedArg : [arg]);
} catch {
+ // Arg is a string
list.push(arg);
}
return list;
diff --git a/public/scripts/slash-commands/SlashCommand.js b/public/scripts/slash-commands/SlashCommand.js
index f485e438e..c56803249 100644
--- a/public/scripts/slash-commands/SlashCommand.js
+++ b/public/scripts/slash-commands/SlashCommand.js
@@ -15,13 +15,13 @@ import { SlashCommandScope } from './SlashCommandScope.js';
* _abortController:SlashCommandAbortController,
* _debugController:SlashCommandDebugController,
* _hasUnnamedArgument:boolean,
- * [id:string]:string|SlashCommandClosure,
+ * [id:string]:string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined,
* }} NamedArguments
*/
/**
* Alternative object for local JSDocs, where you don't need existing pipe, scope, etc. arguments
- * @typedef {{[id:string]:string|SlashCommandClosure}} NamedArgumentsCapture
+ * @typedef {{[id:string]:string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined}} NamedArgumentsCapture
*/
/**
diff --git a/public/scripts/slash-commands/SlashCommandClosure.js b/public/scripts/slash-commands/SlashCommandClosure.js
index ecf1d968c..0ba1726b5 100644
--- a/public/scripts/slash-commands/SlashCommandClosure.js
+++ b/public/scripts/slash-commands/SlashCommandClosure.js
@@ -2,6 +2,7 @@ import { substituteParams } from '../../script.js';
import { delay, escapeRegex, uuidv4 } from '../utils.js';
import { SlashCommand } from './SlashCommand.js';
import { SlashCommandAbortController } from './SlashCommandAbortController.js';
+import { SlashCommandNamedArgument } from './SlashCommandArgument.js';
import { SlashCommandBreak } from './SlashCommandBreak.js';
import { SlashCommandBreakController } from './SlashCommandBreakController.js';
import { SlashCommandBreakPoint } from './SlashCommandBreakPoint.js';
@@ -53,7 +54,7 @@ export class SlashCommandClosure {
*
* @param {string} text
* @param {SlashCommandScope} scope
- * @returns
+ * @returns {string|SlashCommandClosure|(string|SlashCommandClosure)[]}
*/
substituteParams(text, scope = null) {
let isList = false;
@@ -379,6 +380,52 @@ export class SlashCommandClosure {
* @param {import('./SlashCommand.js').NamedArguments} args
*/
async substituteNamedArguments(executor, args) {
+ /**
+ * Handles the assignment of named arguments, considering if they accept multiple values
+ * @param {string} name The name of the argument, as defined for the command execution
+ * @param {string|SlashCommandClosure|(string|SlashCommandClosure)[]} value The value to be assigned
+ */
+ const assign = (name, value) => {
+ // If an array is supposed to be assigned, assign it one by one
+ if (Array.isArray(value)) {
+ for (const val of value) {
+ assign(name, val);
+ }
+ return;
+ }
+
+ const definition = executor.command.namedArgumentList.find(x => x.name == name);
+
+ // Prefer definition name if a valid named args defintion is found
+ name = definition?.name ?? name;
+
+ // Unescape named argument
+ if (value && typeof value == 'string') {
+ value = value
+ .replace(/\\\{/g, '{')
+ .replace(/\\\}/g, '}');
+ }
+
+ // If the named argument accepts multiple values, we have to make sure to build an array correctly
+ if (definition?.acceptsMultiple) {
+ if (args[name] !== undefined) {
+ // If there already is something for that named arg, make the value is an array and add to it
+ let currentValue = args[name];
+ if (!Array.isArray(currentValue)) {
+ currentValue = [currentValue];
+ }
+ currentValue.push(value);
+ args[name] = currentValue;
+ } else {
+ // If there is nothing in there, we create an array with that singular value
+ args[name] = [value];
+ }
+ } else {
+ args[name] !== undefined && console.debug(`Named argument assigned multiple times: ${name}`);
+ args[name] = value;
+ }
+ };
+
// substitute named arguments
for (const arg of executor.namedArgumentList) {
if (arg.value instanceof SlashCommandClosure) {
@@ -390,19 +437,12 @@ export class SlashCommandClosure {
closure.debugController = this.debugController;
}
if (closure.executeNow) {
- args[arg.name] = (await closure.execute())?.pipe;
+ assign(arg.name, (await closure.execute())?.pipe);
} else {
- args[arg.name] = closure;
+ assign(arg.name, closure);
}
} else {
- args[arg.name] = this.substituteParams(arg.value);
- }
- // unescape named argument
- if (typeof args[arg.name] == 'string') {
- args[arg.name] = args[arg.name]
- ?.replace(/\\\{/g, '{')
- ?.replace(/\\\}/g, '}')
- ;
+ assign(arg.name, this.substituteParams(arg.value));
}
}
}
@@ -424,6 +464,7 @@ export class SlashCommandClosure {
} else {
value = [];
for (let i = 0; i < executor.unnamedArgumentList.length; i++) {
+ /** @type {string|SlashCommandClosure|(string|SlashCommandClosure)[]} */
let v = executor.unnamedArgumentList[i].value;
if (v instanceof SlashCommandClosure) {
/**@type {SlashCommandClosure}*/
@@ -467,6 +508,14 @@ export class SlashCommandClosure {
return v;
});
}
+
+ value ??= '';
+
+ // Make sure that if unnamed args are split, it should always return an array
+ if (executor.command.splitUnnamedArgument && !Array.isArray(value)) {
+ value = [value];
+ }
+
return value;
}
diff --git a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js
index 5612f47b5..5a40965c1 100644
--- a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js
+++ b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js
@@ -2,7 +2,7 @@ import { chat_metadata, characters, substituteParams, chat, extension_prompt_rol
import { extension_settings } from '../extensions.js';
import { getGroupMembers, groups } from '../group-chats.js';
import { power_user } from '../power-user.js';
-import { searchCharByName, getTagsList, tags } from '../tags.js';
+import { searchCharByName, getTagsList, tags, tag_map } from '../tags.js';
import { world_names } from '../world-info.js';
import { SlashCommandClosure } from './SlashCommandClosure.js';
import { SlashCommandEnumValue, enumTypes } from './SlashCommandEnumValue.js';
@@ -152,6 +152,35 @@ export const commonEnumProviders = {
].filter((item, idx, list)=>idx == list.findIndex(it=>it.value == item.value));
},
+ /**
+ * Enum values for numbers and variable names
+ *
+ * Includes all variable names and the ability to specify any number
+ *
+ * @param {SlashCommandExecutor} executor - The executor of the slash command
+ * @param {SlashCommandScope} scope - The scope of the slash command
+ * @returns {SlashCommandEnumValue[]} The enum values
+ */
+ numbersAndVariables: (executor, scope) => [
+ ...commonEnumProviders.variables('all')(executor, scope),
+ new SlashCommandEnumValue(
+ 'any variable name',
+ null,
+ enumTypes.variable,
+ enumIcons.variable,
+ (input) => /^\w*$/.test(input),
+ (input) => input,
+ ),
+ new SlashCommandEnumValue(
+ 'any number',
+ null,
+ enumTypes.number,
+ enumIcons.number,
+ (input) => input == '' || !Number.isNaN(Number(input)),
+ (input) => input,
+ ),
+ ],
+
/**
* All possible char entities, like characters and groups. Can be filtered down to just one type.
*
@@ -181,6 +210,18 @@ export const commonEnumProviders = {
*/
personas: () => Object.values(power_user.personas).map(persona => new SlashCommandEnumValue(persona, null, enumTypes.name, enumIcons.persona)),
+ /**
+ * All possible tags, or only those that have been assigned
+ *
+ * @param {('all' | 'assigned')} [mode='all'] - Which types of tags to show
+ * @returns {() => SlashCommandEnumValue[]}
+ */
+ tags: (mode = 'all') => () => {
+ let assignedTags = mode === 'assigned' ? new Set(Object.values(tag_map).flat()) : new Set();
+ return tags.filter(tag => mode === 'all' || (mode === 'assigned' && assignedTags.has(tag.id)))
+ .map(tag => new SlashCommandEnumValue(tag.name, null, enumTypes.command, enumIcons.tag));
+ },
+
/**
* All possible tags for a given char/group entity
*
@@ -193,7 +234,7 @@ export const commonEnumProviders = {
if (charName instanceof SlashCommandClosure) throw new Error('Argument \'name\' does not support closures');
const key = searchCharByName(substituteParams(charName), { suppressLogging: true });
const assigned = key ? getTagsList(key) : [];
- return tags.filter(it => !key || mode === 'all' || mode === 'existing' && assigned.includes(it) || mode === 'not-existing' && !assigned.includes(it))
+ return tags.filter(it => mode === 'all' || mode === 'existing' && assigned.includes(it) || mode === 'not-existing' && !assigned.includes(it))
.map(tag => new SlashCommandEnumValue(tag.name, null, enumTypes.command, enumIcons.tag));
},
diff --git a/public/scripts/tags.js b/public/scripts/tags.js
index c56e0d1cb..9a1950d0a 100644
--- a/public/scripts/tags.js
+++ b/public/scripts/tags.js
@@ -15,7 +15,7 @@ import {
import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, FilterHelper } from './filters.js';
import { groupCandidatesFilter, groups, selected_group } from './group-chats.js';
-import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, flashHighlight, equalsIgnoreCaseAndAccents, includesIgnoreCaseAndAccents, removeFromArray, getFreeName, debounce } from './utils.js';
+import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, flashHighlight, equalsIgnoreCaseAndAccents, includesIgnoreCaseAndAccents, removeFromArray, getFreeName, debounce, findChar } from './utils.js';
import { power_user } from './power-user.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
@@ -50,7 +50,6 @@ export {
removeTagFromMap,
};
-/** @typedef {import('../scripts/popup.js').Popup} Popup */
/** @typedef {import('../script.js').Character} Character */
const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter';
@@ -507,7 +506,7 @@ export function getTagKeyForEntityElement(element) {
*/
export function searchCharByName(charName, { suppressLogging = false } = {}) {
const entity = charName
- ? (characters.find(x => x.name === charName) || groups.find(x => x.name == charName))
+ ? (findChar({ name: charName }) || groups.find(x => equalsIgnoreCaseAndAccents(x.name, charName)))
: (selected_group ? groups.find(x => x.id == selected_group) : characters[this_chid]);
const key = getTagKeyForEntity(entity);
if (!key) {
@@ -1861,8 +1860,9 @@ function registerTagsSlashCommands() {
return String(result);
},
namedArgumentList: [
- SlashCommandNamedArgument.fromProps({ name: 'name',
- description: 'Character name',
+ SlashCommandNamedArgument.fromProps({
+ name: 'name',
+ description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: '{{char}}',
enumProvider: commonEnumProviders.characters(),
@@ -1907,7 +1907,7 @@ function registerTagsSlashCommands() {
},
namedArgumentList: [
SlashCommandNamedArgument.fromProps({ name: 'name',
- description: 'Character name',
+ description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: '{{char}}',
enumProvider: commonEnumProviders.characters(),
@@ -1950,7 +1950,7 @@ function registerTagsSlashCommands() {
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'name',
- description: 'Character name',
+ description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: '{{char}}',
enumProvider: commonEnumProviders.characters(),
@@ -1993,7 +1993,7 @@ function registerTagsSlashCommands() {
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'name',
- description: 'Character name',
+ description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: '{{char}}',
enumProvider: commonEnumProviders.characters(),
diff --git a/public/scripts/utils.js b/public/scripts/utils.js
index 9fc1e1e99..6cf92f5c0 100644
--- a/public/scripts/utils.js
+++ b/public/scripts/utils.js
@@ -1,10 +1,12 @@
import { getContext } from './extensions.js';
-import { getRequestHeaders } from '../script.js';
+import { characters, getRequestHeaders, this_chid } from '../script.js';
import { isMobile } from './RossAscends-mods.js';
import { collapseNewlines } from './power-user.js';
import { debounce_timeout } from './constants.js';
import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
+import { getTagsList } from './tags.js';
+import { groups, selected_group } from './group-chats.js';
/**
* Pagination status string template.
@@ -2110,3 +2112,74 @@ export async function showFontAwesomePicker(customList = null) {
}
return null;
}
+
+/**
+ * Finds a character by name, with optional filtering and precedence for avatars
+ * @param {object} [options={}] - The options for the search
+ * @param {string?} [options.name=null] - The name to search for
+ * @param {boolean} [options.allowAvatar=true] - Whether to allow searching by avatar
+ * @param {boolean} [options.insensitive=true] - Whether the search should be case insensitive
+ * @param {string[]?} [options.filteredByTags=null] - Tags to filter characters by
+ * @param {boolean} [options.preferCurrentChar=true] - Whether to prefer the current character(s)
+ * @param {boolean} [options.quiet=false] - Whether to suppress warnings
+ * @returns {any?} - The found character or null if not found
+ */
+export function findChar({ name = null, allowAvatar = true, insensitive = true, filteredByTags = null, preferCurrentChar = true, quiet = false } = {}) {
+ const matches = (char) => !name || (allowAvatar && char.avatar === name) || (insensitive ? equalsIgnoreCaseAndAccents(char.name, name) : char.name === name);
+
+ // Filter characters by tags if provided
+ let filteredCharacters = characters;
+ if (filteredByTags) {
+ filteredCharacters = characters.filter(char => {
+ const charTags = getTagsList(char.avatar, false);
+ return filteredByTags.every(tagName => charTags.some(x => x.name == tagName));
+ });
+ }
+
+ // Get the current character(s)
+ /** @type {any[]} */
+ const currentChars = selected_group ? groups.find(group => group.id === selected_group)?.members.map(member => filteredCharacters.find(char => char.avatar === member))
+ : filteredCharacters.filter(char => characters[this_chid]?.avatar === char.avatar);
+
+ // If we have a current char and prefer it, return that if it matches
+ if (preferCurrentChar) {
+ const preferredCharSearch = currentChars.filter(matches);
+ if (preferredCharSearch.length > 1) {
+ if (!quiet) toastr.warning('Multiple characters found for given conditions.');
+ else console.warn('Multiple characters found for given conditions. Returning the first match.');
+ }
+ if (preferredCharSearch.length) {
+ return preferredCharSearch[0];
+ }
+ }
+
+ // If allowAvatar is true, search by avatar first
+ if (allowAvatar && name) {
+ const characterByAvatar = filteredCharacters.find(char => char.avatar === name);
+ if (characterByAvatar) {
+ return characterByAvatar;
+ }
+ }
+
+ // Search for matching characters by name
+ const matchingCharacters = name ? filteredCharacters.filter(matches) : filteredCharacters;
+ if (matchingCharacters.length > 1) {
+ if (!quiet) toastr.warning('Multiple characters found for given conditions.');
+ else console.warn('Multiple characters found for given conditions. Returning the first match.');
+ }
+
+ return matchingCharacters[0] || null;
+}
+
+/**
+ * Gets the index of a character based on the character object
+ * @param {object} char - The character object to find the index for
+ * @throws {Error} If the character is not found
+ * @returns {number} The index of the character in the characters array
+ */
+export function getCharIndex(char) {
+ if (!char) throw new Error('Character is undefined');
+ const index = characters.findIndex(c => c.avatar === char.avatar);
+ if (index === -1) throw new Error(`Character not found: ${char.avatar}`);
+ return index;
+}
diff --git a/public/scripts/variables.js b/public/scripts/variables.js
index e9cd8c653..7c9830a36 100644
--- a/public/scripts/variables.js
+++ b/public/scripts/variables.js
@@ -669,8 +669,8 @@ function deleteGlobalVariable(name) {
}
/**
- * Parses a series of numeric values from a string.
- * @param {string} value A space-separated list of numeric values or variable names
+ * Parses a series of numeric values from a string or a string array.
+ * @param {string|string[]} value A space-separated list of numeric values or variable names
* @param {SlashCommandScope} scope Scope
* @returns {number[]} An array of numeric values
*/
@@ -679,11 +679,17 @@ function parseNumericSeries(value, scope = null) {
return [value];
}
- const array = value
- .split(' ')
- .map(i => i.trim())
+ /** @type {(string|number)[]} */
+ let values = Array.isArray(value) ? value : value.split(' ');
+
+ // If a JSON array was provided as the only value, convert it to an array
+ if (values.length === 1 && typeof values[0] === 'string' && values[0].startsWith('[')) {
+ values = convertValueType(values[0], 'array');
+ }
+
+ const array = values.map(i => typeof i === 'string' ? i.trim() : i)
.filter(i => i !== '')
- .map(i => isNaN(Number(i)) ? Number(resolveVariable(i, scope)) : Number(i))
+ .map(i => isNaN(Number(i)) ? Number(resolveVariable(String(i), scope)) : Number(i))
.filter(i => !isNaN(i));
return array;
@@ -703,7 +709,7 @@ function performOperation(value, operation, singleOperand = false, scope = null)
const result = singleOperand ? operation(array[0]) : operation(array);
- if (isNaN(result) || !isFinite(result)) {
+ if (isNaN(result)) {
return 0;
}
@@ -731,7 +737,7 @@ function maxValuesCallback(args, value) {
}
function subValuesCallback(args, value) {
- return performOperation(value, (array) => array[0] - array[1], false, args._scope);
+ return performOperation(value, (array) => array.reduce((a, b) => a - b, array.shift() ?? 0), false, args._scope);
}
function divValuesCallback(args, value) {
@@ -1595,36 +1601,15 @@ export function registerVariableCommands() {
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'add',
- callback: (args, /**@type {string[]}*/value) => addValuesCallback(args, value.join(' ')),
+ callback: (args, value) => addValuesCallback(args, value),
returns: 'sum of the provided values',
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'values to sum',
- typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
+ typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
isRequired: true,
acceptsMultiple: true,
- enumProvider: (executor, scope) => {
- const vars = commonEnumProviders.variables('all')(executor, scope);
- vars.push(
- new SlashCommandEnumValue(
- 'any variable name',
- null,
- enumTypes.variable,
- enumIcons.variable,
- (input) => /^\w*$/.test(input),
- (input) => input,
- ),
- new SlashCommandEnumValue(
- 'any number',
- null,
- enumTypes.number,
- enumIcons.number,
- (input) => input == '' || !Number.isNaN(Number(input)),
- (input) => input,
- ),
- );
- return vars;
- },
+ enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
@@ -1632,7 +1617,9 @@ export function registerVariableCommands() {
helpString: `
Performs an addition of the set of values and passes the result down the pipe.
- Can use variable names.
+
+
+ Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
Example:
@@ -1640,6 +1627,9 @@ export function registerVariableCommands() {
/add 10 i 30 j
+
+ /add ["count", 15, 2, "i"]
+
`,
@@ -1651,16 +1641,20 @@ export function registerVariableCommands() {
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'values to multiply',
- typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
+ typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
isRequired: true,
acceptsMultiple: true,
- enumProvider: commonEnumProviders.variables('all'),
+ enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
+ splitUnnamedArgument: true,
helpString: `
- Performs a multiplication of the set of values and passes the result down the pipe. Can use variable names.
+ Performs a multiplication of the set of values and passes the result down the pipe.
+
+
+ Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
Examples:
@@ -1668,6 +1662,9 @@ export function registerVariableCommands() {
/mul 10 i 30 j
+
+ /mul ["count", 15, 2, "i"]
+
`,
@@ -1679,16 +1676,20 @@ export function registerVariableCommands() {
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'values to find the max',
- typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
+ typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
isRequired: true,
acceptsMultiple: true,
- enumProvider: commonEnumProviders.variables('all'),
+ enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
+ splitUnnamedArgument: true,
helpString: `
- Returns the maximum value of the set of values and passes the result down the pipe. Can use variable names.
+ Returns the maximum value of the set of values and passes the result down the pipe.
+
+
+ Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
Examples:
@@ -1696,6 +1697,9 @@ export function registerVariableCommands() {
/max 10 i 30 j
+
+ /max ["count", 15, 2, "i"]
+
`,
@@ -1707,17 +1711,20 @@ export function registerVariableCommands() {
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'values to find the min',
- typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
+ typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
isRequired: true,
acceptsMultiple: true,
- enumProvider: commonEnumProviders.variables('all'),
+ enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
+ splitUnnamedArgument: true,
helpString: `
Returns the minimum value of the set of values and passes the result down the pipe.
- Can use variable names.
+
+
+ Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
Example:
@@ -1725,6 +1732,9 @@ export function registerVariableCommands() {
/min 10 i 30 j
+
+ /min ["count", 15, 2, "i"]
+
`,
@@ -1735,18 +1745,21 @@ export function registerVariableCommands() {
returns: 'difference of the provided values',
unnamedArgumentList: [
SlashCommandArgument.fromProps({
- description: 'values to find the difference',
- typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
+ description: 'values to subtract, starting form the first provided value',
+ typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
isRequired: true,
acceptsMultiple: true,
- enumProvider: commonEnumProviders.variables('all'),
+ enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
+ splitUnnamedArgument: true,
helpString: `
Performs a subtraction of the set of values and passes the result down the pipe.
- Can use variable names.
+
+
+ Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
Example:
@@ -1754,6 +1767,9 @@ export function registerVariableCommands() {
/sub i 5
+
+ /sub ["count", 4, "i"]
+
`,
@@ -1767,17 +1783,18 @@ export function registerVariableCommands() {
description: 'dividend',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
- enumProvider: commonEnumProviders.variables('all'),
+ enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
SlashCommandArgument.fromProps({
description: 'divisor',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
- enumProvider: commonEnumProviders.variables('all'),
+ enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
+ splitUnnamedArgument: true,
helpString: `
Performs a division of two values and passes the result down the pipe.
@@ -1802,17 +1819,18 @@ export function registerVariableCommands() {
description: 'dividend',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
- enumProvider: commonEnumProviders.variables('all'),
+ enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
SlashCommandArgument.fromProps({
description: 'divisor',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
- enumProvider: commonEnumProviders.variables('all'),
+ enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
+ splitUnnamedArgument: true,
helpString: `
Performs a modulo operation of two values and passes the result down the pipe.
@@ -1837,17 +1855,18 @@ export function registerVariableCommands() {
description: 'base',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
- enumProvider: commonEnumProviders.variables('all'),
+ enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
SlashCommandArgument.fromProps({
description: 'exponent',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
- enumProvider: commonEnumProviders.variables('all'),
+ enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
+ splitUnnamedArgument: true,
helpString: `
Performs a power operation of two values and passes the result down the pipe.
@@ -1872,7 +1891,7 @@ export function registerVariableCommands() {
description: 'value',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
- enumProvider: commonEnumProviders.variables('all'),
+ enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
@@ -1900,7 +1919,7 @@ export function registerVariableCommands() {
description: 'value',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
- enumProvider: commonEnumProviders.variables('all'),
+ enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
@@ -1929,7 +1948,7 @@ export function registerVariableCommands() {
description: 'value',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
- enumProvider: commonEnumProviders.variables('all'),
+ enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
@@ -1957,7 +1976,7 @@ export function registerVariableCommands() {
description: 'value',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
- enumProvider: commonEnumProviders.variables('all'),
+ enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
@@ -1985,7 +2004,7 @@ export function registerVariableCommands() {
description: 'value',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
- enumProvider: commonEnumProviders.variables('all'),
+ enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
@@ -2013,7 +2032,7 @@ export function registerVariableCommands() {
description: 'value',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
- enumProvider: commonEnumProviders.variables('all'),
+ enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
diff --git a/public/style.css b/public/style.css
index 1c8096d86..7d55cf832 100644
--- a/public/style.css
+++ b/public/style.css
@@ -978,15 +978,15 @@ body .panelControlBar {
justify-content: center;
z-index: 9999;
grid-row-start: 2;
- grid-column-start: 4;
- flex-flow: column;
font-size: 30px;
cursor: pointer;
align-self: center;
position: absolute;
- bottom: 15px;
+bottom: 15px;
+flex-flow: column;
}
+
.swipes-counter {
color: var(--SmartThemeBodyColor);
font-size: 12px;
@@ -994,6 +994,9 @@ body .panelControlBar {
font-family: var(--mainFontFamily);
font-weight: 400;
align-self: center;
+ min-width: 40px;
+ display: flex;
+ justify-content: center;
}
.swipe_left {
@@ -1003,6 +1006,7 @@ body .panelControlBar {
.swipe_right {
right: 5px;
+ align-self:end;
}
.ui-settings {
@@ -2634,6 +2638,11 @@ select option:not(:checked) {
color: var(--active) !important;
}
+#instruct_enabled_label .menu_button:not(.toggleEnabled),
+#sysprompt_enabled_label .menu_button:not(.toggleEnabled) {
+ color: Red;
+}
+
.displayBlock {
display: block !important;
}