From 2444e9be43fe2824423716e9ae5c9a34174d70c0 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sat, 28 Sep 2024 21:59:29 +0200 Subject: [PATCH 01/34] STscript allow named arguments to be an array - Use named args definition and "acceptsMultiple" to build arrays of values, if provided - Add a debug warning if non-multiple named args are provided multiple times --- public/scripts/slash-commands/SlashCommand.js | 4 +- .../slash-commands/SlashCommandClosure.js | 63 +++++++++++++++---- 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/public/scripts/slash-commands/SlashCommand.js b/public/scripts/slash-commands/SlashCommand.js index f485e438e..5f8726ba0 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)[], * }} 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)[]}} NamedArgumentsCapture */ /** diff --git a/public/scripts/slash-commands/SlashCommandClosure.js b/public/scripts/slash-commands/SlashCommandClosure.js index ecf1d968c..97a206138 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, just assign it as singular value, until multiple values are found + 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}*/ From d231b7f5f4bdcad0d4b3c341d5c59f7477856b0c Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sat, 28 Sep 2024 22:14:33 +0200 Subject: [PATCH 02/34] "acceptsMultiple" should always return array --- public/scripts/slash-commands/SlashCommandClosure.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/scripts/slash-commands/SlashCommandClosure.js b/public/scripts/slash-commands/SlashCommandClosure.js index 97a206138..45c4d48ba 100644 --- a/public/scripts/slash-commands/SlashCommandClosure.js +++ b/public/scripts/slash-commands/SlashCommandClosure.js @@ -417,8 +417,8 @@ export class SlashCommandClosure { currentValue.push(value); args[name] = currentValue; } else { - // If there is nothing in there, just assign it as singular value, until multiple values are found - args[name] = value; + // 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}`); From 6714bb8c1506e6917b7aaad3e8e57379aac8e0f6 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sun, 15 Sep 2024 17:53:14 +0200 Subject: [PATCH 03/34] Prefer current char if multiple have the same name - Match against current chat character first - Also removed avatar-key matching, it was broken --- public/scripts/slash-commands.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 37e99615d..01f1dbb37 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -3138,12 +3138,12 @@ 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 chatCharacter = this_chid !== undefined ? characters[this_chid] : null; const isNeutralCharacter = !chatCharacter && name2 === neutralCharacterName && name === neutralCharacterName; + const character = chatCharacter.name === name ? chatCharacter : characters.find(x => x.name === name); + let force_avatar, original_avatar; + 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 } From d8e57c507f0243d6888936011d2c5c9a8698f94a Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sun, 15 Sep 2024 17:55:13 +0200 Subject: [PATCH 04/34] Allow case-insensitive name search --- public/scripts/slash-commands.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 01f1dbb37..59a1b2ccb 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, 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'; @@ -3141,7 +3141,7 @@ export async function sendMessageAs(args, text) { const chatCharacter = this_chid !== undefined ? characters[this_chid] : null; const isNeutralCharacter = !chatCharacter && name2 === neutralCharacterName && name === neutralCharacterName; - const character = chatCharacter.name === name ? chatCharacter : characters.find(x => x.name === name); + const character = equalsIgnoreCaseAndAccents(chatCharacter.name, name) ? chatCharacter : characters.find(x => equalsIgnoreCaseAndAccents(x.name, name)); let force_avatar, original_avatar; if (chatCharacter === character || isNeutralCharacter) { @@ -3157,7 +3157,7 @@ export async function sendMessageAs(args, text) { } const message = { - name: name, + name: character?.name || name, is_user: false, is_system: isSystem, send_date: getMessageTimeStamp(), From 4594353c72c378777f1ff4e1efb17df7b681b546 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sun, 15 Sep 2024 18:21:31 +0200 Subject: [PATCH 05/34] Add avatar= to /sendas to allow avatar override --- public/scripts/slash-commands.js | 34 ++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 59a1b2ccb..a804a43bf 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -185,9 +185,18 @@ export function initDefaultSlashCommands() { enumProvider: commonEnumProviders.characters('character'), forceEnum: false, }), - new SlashCommandNamedArgument( - 'compact', 'Use compact layout', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', - ), + SlashCommandNamedArgument.fromProps({ + name: 'avatar', + description: 'Optional character avatar override (Can be either avatar filename or just the character name to pull the avatar from)', + typeList: [ARGUMENT_TYPE.STRING], + enumProvider: commonEnumProviders.characters('character'), + }), + SlashCommandNamedArgument.fromProps({ + name: 'compact', + description: 'Use compact layout', + typeList: [ARGUMENT_TYPE.BOOLEAN], + defaultValue: 'false', + }), SlashCommandNamedArgument.fromProps({ name: 'at', 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.', @@ -3142,14 +3151,23 @@ export async function sendMessageAs(args, text) { const isNeutralCharacter = !chatCharacter && name2 === neutralCharacterName && name === neutralCharacterName; const character = equalsIgnoreCaseAndAccents(chatCharacter.name, name) ? chatCharacter : characters.find(x => equalsIgnoreCaseAndAccents(x.name, name)); - let force_avatar, original_avatar; - if (chatCharacter === character || isNeutralCharacter) { + let avatarChar = character; + if (args.avatar) { + avatarChar = characters.find(x => x.avatar == args.avatar) ?? characters.find(x => equalsIgnoreCaseAndAccents(x.name, args.avatar)); + if (!avatarChar) { + toastr.warning(`Character for avatar ${args.avatar} not found`); + return ''; + } + } + + let force_avatar, original_avatar; + if (chatCharacter === avatarChar || 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 if (avatarChar && avatarChar.avatar !== 'none') { + force_avatar = getThumbnailUrl('avatar', avatarChar.avatar); + original_avatar = avatarChar.avatar; } else { force_avatar = default_avatar; From 22d8a654d95763213a7d823adc2ae695ce6ca129 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sun, 15 Sep 2024 19:20:43 +0200 Subject: [PATCH 06/34] Allow searching on /sendas via tag= --- public/scripts/slash-commands.js | 68 +++++++++++++++---- .../SlashCommandCommonEnumsProvider.js | 2 +- 2 files changed, 56 insertions(+), 14 deletions(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index a804a43bf..7ca6187cb 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -187,10 +187,17 @@ export function initDefaultSlashCommands() { }), SlashCommandNamedArgument.fromProps({ name: 'avatar', - description: 'Optional character avatar override (Can be either avatar filename or just the character name to pull the avatar from)', + description: 'Character avatar override (Can be either avatar filename or just the character name to pull the avatar from)', typeList: [ARGUMENT_TYPE.STRING], enumProvider: commonEnumProviders.characters('character'), }), + 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. (does not apply to the avatar argument)', + typeList: [ARGUMENT_TYPE.STRING], + enumProvider: commonEnumProviders.tagsForChar('all'), + acceptsMultiple: true, + }), SlashCommandNamedArgument.fromProps({ name: 'compact', description: 'Use compact layout', @@ -3109,6 +3116,44 @@ async function setNarratorName(_, text) { return ''; } +/** + * Finds a character by name, with optional filtering and precedence for avatars + * @param {string} name - The name to search for + * @param {object} [options={}] - The options for the search + * @param {boolean} [options.allowAvatar=false] - 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 {any?} [options.preferCurrentChar=null] - The current character to prefer + * @returns {any?} - The found character or null if not found + */ +export function findCharByName(name, { allowAvatar = false, insensitive = true, filteredByTags = null, preferCurrentChar = null } = {}) { + const matches = (char) => (allowAvatar && char.avatar === name) || insensitive ? equalsIgnoreCaseAndAccents(char.name, name) : char.name === name; + + // If we have a current char and prefer it, return that if it matches - unless tags are provided, they have precedence + if (preferCurrentChar && !filteredByTags && matches(preferCurrentChar)) { + return preferCurrentChar; + } + + // Filter characters by tags if provided + let filteredCharacters = characters; + if (filteredByTags) { + filteredCharacters = characters.filter(char => filteredByTags.every(tag => char.tags.includes(tag))); + } + + // If allowAvatar is true, search by avatar first + if (allowAvatar) { + const characterByAvatar = filteredCharacters.find(char => char.avatar === name); + if (characterByAvatar) { + return characterByAvatar; + } + } + + // Search for a matching character by name + let character = filteredCharacters.find(matches); + + return character; +} + export async function sendMessageAs(args, text) { if (!text) { return ''; @@ -3150,24 +3195,21 @@ export async function sendMessageAs(args, text) { const chatCharacter = this_chid !== undefined ? characters[this_chid] : null; const isNeutralCharacter = !chatCharacter && name2 === neutralCharacterName && name === neutralCharacterName; - const character = equalsIgnoreCaseAndAccents(chatCharacter.name, name) ? chatCharacter : characters.find(x => equalsIgnoreCaseAndAccents(x.name, name)); + const character = findCharByName(name, { filteredByTags: args?.tags, preferCurrentChar: chatCharacter }); - let avatarChar = character; - if (args.avatar) { - avatarChar = characters.find(x => x.avatar == args.avatar) ?? characters.find(x => equalsIgnoreCaseAndAccents(x.name, args.avatar)); - if (!avatarChar) { - toastr.warning(`Character for avatar ${args.avatar} not found`); - return ''; - } + const avatarCharacter = args.avatar ? findCharByName(args.avatar) : character; + if (args.avatar && !avatarCharacter) { + toastr.warning(`Character for avatar ${args.avatar} not found`); + return ''; } let force_avatar, original_avatar; - if (chatCharacter === avatarChar || isNeutralCharacter) { + if (chatCharacter === avatarCharacter || isNeutralCharacter) { // If the targeted character is the currently selected one in a solo chat, we don't need to force any avatars } - else if (avatarChar && avatarChar.avatar !== 'none') { - force_avatar = getThumbnailUrl('avatar', avatarChar.avatar); - original_avatar = avatarChar.avatar; + else if (avatarCharacter && avatarCharacter.avatar !== 'none') { + force_avatar = getThumbnailUrl('avatar', avatarCharacter.avatar); + original_avatar = avatarCharacter.avatar; } else { force_avatar = default_avatar; diff --git a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js index 5612f47b5..07df46b3e 100644 --- a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js +++ b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js @@ -193,7 +193,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)); }, From 0be48c567aaf94c922b77a0afaf2b70782d5ddc2 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sun, 29 Sep 2024 00:36:13 +0200 Subject: [PATCH 07/34] /char-find command to get a specific unique char - findChar utility function that does the heavy lifting of finding a specific char based on conditions - Log/warn if multiple characters match - Validation function for named args that should be arrays --- public/scripts/slash-commands.js | 134 ++++++++++++++++-- public/scripts/slash-commands/SlashCommand.js | 4 +- .../SlashCommandCommonEnumsProvider.js | 14 +- 3 files changed, 138 insertions(+), 14 deletions(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 7ca6187cb..b94ddfbf0 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -71,6 +71,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js'; import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js'; import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js'; +import { getTagsList } from './tags.js'; export { executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand, }; @@ -173,6 +174,65 @@ 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'); + + const char = findChar({ name: name, filteredByTags: validateArrayArgString(args.tag, 'tag'), preferCurrentChar: isTrueBoolean(args.preferCurrent) }); + 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', + }), + ], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'Character name', + typeList: [ARGUMENT_TYPE.STRING], + enumProvider: commonEnumProviders.characters('character'), + forceEnum: false, + }), + ], + helpString: ` +
+ Searches for a character and returns its avatar key. +
+
+ 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. +
+
+ Example: +
    +
  • +
    /char-find name="Chloe"
    + Returns the avatar key for "Chloe". +
  • +
  • +
    /search name="Chloe" tag="friend"
    + Returns the avatar key for the character "Chloe" that is tagged with "friend". + This is useful if you for example have multiple characters named "Chloe", and the others are "foe", "goddess", or anything else, + so you can actually select the character you are looking for. +
  • +
+
+ `, + })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'sendas', callback: sendMessageAs, @@ -3116,42 +3176,94 @@ 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; +} + /** * Finds a character by name, with optional filtering and precedence for avatars - * @param {string} name - The name to search for * @param {object} [options={}] - The options for the search + * @param {string?} [options.name=null] - The name to search for * @param {boolean} [options.allowAvatar=false] - 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 {any?} [options.preferCurrentChar=null] - The current character to prefer + * @param {boolean} [options.preferCurrentChar=false] - 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 findCharByName(name, { allowAvatar = false, insensitive = true, filteredByTags = null, preferCurrentChar = null } = {}) { - const matches = (char) => (allowAvatar && char.avatar === name) || insensitive ? equalsIgnoreCaseAndAccents(char.name, name) : char.name === name; +export function findChar({ name = null, allowAvatar = false, insensitive = true, filteredByTags = null, preferCurrentChar = false, quiet = false } = {}) { + const matches = (char) => (allowAvatar && char.avatar === name) || (insensitive ? equalsIgnoreCaseAndAccents(char.name, name) : char.name === name); + + // Get the current character(s) + const currentChars = selected_group ? groups.find(group => group.id === selected_group)?.members.map(member => characters.find(char => char.avatar === member)) : [characters[this_chid]]; // If we have a current char and prefer it, return that if it matches - unless tags are provided, they have precedence - if (preferCurrentChar && !filteredByTags && matches(preferCurrentChar)) { - return preferCurrentChar; + if (preferCurrentChar && !filteredByTags) { + const preferredChar = currentChars.find(matches); + if (preferredChar) { + return preferredChar; + } } // Filter characters by tags if provided let filteredCharacters = characters; if (filteredByTags) { - filteredCharacters = characters.filter(char => filteredByTags.every(tag => char.tags.includes(tag))); + filteredCharacters = characters.filter(char => { + const charTags = getTagsList(char.avatar, false); + return filteredByTags.every(tagName => charTags.some(x => x.name == tagName)); + }); } // If allowAvatar is true, search by avatar first - if (allowAvatar) { + if (allowAvatar && name) { const characterByAvatar = filteredCharacters.find(char => char.avatar === name); if (characterByAvatar) { return characterByAvatar; } } - // Search for a matching character by name - let character = filteredCharacters.find(matches); + // 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 name "${name}" and given conditions.`); + else console.warn(`Multiple characters found for name "${name}". Returning the first match.`); + } - return character; + return matchingCharacters[0] || null; } export async function sendMessageAs(args, text) { diff --git a/public/scripts/slash-commands/SlashCommand.js b/public/scripts/slash-commands/SlashCommand.js index 5f8726ba0..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|(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|(string|SlashCommandClosure)[]}} NamedArgumentsCapture + * @typedef {{[id:string]:string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined}} NamedArgumentsCapture */ /** diff --git a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js index 07df46b3e..4dd22c8fe 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'; @@ -181,6 +181,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 * From 231ea98b25e227e7b273115cebab7178c8866b52 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sun, 29 Sep 2024 00:53:43 +0200 Subject: [PATCH 08/34] Update /sendas to work with findChar --- public/scripts/slash-commands.js | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index b94ddfbf0..3e67f98ed 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -251,13 +251,6 @@ export function initDefaultSlashCommands() { typeList: [ARGUMENT_TYPE.STRING], enumProvider: commonEnumProviders.characters('character'), }), - 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. (does not apply to the avatar argument)', - typeList: [ARGUMENT_TYPE.STRING], - enumProvider: commonEnumProviders.tagsForChar('all'), - acceptsMultiple: true, - }), SlashCommandNamedArgument.fromProps({ name: 'compact', description: 'Use compact layout', @@ -3218,14 +3211,14 @@ export function validateArrayArg(arg, name, { allowUndefined = true } = {}) { * 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=false] - Whether to allow searching by avatar + * @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=false] - Whether to prefer the current character(s) + * @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 = false, insensitive = true, filteredByTags = null, preferCurrentChar = false, quiet = false } = {}) { +export function findChar({ name = null, allowAvatar = true, insensitive = true, filteredByTags = null, preferCurrentChar = true, quiet = false } = {}) { const matches = (char) => (allowAvatar && char.avatar === name) || (insensitive ? equalsIgnoreCaseAndAccents(char.name, name) : char.name === name); // Get the current character(s) @@ -3304,19 +3297,18 @@ export async function sendMessageAs(args, text) { const isSystem = bias && !removeMacros(mesText).length; const compact = isTrueBoolean(args?.compact); - const chatCharacter = this_chid !== undefined ? characters[this_chid] : null; - const isNeutralCharacter = !chatCharacter && name2 === neutralCharacterName && name === neutralCharacterName; + const character = findChar({ name: name }); - const character = findCharByName(name, { filteredByTags: args?.tags, preferCurrentChar: chatCharacter }); - - const avatarCharacter = args.avatar ? findCharByName(args.avatar) : character; + const avatarCharacter = args.avatar ? findChar({ name: args.avatar }) : character; if (args.avatar && !avatarCharacter) { toastr.warning(`Character for avatar ${args.avatar} not found`); return ''; } + const isNeutralCharacter = !character && name2 === neutralCharacterName && name === neutralCharacterName; + let force_avatar, original_avatar; - if (chatCharacter === avatarCharacter || isNeutralCharacter) { + if (character === avatarCharacter || isNeutralCharacter) { // If the targeted character is the currently selected one in a solo chat, we don't need to force any avatars } else if (avatarCharacter && avatarCharacter.avatar !== 'none') { From 5a0e70e8fa377ca9cf6e9671c748852ef17e1de0 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sun, 29 Sep 2024 00:55:41 +0200 Subject: [PATCH 09/34] Add "quiet" to /char-find --- public/scripts/slash-commands.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 3e67f98ed..4a801da8b 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -180,8 +180,9 @@ export function initDefaultSlashCommands() { 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: isTrueBoolean(args.preferCurrent) }); + const char = findChar({ name: name, filteredByTags: validateArrayArgString(args.tag, 'tag'), preferCurrentChar: isTrueBoolean(args.preferCurrent), quiet: isTrueBoolean(args.quiet) }); return char?.avatar ?? ''; }, returns: 'the avatar key (unique identifier) of the character', @@ -199,6 +200,13 @@ export function initDefaultSlashCommands() { 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({ From ca009dee594f4617964d5a9b6f267f50271b201b Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sun, 29 Sep 2024 01:17:13 +0200 Subject: [PATCH 10/34] Respect tags for current char(s) selection - Respect tags for prefer current char(s) - Fix preferCurrentChar not being true by default --- public/scripts/slash-commands.js | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 4a801da8b..0286fb915 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -182,7 +182,7 @@ export function initDefaultSlashCommands() { 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: isTrueBoolean(args.preferCurrent), quiet: isTrueBoolean(args.quiet) }); + 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', @@ -3229,17 +3229,6 @@ export function validateArrayArg(arg, name, { allowUndefined = true } = {}) { export function findChar({ name = null, allowAvatar = true, insensitive = true, filteredByTags = null, preferCurrentChar = true, quiet = false } = {}) { const matches = (char) => (allowAvatar && char.avatar === name) || (insensitive ? equalsIgnoreCaseAndAccents(char.name, name) : char.name === name); - // Get the current character(s) - const currentChars = selected_group ? groups.find(group => group.id === selected_group)?.members.map(member => characters.find(char => char.avatar === member)) : [characters[this_chid]]; - - // If we have a current char and prefer it, return that if it matches - unless tags are provided, they have precedence - if (preferCurrentChar && !filteredByTags) { - const preferredChar = currentChars.find(matches); - if (preferredChar) { - return preferredChar; - } - } - // Filter characters by tags if provided let filteredCharacters = characters; if (filteredByTags) { @@ -3249,6 +3238,23 @@ export function findChar({ name = null, allowAvatar = true, insensitive = true, }); } + // 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.find(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 name "${name}" and given conditions.`); + else console.warn(`Multiple characters found for name "${name}". 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); From d6e52dbb9727ff67ec7d52b664b17efcb363c208 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sun, 29 Sep 2024 02:14:06 +0200 Subject: [PATCH 11/34] Update /ask with new char find functionality --- public/scripts/slash-commands.js | 110 +++++++++++++++++++------------ 1 file changed, 67 insertions(+), 43 deletions(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 0286fb915..2f2e27fdd 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -247,7 +247,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'), @@ -255,7 +255,7 @@ export function initDefaultSlashCommands() { }), SlashCommandNamedArgument.fromProps({ name: 'avatar', - description: 'Character avatar override (Can be either avatar filename or just the character name to pull the avatar from)', + description: 'Character avatar override (Can be either avatar key or just the character name to pull the avatar from)', typeList: [ARGUMENT_TYPE.STRING], enumProvider: commonEnumProviders.characters('character'), }), @@ -288,6 +288,9 @@ export function initDefaultSlashCommands() {
/sendas name="Chloe" Hello, guys!
will send "Hello, guys!" from "Chloe". +
  • +
    /sendas name="Chloe" avatar="BigBadBoss" Hehehe, I am the big bad evil, fear me.
    + will send a message as the character "Chloe", but utilizing the avatar from a character named "BigBadBoss".
    @@ -509,7 +512,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'), @@ -2558,26 +2561,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 @@ -2588,19 +2587,9 @@ 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)) { @@ -2613,7 +2602,7 @@ async function askCharacter(args, text) { // 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; } @@ -2624,7 +2613,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(); @@ -3215,6 +3204,41 @@ export function validateArrayArg(arg, name, { allowUndefined = true } = {}) { 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, + }; +} + /** * Finds a character by name, with optional filtering and precedence for avatars * @param {object} [options={}] - The options for the search @@ -3273,6 +3297,19 @@ export function findChar({ name = null, allowAvatar = true, insensitive = true, 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; +} + export async function sendMessageAs(args, text) { if (!text) { return ''; @@ -3319,23 +3356,10 @@ export async function sendMessageAs(args, text) { return ''; } - const isNeutralCharacter = !character && name2 === neutralCharacterName && name === neutralCharacterName; - - let force_avatar, original_avatar; - if (character === avatarCharacter || isNeutralCharacter) { - // If the targeted character is the currently selected one in a solo chat, we don't need to force any avatars - } - else if (avatarCharacter && avatarCharacter.avatar !== 'none') { - force_avatar = getThumbnailUrl('avatar', avatarCharacter.avatar); - original_avatar = avatarCharacter.avatar; - } - else { - force_avatar = default_avatar; - original_avatar = default_avatar; - } + const { name: nameForMessage, force_avatar, original_avatar } = getNameAndAvatarForMessage(avatarCharacter, name); const message = { - name: character?.name || name, + name: nameForMessage, is_user: false, is_system: isSystem, send_date: getMessageTimeStamp(), From 145023ba8d88fe6ecca29920dbdc86e446efede0 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sun, 29 Sep 2024 02:36:33 +0200 Subject: [PATCH 12/34] Update existing slash commands to findChar - Update /gen to utilize new char find functionality - Update /go to utilize new char find functionality - Update /delname to utilize new char find functionality - Change /send persona search to equalsIgnoreCaseAndAccents --- public/scripts/slash-commands.js | 54 +++++++++++++++++--------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 2f2e27fdd..95e8586ec 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -461,12 +461,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', @@ -531,7 +533,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'), @@ -936,7 +938,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)], @@ -2373,7 +2375,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); @@ -2895,7 +2898,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]; } } @@ -2938,7 +2941,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) => { @@ -2996,31 +3001,28 @@ async function goToCharacterCallback(_, name) { 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(); } From edcf52e3a8ece77bb845b85d20465b3a8dccfb0f Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sun, 29 Sep 2024 02:54:12 +0200 Subject: [PATCH 13/34] Update more commands for new char find - Update /member-add to utilize new char find functionality - Update /tag-add, /tag-remove, /tag-exists and /tag-list to utilize new char find functionality . Update /lastsprite to utilize new char find functionality --- public/scripts/extensions/expressions/index.js | 11 +++++++++-- public/scripts/extensions/gallery/index.js | 1 + public/scripts/slash-commands.js | 17 +++++++---------- public/scripts/tags.js | 14 ++++++++------ 4 files changed, 25 insertions(+), 18 deletions(-) diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index 71be1e422..dc8ce8ba6 100644 --- a/public/scripts/extensions/expressions/index.js +++ b/public/scripts/extensions/expressions/index.js @@ -12,6 +12,7 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from ' import { isFunctionCallingSupported } from '../../openai.js'; import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js'; import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; +import { findChar } from '../../slash-commands.js'; export { MODULE_NAME }; const MODULE_NAME = 'expressions'; @@ -2105,14 +2106,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/slash-commands.js b/public/scripts/slash-commands.js index 95e8586ec..4be6bca7b 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -210,7 +210,7 @@ export function initDefaultSlashCommands() { ], unnamedArgumentList: [ SlashCommandArgument.fromProps({ - description: 'Character name', + description: 'Character name - or unique character identifier (avatar key)', typeList: [ARGUMENT_TYPE.STRING], enumProvider: commonEnumProviders.characters('character'), forceEnum: false, @@ -700,7 +700,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')() : [], @@ -2810,26 +2810,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)) { diff --git a/public/scripts/tags.js b/public/scripts/tags.js index c56e0d1cb..447e01ded 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -26,6 +26,7 @@ import { debounce_timeout } from './constants.js'; import { INTERACTABLE_CONTROL_CLASS } from './keyboard.js'; import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js'; import { renderTemplateAsync } from './templates.js'; +import { findChar } from './slash-commands.js'; export { TAG_FOLDER_TYPES, @@ -507,7 +508,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 +1862,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 +1909,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 +1952,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 +1995,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(), From d7bad6335c34744a7c58875c1d075d61f69cc333 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sun, 29 Sep 2024 03:20:01 +0200 Subject: [PATCH 14/34] Refactor findChar to utils - Refactor and move finChar to utils, instead of slashcommands function - Refactor scrapers to use actual init functionality --- public/script.js | 3 +- .../scripts/extensions/expressions/index.js | 3 +- public/scripts/scrapers.js | 21 ++-- public/scripts/slash-commands.js | 98 +------------------ public/scripts/tags.js | 4 +- public/scripts/utils.js | 75 +++++++++++++- 6 files changed, 94 insertions(+), 110 deletions(-) diff --git a/public/script.js b/public/script.js index 6d93bc03b..b0ca08f6a 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(); + initScrapers(); doDailyExtensionUpdatesCheck(); await hideLoader(); await fixViewport(); diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index dc8ce8ba6..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'; @@ -12,7 +12,6 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from ' import { isFunctionCallingSupported } from '../../openai.js'; import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js'; import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; -import { findChar } from '../../slash-commands.js'; export { MODULE_NAME }; const MODULE_NAME = 'expressions'; diff --git a/public/scripts/scrapers.js b/public/scripts/scrapers.js index 86d2ba567..09d42edb1 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 */ @@ -42,6 +43,10 @@ export class ScraperManager { return; } + if (scraper.init) { + 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 function initScrapers() { + ScraperManager.registerDataBankScraper(new FileScraper()); + ScraperManager.registerDataBankScraper(new Notepad()); + ScraperManager.registerDataBankScraper(new WebScraper()); + ScraperManager.registerDataBankScraper(new MediaWikiScraper()); + ScraperManager.registerDataBankScraper(new FandomScraper()); + ScraperManager.registerDataBankScraper(new YouTubeScraper()); +} diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 4be6bca7b..1e961beaa 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, equalsIgnoreCaseAndAccents, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js'; +import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, 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,10 +68,8 @@ 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'; -import { getTagsList } from './tags.js'; export { executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand, }; @@ -2969,29 +2967,6 @@ 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'); @@ -3238,77 +3213,6 @@ export function getNameAndAvatarForMessage(character, name = 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) => (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.find(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 name "${name}" and given conditions.`); - else console.warn(`Multiple characters found for name "${name}". 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 name "${name}" and given conditions.`); - else console.warn(`Multiple characters found for name "${name}". 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; -} - export async function sendMessageAs(args, text) { if (!text) { return ''; diff --git a/public/scripts/tags.js b/public/scripts/tags.js index 447e01ded..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'; @@ -26,7 +26,6 @@ import { debounce_timeout } from './constants.js'; import { INTERACTABLE_CONTROL_CLASS } from './keyboard.js'; import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js'; import { renderTemplateAsync } from './templates.js'; -import { findChar } from './slash-commands.js'; export { TAG_FOLDER_TYPES, @@ -51,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'; diff --git a/public/scripts/utils.js b/public/scripts/utils.js index 9fc1e1e99..9ddd0413c 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) => (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.find(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 name "${name}" and given conditions.`); + else console.warn(`Multiple characters found for name "${name}". 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 name "${name}" and given conditions.`); + else console.warn(`Multiple characters found for name "${name}". 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; +} From 44c8d45957d99190489a449f4705301ac9984eb5 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sun, 29 Sep 2024 04:02:00 +0200 Subject: [PATCH 15/34] missing import --- public/scripts/slash-commands.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 1e961beaa..157843515 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, equalsIgnoreCaseAndAccents, findChar, 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'; From ab83138b1ee682baf193980b589c64fb08cf5e00 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sun, 29 Sep 2024 11:02:54 +0200 Subject: [PATCH 16/34] missing closing tag --- public/scripts/slash-commands.js | 1 + 1 file changed, 1 insertion(+) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 157843515..64f6fc83d 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -289,6 +289,7 @@ export function initDefaultSlashCommands() {
  • /sendas name="Chloe" avatar="BigBadBoss" Hehehe, I am the big bad evil, fear me.
    will send a message as the character "Chloe", but utilizing the avatar from a character named "BigBadBoss". +
  • From cc527a3a3335c29bce38cf8c23bb097a4f51c7e6 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sun, 29 Sep 2024 11:27:07 +0200 Subject: [PATCH 17/34] Correct async calls to init scrapers --- public/script.js | 2 +- public/scripts/scrapers.js | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/public/script.js b/public/script.js index b0ca08f6a..b64104cc5 100644 --- a/public/script.js +++ b/public/script.js @@ -959,7 +959,7 @@ async function firstLoadInit() { initCfg(); initLogprobs(); initInputMarkdown(); - initScrapers(); + await initScrapers(); doDailyExtensionUpdatesCheck(); await hideLoader(); await fixViewport(); diff --git a/public/scripts/scrapers.js b/public/scripts/scrapers.js index 09d42edb1..5a21dedd0 100644 --- a/public/scripts/scrapers.js +++ b/public/scripts/scrapers.js @@ -37,14 +37,14 @@ 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) { - scraper.init(); + await scraper.init(); } ScraperManager.#scrapers.push(scraper); @@ -571,11 +571,11 @@ class YouTubeScraper { } } -export function initScrapers() { - 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()); } From c3d5fba598ca07a36ff28b3697fd6403c628682e Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 29 Sep 2024 12:48:40 +0300 Subject: [PATCH 18/34] Support multiple in /setpromptentry --- public/scripts/slash-commands.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 37e99615d..d9bdd4e7a 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -3561,11 +3561,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; From 8e445c75ae706cc636cb6e7b306a695985fc48d0 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sun, 29 Sep 2024 13:38:15 +0200 Subject: [PATCH 19/34] Fix bug with undefined char on empty chats --- public/scripts/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/utils.js b/public/scripts/utils.js index 9ddd0413c..93c752a9c 100644 --- a/public/scripts/utils.js +++ b/public/scripts/utils.js @@ -2139,7 +2139,7 @@ export function findChar({ name = null, allowAvatar = true, insensitive = true, // 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.find(char => characters[this_chid]?.avatar === char.avatar)]; + : 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) { From 9f0c2300d2d36e43c609fbf75d97e9d68a872c1e Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 29 Sep 2024 16:24:40 +0300 Subject: [PATCH 20/34] Add connection map aliases --- public/script.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/public/script.js b/public/script.js index 6d93bc03b..2e8b8d736 100644 --- a/public/script.js +++ b/public/script.js @@ -8476,12 +8476,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', From 7c2fcbc522dc9f80093dde70b1967acad8c113b8 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sun, 29 Sep 2024 17:00:28 +0200 Subject: [PATCH 21/34] Fix non-provided name not correctly preferring cur --- public/scripts/utils.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/public/scripts/utils.js b/public/scripts/utils.js index 93c752a9c..6cf92f5c0 100644 --- a/public/scripts/utils.js +++ b/public/scripts/utils.js @@ -2125,7 +2125,7 @@ export async function showFontAwesomePicker(customList = null) { * @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) => (allowAvatar && char.avatar === name) || (insensitive ? equalsIgnoreCaseAndAccents(char.name, name) : char.name === name); + 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; @@ -2145,8 +2145,8 @@ export function findChar({ name = null, allowAvatar = true, insensitive = true, if (preferCurrentChar) { const preferredCharSearch = currentChars.filter(matches); if (preferredCharSearch.length > 1) { - if (!quiet) toastr.warning(`Multiple characters found for name "${name}" and given conditions.`); - else console.warn(`Multiple characters found for name "${name}". Returning the first match.`); + 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]; @@ -2164,8 +2164,8 @@ export function findChar({ name = null, allowAvatar = true, insensitive = true, // 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 name "${name}" and given conditions.`); - else console.warn(`Multiple characters found for name "${name}". Returning the first match.`); + 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; From 314771fd9a1a5d9ec6ae3677ae0daf4992018565 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sun, 29 Sep 2024 17:32:18 +0200 Subject: [PATCH 22/34] Fix /sendas wrong name being used --- public/scripts/slash-commands.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index aca7e9e03..4efb90e65 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -3260,10 +3260,10 @@ export async function sendMessageAs(args, text) { return ''; } - const { name: nameForMessage, force_avatar, original_avatar } = getNameAndAvatarForMessage(avatarCharacter, name); + const { name: avatarCharName, force_avatar, original_avatar } = getNameAndAvatarForMessage(avatarCharacter, name); const message = { - name: nameForMessage, + name: character?.name || name || avatarCharName, is_user: false, is_system: isSystem, send_date: getMessageTimeStamp(), From 2f43c8e227205a31bb8adb1110ea9d549e4cef17 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 29 Sep 2024 19:12:26 +0300 Subject: [PATCH 23/34] Fix /ask in neutral chats --- public/scripts/slash-commands.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index d9bdd4e7a..eb81a0218 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -2530,8 +2530,13 @@ async function askCharacter(args, text) { 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 From 8061f453687bfc95edb3de7a3d6702dd48c42bd8 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 29 Sep 2024 19:13:19 +0300 Subject: [PATCH 24/34] RossMods: Debounce online status display --- public/scripts/RossAscends-mods.js | 33 +++++++++++++++--------------- 1 file changed, 17 insertions(+), 16 deletions(-) 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 () { From 020741d78b403f302c5f6ee7cc7c928463e040f1 Mon Sep 17 00:00:00 2001 From: RossAscends <124905043+RossAscends@users.noreply.github.com> Date: Mon, 30 Sep 2024 21:49:39 +0900 Subject: [PATCH 25/34] make AF panel toggles red and opaque when disabled --- public/index.html | 69 ++++++++++++++++++++++++----------------------- public/style.css | 23 +++++++++++++--- 2 files changed, 56 insertions(+), 36 deletions(-) diff --git a/public/index.html b/public/index.html index 13f59da05..0aed22dc3 100644 --- a/public/index.html +++ b/public/index.html @@ -3248,25 +3248,26 @@
    +

    +
    + Instruct Template + + + +
    +
    + + +
    +

    -

    -
    - Instruct Template - - - -
    -
    - - -
    -

    +
    @@ -3431,21 +3432,22 @@
    +

    +
    + System Prompt + + + +
    +
    + +
    +

    -

    -
    - System Prompt - - - -
    -
    - -
    -

    +
    @@ -5939,7 +5941,8 @@
    - diff --git a/public/style.css b/public/style.css index 1c8096d86..fc5ab520e 100644 --- a/public/style.css +++ b/public/style.css @@ -978,13 +978,21 @@ 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; + +} +.swipe_left{ +position: absolute; +bottom: 15px; +flex-flow: column; +} + +.swipeRightBlock { position: absolute; - bottom: 15px; + right: 0; + bottom: 0; } .swipes-counter { @@ -994,6 +1002,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 +1014,7 @@ body .panelControlBar { .swipe_right { right: 5px; + align-self:end; } .ui-settings { @@ -2634,6 +2646,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; } From 8cb4aa6a495d4056d9c6d9997d35b0dbc33319f8 Mon Sep 17 00:00:00 2001 From: RossAscends <124905043+RossAscends@users.noreply.github.com> Date: Mon, 30 Sep 2024 22:00:38 +0900 Subject: [PATCH 26/34] whoopsie daisey --- public/index.html | 7 +++---- public/style.css | 10 +--------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/public/index.html b/public/index.html index 0aed22dc3..741871edd 100644 --- a/public/index.html +++ b/public/index.html @@ -5941,10 +5941,9 @@
    -
    - -
    -
    +
    diff --git a/public/style.css b/public/style.css index fc5ab520e..7d55cf832 100644 --- a/public/style.css +++ b/public/style.css @@ -981,19 +981,11 @@ body .panelControlBar { font-size: 30px; cursor: pointer; align-self: center; - -} -.swipe_left{ -position: absolute; + position: absolute; bottom: 15px; flex-flow: column; } -.swipeRightBlock { - position: absolute; - right: 0; - bottom: 0; -} .swipes-counter { color: var(--SmartThemeBodyColor); From dedb96ec8d6f99c1ff8742ca4ae039e43b913926 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 30 Sep 2024 15:05:15 +0000 Subject: [PATCH 27/34] Don't trim on whitespace user prefix sequence --- public/script.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/script.js b/public/script.js index 2e8b8d736..cda7adea3 100644 --- a/public/script.js +++ b/public/script.js @@ -5427,6 +5427,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 +5435,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)); } From 140aeb2bb7516e2890181a4065ae88b63c2966bc Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Mon, 30 Sep 2024 20:22:44 +0200 Subject: [PATCH 28/34] Fix piping not using array for empty unnamed args - Lenny said if `splitUnnamedArgument` is enabled, the callback should always receive an array. - It is intended that if no value was provided, it'll get an array with an empty string. Because.. if no argument was provided for a non-split arg, it'll receive an empty string too. --- public/scripts/slash-commands/SlashCommandClosure.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/public/scripts/slash-commands/SlashCommandClosure.js b/public/scripts/slash-commands/SlashCommandClosure.js index 45c4d48ba..0ba1726b5 100644 --- a/public/scripts/slash-commands/SlashCommandClosure.js +++ b/public/scripts/slash-commands/SlashCommandClosure.js @@ -508,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; } From 0d38e634714ccb254799b390293066e54912bc37 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Mon, 30 Sep 2024 20:28:52 +0200 Subject: [PATCH 29/34] Why join and then split again, eh? - Refactor /add actually using the array provided as the array for the internal `parseNumericSeries` inside `performOperation` --- public/scripts/variables.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/public/scripts/variables.js b/public/scripts/variables.js index e9cd8c653..4e998efae 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,9 +679,8 @@ function parseNumericSeries(value, scope = null) { return [value]; } - const array = value - .split(' ') - .map(i => i.trim()) + const values = Array.isArray(value) ? value : value.split(' '); + const array = values.map(i => i.trim()) .filter(i => i !== '') .map(i => isNaN(Number(i)) ? Number(resolveVariable(i, scope)) : Number(i)) .filter(i => !isNaN(i)); @@ -1595,7 +1594,7 @@ 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({ From 4855f254192de90d39e235c72d3e8511d5581e96 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Mon, 30 Sep 2024 20:45:39 +0200 Subject: [PATCH 30/34] Update /mul, /max and /min definition - Update command definition for /mul, /max and /min to fit the actual code behind, split them too - Add numbersAndVariables enum provider, to centralize --- .../SlashCommandCommonEnumsProvider.js | 29 +++++++++++++++++ public/scripts/variables.js | 32 ++++--------------- 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js index 5612f47b5..a6a589238 100644 --- a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js +++ b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.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. * diff --git a/public/scripts/variables.js b/public/scripts/variables.js index 4e998efae..bb53a487a 100644 --- a/public/scripts/variables.js +++ b/public/scripts/variables.js @@ -1602,28 +1602,7 @@ export function registerVariableCommands() { typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], 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, }), ], @@ -1653,10 +1632,11 @@ export function registerVariableCommands() { typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], 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. @@ -1681,10 +1661,11 @@ export function registerVariableCommands() { typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], 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. @@ -1709,10 +1690,11 @@ export function registerVariableCommands() { typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], 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. From 224249a0d21dd076ebbf73bc990ac8f66e7137f6 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Mon, 30 Sep 2024 20:50:18 +0200 Subject: [PATCH 31/34] Fix /sub actually allowing more than two vals now - Fix by subtracting all from the first value - Update the definition too --- public/scripts/variables.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/public/scripts/variables.js b/public/scripts/variables.js index bb53a487a..5e3449f02 100644 --- a/public/scripts/variables.js +++ b/public/scripts/variables.js @@ -730,7 +730,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) { @@ -1716,14 +1716,15 @@ export function registerVariableCommands() { returns: 'difference of the provided values', unnamedArgumentList: [ SlashCommandArgument.fromProps({ - description: 'values to find the difference', + description: 'values to subtract, starting form the first provided value', typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], 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. From 8ff2ef086be4dd345344b98daf7111ae8ad21778 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Mon, 30 Sep 2024 20:59:42 +0200 Subject: [PATCH 32/34] Update defs and enums for other math commands - Now even the commands like `/div count charnumber` work well with auto complete --- public/scripts/variables.js | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/public/scripts/variables.js b/public/scripts/variables.js index 5e3449f02..b06728359 100644 --- a/public/scripts/variables.js +++ b/public/scripts/variables.js @@ -1749,17 +1749,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. @@ -1784,17 +1785,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. @@ -1819,17 +1821,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. @@ -1854,7 +1857,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, }), ], @@ -1882,7 +1885,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, }), ], @@ -1911,7 +1914,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, }), ], @@ -1939,7 +1942,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, }), ], @@ -1967,7 +1970,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, }), ], @@ -1995,7 +1998,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, }), ], From 2dc7b5ded1e48224447ac4cab179dc3a01a59338 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Mon, 30 Sep 2024 21:24:22 +0200 Subject: [PATCH 33/34] Allow using JSON arrays for math commands - applies to all that receive a list. /add, /sub, /min, /max etc - Parsing is the same as the other commands where we already allow "LIST" as an argument. --- public/scripts/variables.js | 60 +++++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/public/scripts/variables.js b/public/scripts/variables.js index b06728359..0e18f404a 100644 --- a/public/scripts/variables.js +++ b/public/scripts/variables.js @@ -679,10 +679,17 @@ function parseNumericSeries(value, scope = null) { return [value]; } - const values = Array.isArray(value) ? value : value.split(' '); - const array = values.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; @@ -1599,7 +1606,7 @@ export function registerVariableCommands() { 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: commonEnumProviders.numbersAndVariables, @@ -1610,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: @@ -1618,6 +1627,9 @@ export function registerVariableCommands() {
  • /add 10 i 30 j
  • +
  • +
    /add ["count", 15, 2, "i"]
    +
  • `, @@ -1629,7 +1641,7 @@ 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.numbersAndVariables, @@ -1639,7 +1651,10 @@ export function registerVariableCommands() { 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: @@ -1647,6 +1662,9 @@ export function registerVariableCommands() {
  • /mul 10 i 30 j
  • +
  • +
    /mul ["count", 15, 2, "i"]
    +
  • `, @@ -1658,7 +1676,7 @@ 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.numbersAndVariables, @@ -1668,7 +1686,10 @@ export function registerVariableCommands() { 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: @@ -1676,6 +1697,9 @@ export function registerVariableCommands() {
  • /max 10 i 30 j
  • +
  • +
    /max ["count", 15, 2, "i"]
    +
  • `, @@ -1687,7 +1711,7 @@ 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.numbersAndVariables, @@ -1698,7 +1722,9 @@ export function registerVariableCommands() { 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: @@ -1706,6 +1732,9 @@ export function registerVariableCommands() {
  • /min 10 i 30 j
  • +
  • +
    /min ["count", 15, 2, "i"]
    +
  • `, @@ -1717,7 +1746,7 @@ export function registerVariableCommands() { unnamedArgumentList: [ SlashCommandArgument.fromProps({ description: 'values to subtract, starting form the first provided value', - 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.numbersAndVariables, @@ -1728,7 +1757,9 @@ export function registerVariableCommands() { 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: @@ -1736,6 +1767,9 @@ export function registerVariableCommands() {
  • /sub i 5
  • +
  • +
    /sub ["count", 4, "i"]
    +
  • `, From 3fd846fb5b46d876898dde344c26dbcd6da7f222 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 30 Sep 2024 23:52:01 +0300 Subject: [PATCH 34/34] Allow returning literal Infinity string from math operations --- public/scripts/variables.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/variables.js b/public/scripts/variables.js index 0e18f404a..7c9830a36 100644 --- a/public/scripts/variables.js +++ b/public/scripts/variables.js @@ -709,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; }