Allow searching on /sendas via tag=

22d8a654d95763213a7d823adc2ae695ce6ca129

Wolfsblvt <wolfsblvt@gmail.com>

2 files changed, +54 -12Showing whitespace changes
public/scripts/slash-commands.js+53 -11
@@ -187,11 +187,18 @@ export function initDefaultSlashCommands() {
187187 }),
188188 SlashCommandNamedArgument.fromProps({
189189 name: 'avatar',
190190 description: 'Optional characterCharacter avatar override (Can be either avatar filename or just the character name to pull the avatar from)',
191191 typeList: [ARGUMENT_TYPE.STRING],
192192 enumProvider: commonEnumProviders.characters('character'),
193193 }),
194194 SlashCommandNamedArgument.fromProps({
195+ name: 'tag',
196+ 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)',
197+ typeList: [ARGUMENT_TYPE.STRING],
198+ enumProvider: commonEnumProviders.tagsForChar('all'),
199+ acceptsMultiple: true,
200+ }),
201+ SlashCommandNamedArgument.fromProps({
195202 name: 'compact',
196203 description: 'Use compact layout',
197204 typeList: [ARGUMENT_TYPE.BOOLEAN],
@@ -3109,6 +3116,44 @@ async function setNarratorName(_, text) {
31093116 return '';
31103117}
31113118
3119+/**
3120+ * Finds a character by name, with optional filtering and precedence for avatars
3121+ * @param {string} name - The name to search for
3122+ * @param {object} [options={}] - The options for the search
3123+ * @param {boolean} [options.allowAvatar=false] - Whether to allow searching by avatar
3124+ * @param {boolean} [options.insensitive=true] - Whether the search should be case insensitive
3125+ * @param {string[]?} [options.filteredByTags=null] - Tags to filter characters by
3126+ * @param {any?} [options.preferCurrentChar=null] - The current character to prefer
3127+ * @returns {any?} - The found character or null if not found
3128+ */
3129+export function findCharByName(name, { allowAvatar = false, insensitive = true, filteredByTags = null, preferCurrentChar = null } = {}) {
3130+ const matches = (char) => (allowAvatar && char.avatar === name) || insensitive ? equalsIgnoreCaseAndAccents(char.name, name) : char.name === name;
3131+
3132+ // If we have a current char and prefer it, return that if it matches - unless tags are provided, they have precedence
3133+ if (preferCurrentChar && !filteredByTags && matches(preferCurrentChar)) {
3134+ return preferCurrentChar;
3135+ }
3136+
3137+ // Filter characters by tags if provided
3138+ let filteredCharacters = characters;
3139+ if (filteredByTags) {
3140+ filteredCharacters = characters.filter(char => filteredByTags.every(tag => char.tags.includes(tag)));
3141+ }
3142+
3143+ // If allowAvatar is true, search by avatar first
3144+ if (allowAvatar) {
3145+ const characterByAvatar = filteredCharacters.find(char => char.avatar === name);
3146+ if (characterByAvatar) {
3147+ return characterByAvatar;
3148+ }
3149+ }
3150+
3151+ // Search for a matching character by name
3152+ let character = filteredCharacters.find(matches);
3153+
3154+ return character;
3155+}
3156+
31123157export async function sendMessageAs(args, text) {
31133158 if (!text) {
31143159 return '';
@@ -3150,24 +3195,21 @@ export async function sendMessageAs(args, text) {
31503195 const chatCharacter = this_chid !== undefined ? characters[this_chid] : null;
31513196 const isNeutralCharacter = !chatCharacter && name2 === neutralCharacterName && name === neutralCharacterName;
31523197
31533198 const character = equalsIgnoreCaseAndAccentsfindCharByName(chatCharacter.name, name) ? chatCharacter{ filteredByTags: charactersargs?.find(xtags, =>preferCurrentChar: equalsIgnoreCaseAndAccents(x.name,chatCharacter name)});
31543199
3155- let avatarChar = character;
3200+ const avatarCharacter = args.avatar ? findCharByName(args.avatar) : character;
31563201 if (args.avatar && !avatarCharacter) {
3157- avatarChar = characters.find(x => x.avatar == args.avatar) ?? characters.find(x => equalsIgnoreCaseAndAccents(x.name, args.avatar));
3158- if (!avatarChar) {
31593202 toastr.warning(`Character for avatar ${args.avatar} not found`);
31603203 return '';
31613204 }
3162- }
31633205
31643206 let force_avatar, original_avatar;
31653207 if (chatCharacter === avatarCharavatarCharacter || isNeutralCharacter) {
31663208 // If the targeted character is the currently selected one in a solo chat, we don't need to force any avatars
31673209 }
31683210 else if (avatarCharavatarCharacter && avatarCharavatarCharacter.avatar !== 'none') {
31693211 force_avatar = getThumbnailUrl('avatar', avatarCharavatarCharacter.avatar);
31703212 original_avatar = avatarCharavatarCharacter.avatar;
31713213 }
31723214 else {
31733215 force_avatar = default_avatar;
public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js+1 -1
@@ -193,7 +193,7 @@ export const commonEnumProviders = {
193193 if (charName instanceof SlashCommandClosure) throw new Error('Argument \'name\' does not support closures');
194194 const key = searchCharByName(substituteParams(charName), { suppressLogging: true });
195195 const assigned = key ? getTagsList(key) : [];
196196 return tags.filter(it => !key || mode === 'all' || mode === 'existing' && assigned.includes(it) || mode === 'not-existing' && !assigned.includes(it))
197197 .map(tag => new SlashCommandEnumValue(tag.name, null, enumTypes.command, enumIcons.tag));
198198 },
199199