Merge pull request #2918 from SillyTavern/char-find-and-sendas-extended Char find and sendas extended

56e9b51f4ebab8b2e86e160b1ede00337f12442e

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

Signed
9 files changed, +338 -130Ignore whitespace
public/script.js+2 -1
@@ -230,7 +230,7 @@ import { MacrosParser, evaluateMacros, getLastMessageId } from './scripts/macros
230import { currentUser, setUserControls } from './scripts/user.js';230import { currentUser, setUserControls } from './scripts/user.js';
231import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';231import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';
232import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';232import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';
233import { ScraperManager } from './scripts/scrapers.js';233import { initScrapers, ScraperManager } from './scripts/scrapers.js';
234import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js';234import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js';
235import { SlashCommand } from './scripts/slash-commands/SlashCommand.js';235import { SlashCommand } from './scripts/slash-commands/SlashCommand.js';
236import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './scripts/slash-commands/SlashCommandArgument.js';236import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './scripts/slash-commands/SlashCommandArgument.js';
@@ -959,6 +959,7 @@ async function firstLoadInit() {
959 initCfg();959 initCfg();
960 initLogprobs();960 initLogprobs();
961 initInputMarkdown();961 initInputMarkdown();
962 await initScrapers();
962 doDailyExtensionUpdatesCheck();963 doDailyExtensionUpdatesCheck();
963 await hideLoader();964 await hideLoader();
964 await fixViewport();965 await fixViewport();
public/scripts/extensions/expressions/index.js+9 -3
@@ -2,7 +2,7 @@ import { callPopup, eventSource, event_types, generateRaw, getRequestHeaders, ma
2import { dragElement, isMobile } from '../../RossAscends-mods.js';2import { dragElement, isMobile } from '../../RossAscends-mods.js';
3import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';3import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
4import { loadMovingUIState, power_user } from '../../power-user.js';4import { loadMovingUIState, power_user } from '../../power-user.js';
5import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition } from '../../utils.js';5import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition, findChar } from '../../utils.js';
6import { hideMutedSprites } from '../../group-chats.js';6import { hideMutedSprites } from '../../group-chats.js';
7import { isJsonSchemaSupported } from '../../textgen-settings.js';7import { isJsonSchemaSupported } from '../../textgen-settings.js';
8import { debounce_timeout } from '../../constants.js';8import { debounce_timeout } from '../../constants.js';
@@ -2105,14 +2105,20 @@ function migrateSettings() {
2105 }));2105 }));
2106 SlashCommandParser.addCommandObject(SlashCommand.fromProps({2106 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2107 name: 'lastsprite',2107 name: 'lastsprite',
2108 callback: (_, value) => lastExpression[String(value).trim()] ?? '',2108 callback: (_, name) => {
2109 if (typeof name !== 'string') throw new Error('name must be a string');
2110 const char = findChar({ name: name });
2111 const sprite = lastExpression[char?.name ?? name] ?? '';
2112 return sprite;
2113 },
2109 returns: 'the last set sprite / expression for the named character.',2114 returns: 'the last set sprite / expression for the named character.',
2110 unnamedArgumentList: [2115 unnamedArgumentList: [
2111 SlashCommandArgument.fromProps({2116 SlashCommandArgument.fromProps({
2112 description: 'character name',2117 description: 'Character name - or unique character identifier (avatar key)',
2113 typeList: [ARGUMENT_TYPE.STRING],2118 typeList: [ARGUMENT_TYPE.STRING],
2114 isRequired: true,2119 isRequired: true,
2115 enumProvider: commonEnumProviders.characters('character'),2120 enumProvider: commonEnumProviders.characters('character'),
2121 forceEnum: true,
2116 }),2122 }),
2117 ],2123 ],
2118 helpString: 'Returns the last set sprite / expression for the named character.',2124 helpString: 'Returns the last set sprite / expression for the named character.',
public/scripts/extensions/gallery/index.js+1 -0
@@ -441,6 +441,7 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
441 description: 'character name',441 description: 'character name',
442 typeList: [ARGUMENT_TYPE.STRING],442 typeList: [ARGUMENT_TYPE.STRING],
443 enumProvider: commonEnumProviders.characters('character'),443 enumProvider: commonEnumProviders.characters('character'),
444 forceEnum: true,
444 }),445 }),
445 SlashCommandNamedArgument.fromProps({446 SlashCommandNamedArgument.fromProps({
446 name: 'group',447 name: 'group',
public/scripts/scrapers.js+16 -7
@@ -13,6 +13,7 @@ import { isValidUrl } from './utils.js';
13 * @property {string} description13 * @property {string} description
14 * @property {string} iconClass14 * @property {string} iconClass
15 * @property {boolean} iconAvailable15 * @property {boolean} iconAvailable
16 * @property {() => Promise<void>} [init=null]
16 * @property {() => Promise<boolean>} isAvailable17 * @property {() => Promise<boolean>} isAvailable
17 * @property {() => Promise<File[]>} scrape18 * @property {() => Promise<File[]>} scrape
18 */19 */
@@ -36,12 +37,16 @@ export class ScraperManager {
36 * Register a scraper to be used by the Data Bank.37 * Register a scraper to be used by the Data Bank.
37 * @param {Scraper} scraper Instance of a scraper to register38 * @param {Scraper} scraper Instance of a scraper to register
38 */39 */
39 static registerDataBankScraper(scraper) {40 static async registerDataBankScraper(scraper) {
40 if (ScraperManager.#scrapers.some(s => s.id === scraper.id)) {41 if (ScraperManager.#scrapers.some(s => s.id === scraper.id)) {
41 console.warn(`Scraper with ID ${scraper.id} already registered`);42 console.warn(`Scraper with ID ${scraper.id} already registered`);
42 return;43 return;
43 }44 }
4445
46 if (scraper.init) {
47 await scraper.init();
48 }
49
45 ScraperManager.#scrapers.push(scraper);50 ScraperManager.#scrapers.push(scraper);
46 }51 }
4752
@@ -462,7 +467,9 @@ class YouTubeScraper {
462 this.description = 'Download a transcript from a YouTube video.';467 this.description = 'Download a transcript from a YouTube video.';
463 this.iconClass = 'fa-brands fa-youtube';468 this.iconClass = 'fa-brands fa-youtube';
464 this.iconAvailable = true;469 this.iconAvailable = true;
470 }
465471
472 async init() {
466 SlashCommandParser.addCommandObject(SlashCommand.fromProps({473 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
467 name: 'yt-script',474 name: 'yt-script',
468 callback: async (args, url) => {475 callback: async (args, url) => {
@@ -564,9 +571,11 @@ class YouTubeScraper {
564 }571 }
565}572}
566573
567ScraperManager.registerDataBankScraper(new FileScraper());574export async function initScrapers() {
568ScraperManager.registerDataBankScraper(new Notepad());575 await ScraperManager.registerDataBankScraper(new FileScraper());
569ScraperManager.registerDataBankScraper(new WebScraper());576 await ScraperManager.registerDataBankScraper(new Notepad());
570ScraperManager.registerDataBankScraper(new MediaWikiScraper());577 await ScraperManager.registerDataBankScraper(new WebScraper());
571ScraperManager.registerDataBankScraper(new FandomScraper());578 await ScraperManager.registerDataBankScraper(new MediaWikiScraper());
572ScraperManager.registerDataBankScraper(new YouTubeScraper());579 await ScraperManager.registerDataBankScraper(new FandomScraper());
580 await ScraperManager.registerDataBankScraper(new YouTubeScraper());
581}
public/scripts/slash-commands.js+212 -106
@@ -55,7 +55,7 @@ import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockStat
55import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';55import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
56import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';56import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
57import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';57import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';
58import { debounce, delay, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';58import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
59import { registerVariableCommands, resolveVariable } from './variables.js';59import { registerVariableCommands, resolveVariable } from './variables.js';
60import { background_settings } from './backgrounds.js';60import { background_settings } from './backgrounds.js';
61import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';61import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
@@ -68,7 +68,6 @@ import { SlashCommandNamedArgumentAssignment } from './slash-commands/SlashComma
68import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';68import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
69import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';69import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
70import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';70import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
71import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js';
72import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';71import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
73import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';72import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
74export {73export {
@@ -174,20 +173,96 @@ export function initDefaultSlashCommands() {
174 `,173 `,
175 }));174 }));
176 SlashCommandParser.addCommandObject(SlashCommand.fromProps({175 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
176 name: 'char-find',
177 aliases: ['findchar'],
178 callback: (args, name) => {
179 if (typeof name !== 'string') throw new Error('name must be a string');
180 if (args.preferCurrent instanceof SlashCommandClosure || Array.isArray(args.preferCurrent)) throw new Error('preferCurrent cannot be a closure or array');
181 if (args.quiet instanceof SlashCommandClosure || Array.isArray(args.quiet)) throw new Error('quiet cannot be a closure or array');
182
183 const char = findChar({ name: name, filteredByTags: validateArrayArgString(args.tag, 'tag'), preferCurrentChar: !isFalseBoolean(args.preferCurrent), quiet: isTrueBoolean(args.quiet) });
184 return char?.avatar ?? '';
185 },
186 returns: 'the avatar key (unique identifier) of the character',
187 namedArgumentList: [
188 SlashCommandNamedArgument.fromProps({
189 name: 'tag',
190 description: 'Supply one or more tags to filter down to the correct character for the provided name, if multiple characters have the same name.',
191 typeList: [ARGUMENT_TYPE.STRING],
192 enumProvider: commonEnumProviders.tags('assigned'),
193 acceptsMultiple: true,
194 }),
195 SlashCommandNamedArgument.fromProps({
196 name: 'preferCurrent',
197 description: 'Prefer current character or characters in a group, if multiple characters match',
198 typeList: [ARGUMENT_TYPE.BOOLEAN],
199 defaultValue: 'true',
200 }),
201 SlashCommandNamedArgument.fromProps({
202 name: 'quiet',
203 description: 'Do not show warning if multiple charactrers are found',
204 typeList: [ARGUMENT_TYPE.BOOLEAN],
205 defaultValue: 'false',
206 enumProvider: commonEnumProviders.boolean('trueFalse'),
207 }),
208 ],
209 unnamedArgumentList: [
210 SlashCommandArgument.fromProps({
211 description: 'Character name - or unique character identifier (avatar key)',
212 typeList: [ARGUMENT_TYPE.STRING],
213 enumProvider: commonEnumProviders.characters('character'),
214 forceEnum: false,
215 }),
216 ],
217 helpString: `
218 <div>
219 Searches for a character and returns its avatar key.
220 </div>
221 <div>
222 This can be used to choose the correct character for something like <code>/sendas</code> or other commands in need of a character name
223 if you have multiple characters with the same name.
224 </div>
225 <div>
226 <strong>Example:</strong>
227 <ul>
228 <li>
229 <pre><code>/char-find name="Chloe"</code></pre>
230 Returns the avatar key for "Chloe".
231 </li>
232 <li>
233 <pre><code>/search name="Chloe" tag="friend"</code></pre>
234 Returns the avatar key for the character "Chloe" that is tagged with "friend".
235 This is useful if you for example have multiple characters named "Chloe", and the others are "foe", "goddess", or anything else,
236 so you can actually select the character you are looking for.
237 </li>
238 </ul>
239 </div>
240 `,
241 }));
242 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
177 name: 'sendas',243 name: 'sendas',
178 callback: sendMessageAs,244 callback: sendMessageAs,
179 namedArgumentList: [245 namedArgumentList: [
180 SlashCommandNamedArgument.fromProps({246 SlashCommandNamedArgument.fromProps({
181 name: 'name',247 name: 'name',
182 description: 'Character name',248 description: 'Character name - or unique character identifier (avatar key)',
183 typeList: [ARGUMENT_TYPE.STRING],249 typeList: [ARGUMENT_TYPE.STRING],
184 isRequired: true,250 isRequired: true,
185 enumProvider: commonEnumProviders.characters('character'),251 enumProvider: commonEnumProviders.characters('character'),
186 forceEnum: false,252 forceEnum: false,
187 }),253 }),
188 new SlashCommandNamedArgument(254 SlashCommandNamedArgument.fromProps({
189 'compact', 'Use compact layout', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',255 name: 'avatar',
190 ),256 description: 'Character avatar override (Can be either avatar key or just the character name to pull the avatar from)',
257 typeList: [ARGUMENT_TYPE.STRING],
258 enumProvider: commonEnumProviders.characters('character'),
259 }),
260 SlashCommandNamedArgument.fromProps({
261 name: 'compact',
262 description: 'Use compact layout',
263 typeList: [ARGUMENT_TYPE.BOOLEAN],
264 defaultValue: 'false',
265 }),
191 SlashCommandNamedArgument.fromProps({266 SlashCommandNamedArgument.fromProps({
192 name: 'at',267 name: 'at',
193 description: 'position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values are accepted and will work similarly to how \'depth\' usually works. For example, -1 will insert the message right before the last message in chat.',268 description: 'position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values are accepted and will work similarly to how \'depth\' usually works. For example, -1 will insert the message right before the last message in chat.',
@@ -211,6 +286,10 @@ export function initDefaultSlashCommands() {
211 <pre><code>/sendas name="Chloe" Hello, guys!</code></pre>286 <pre><code>/sendas name="Chloe" Hello, guys!</code></pre>
212 will send "Hello, guys!" from "Chloe".287 will send "Hello, guys!" from "Chloe".
213 </li>288 </li>
289 <li>
290 <pre><code>/sendas name="Chloe" avatar="BigBadBoss" Hehehe, I am the big bad evil, fear me.</code></pre>
291 will send a message as the character "Chloe", but utilizing the avatar from a character named "BigBadBoss".
292 </li>
214 </ul>293 </ul>
215 </div>294 </div>
216 <div>295 <div>
@@ -381,12 +460,14 @@ export function initDefaultSlashCommands() {
381 SlashCommandParser.addCommandObject(SlashCommand.fromProps({460 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
382 name: 'go',461 name: 'go',
383 callback: goToCharacterCallback,462 callback: goToCharacterCallback,
463 returns: 'The character/group name',
384 unnamedArgumentList: [464 unnamedArgumentList: [
385 SlashCommandArgument.fromProps({465 SlashCommandArgument.fromProps({
386 description: 'name',466 description: 'Character name - or unique character identifier (avatar key)',
387 typeList: [ARGUMENT_TYPE.STRING],467 typeList: [ARGUMENT_TYPE.STRING],
388 isRequired: true,468 isRequired: true,
389 enumProvider: commonEnumProviders.characters('all'),469 enumProvider: commonEnumProviders.characters('all'),
470 forceEnum: true,
390 }),471 }),
391 ],472 ],
392 helpString: 'Opens up a chat with the character or group by its name',473 helpString: 'Opens up a chat with the character or group by its name',
@@ -432,7 +513,7 @@ export function initDefaultSlashCommands() {
432 namedArgumentList: [513 namedArgumentList: [
433 SlashCommandNamedArgument.fromProps({514 SlashCommandNamedArgument.fromProps({
434 name: 'name',515 name: 'name',
435 description: 'character name',516 description: 'Character name - or unique character identifier (avatar key)',
436 typeList: [ARGUMENT_TYPE.STRING],517 typeList: [ARGUMENT_TYPE.STRING],
437 isRequired: true,518 isRequired: true,
438 enumProvider: commonEnumProviders.characters('character'),519 enumProvider: commonEnumProviders.characters('character'),
@@ -451,7 +532,7 @@ export function initDefaultSlashCommands() {
451 namedArgumentList: [],532 namedArgumentList: [],
452 unnamedArgumentList: [533 unnamedArgumentList: [
453 SlashCommandArgument.fromProps({534 SlashCommandArgument.fromProps({
454 description: 'name',535 description: 'Character name - or unique character identifier (avatar key)',
455 typeList: [ARGUMENT_TYPE.STRING],536 typeList: [ARGUMENT_TYPE.STRING],
456 isRequired: true,537 isRequired: true,
457 enumProvider: commonEnumProviders.characters('character'),538 enumProvider: commonEnumProviders.characters('character'),
@@ -618,7 +699,7 @@ export function initDefaultSlashCommands() {
618 aliases: ['addmember', 'memberadd'],699 aliases: ['addmember', 'memberadd'],
619 unnamedArgumentList: [700 unnamedArgumentList: [
620 SlashCommandArgument.fromProps({701 SlashCommandArgument.fromProps({
621 description: 'character name',702 description: 'Character name - or unique character identifier (avatar key)',
622 typeList: [ARGUMENT_TYPE.STRING],703 typeList: [ARGUMENT_TYPE.STRING],
623 isRequired: true,704 isRequired: true,
624 enumProvider: () => selected_group ? commonEnumProviders.characters('character')() : [],705 enumProvider: () => selected_group ? commonEnumProviders.characters('character')() : [],
@@ -856,7 +937,7 @@ export function initDefaultSlashCommands() {
856 ),937 ),
857 SlashCommandNamedArgument.fromProps({938 SlashCommandNamedArgument.fromProps({
858 name: 'name',939 name: 'name',
859 description: 'in-prompt name for instruct mode',940 description: 'in-prompt character name for instruct mode (or unique character identifier (avatar key), which will be used as name)',
860 typeList: [ARGUMENT_TYPE.STRING],941 typeList: [ARGUMENT_TYPE.STRING],
861 defaultValue: 'System',942 defaultValue: 'System',
862 enumProvider: () => [...commonEnumProviders.characters('character')(), new SlashCommandEnumValue('System', null, enumTypes.enum, enumIcons.assistant)],943 enumProvider: () => [...commonEnumProviders.characters('character')(), new SlashCommandEnumValue('System', null, enumTypes.enum, enumIcons.assistant)],
@@ -2293,7 +2374,8 @@ async function generateCallback(args, value) {
22932374
2294 setEphemeralStopStrings(resolveVariable(args?.stop));2375 setEphemeralStopStrings(resolveVariable(args?.stop));
2295 const name = args?.name;2376 const name = args?.name;
2296 const result = await generateQuietPrompt(value, quietToLoud, false, '', name, length);2377 const char = findChar({ name: name });
2378 const result = await generateQuietPrompt(value, quietToLoud, false, '', char?.name ?? name, length);
2297 return result;2379 return result;
2298 } catch (err) {2380 } catch (err) {
2299 console.error('Error on /gen generation', err);2381 console.error('Error on /gen generation', err);
@@ -2481,26 +2563,22 @@ async function askCharacter(args, text) {
2481 return '';2563 return '';
2482 }2564 }
24832565
2484 let name = '';2566 if (!args.name) {
24852567 toastr.warning('You must specify a name of the character to ask.');
2486 if (args?.name) {2568 return '';
2487 name = args.name.trim();
2488
2489 if (!name) {
2490 toastr.warning('You must specify a name of the character to ask.');
2491 return '';
2492 }
2493 }2569 }
24942570
2495 const prevChId = this_chid;2571 const prevChId = this_chid;
24962572
2497 // Find the character2573 // Find the character
2498 const chId = characters.findIndex((e) => e.name === name || e.avatar === name);2574 const character = findChar({ name: args?.name });
2499 if (!characters[chId] || chId === -1) {2575 if (!character) {
2500 toastr.error('Character not found.');2576 toastr.error('Character not found.');
2501 return '';2577 return '';
2502 }2578 }
25032579
2580 const chId = getCharIndex(character);
2581
2504 if (text) {2582 if (text) {
2505 const mesText = getRegexedString(text.trim(), regex_placement.SLASH_COMMAND);2583 const mesText = getRegexedString(text.trim(), regex_placement.SLASH_COMMAND);
2506 // Sending a message implicitly saves the chat, so this needs to be done before changing the character2584 // Sending a message implicitly saves the chat, so this needs to be done before changing the character
@@ -2511,19 +2589,9 @@ async function askCharacter(args, text) {
2511 // Override character and send a user message2589 // Override character and send a user message
2512 setCharacterId(String(chId));2590 setCharacterId(String(chId));
25132591
2514 const character = characters[chId];2592 const { name, force_avatar, original_avatar } = getNameAndAvatarForMessage(character, args?.name);
2515 let force_avatar, original_avatar;
2516
2517 if (character && character.avatar !== 'none') {
2518 force_avatar = getThumbnailUrl('avatar', character.avatar);
2519 original_avatar = character.avatar;
2520 }
2521 else {
2522 force_avatar = default_avatar;
2523 original_avatar = default_avatar;
2524 }
25252593
2526 setCharacterName(character.name);2594 setCharacterName(name);
25272595
2528 const restoreCharacter = () => {2596 const restoreCharacter = () => {
2529 if (String(this_chid) !== String(chId)) {2597 if (String(this_chid) !== String(chId)) {
@@ -2541,7 +2609,7 @@ async function askCharacter(args, text) {
2541 // Only force the new avatar if the character name is the same2609 // Only force the new avatar if the character name is the same
2542 // This skips if an error was fired2610 // This skips if an error was fired
2543 const lastMessage = chat[chat.length - 1];2611 const lastMessage = chat[chat.length - 1];
2544 if (lastMessage && lastMessage?.name === character.name) {2612 if (lastMessage && lastMessage?.name === name) {
2545 lastMessage.force_avatar = force_avatar;2613 lastMessage.force_avatar = force_avatar;
2546 lastMessage.original_avatar = original_avatar;2614 lastMessage.original_avatar = original_avatar;
2547 }2615 }
@@ -2552,7 +2620,7 @@ async function askCharacter(args, text) {
2552 // Run generate and restore previous character2620 // Run generate and restore previous character
2553 try {2621 try {
2554 eventSource.once(event_types.MESSAGE_RECEIVED, restoreCharacter);2622 eventSource.once(event_types.MESSAGE_RECEIVED, restoreCharacter);
2555 toastr.info(`Asking ${character.name} something...`);2623 toastr.info(`Asking ${name} something...`);
2556 askResult = await Generate('ask_command');2624 askResult = await Generate('ask_command');
2557 } catch (error) {2625 } catch (error) {
2558 restoreCharacter();2626 restoreCharacter();
@@ -2746,26 +2814,23 @@ async function removeGroupMemberCallback(_, arg) {
2746 return '';2814 return '';
2747}2815}
27482816
2749async function addGroupMemberCallback(_, arg) {2817async function addGroupMemberCallback(_, name) {
2750 if (!selected_group) {2818 if (!selected_group) {
2751 toastr.warning('Cannot run /memberadd command outside of a group chat.');2819 toastr.warning('Cannot run /memberadd command outside of a group chat.');
2752 return '';2820 return '';
2753 }2821 }
27542822
2755 if (!arg) {2823 if (!name) {
2756 console.warn('WARN: No argument provided for /memberadd command');2824 console.warn('WARN: No argument provided for /memberadd command');
2757 return '';2825 return '';
2758 }2826 }
27592827
2760 arg = arg.trim();2828 const character = findChar({ name: name, preferCurrentChar: false });
2761 const chid = findCharacterIndex(arg);2829 if (!character) {
27622830 console.warn(`WARN: No character found for argument ${name}`);
2763 if (chid === -1) {
2764 console.warn(`WARN: No character found for argument ${arg}`);
2765 return '';2831 return '';
2766 }2832 }
27672833
2768 const character = characters[chid];
2769 const group = groups.find(x => x.id === selected_group);2834 const group = groups.find(x => x.id === selected_group);
27702835
2771 if (!group || !Array.isArray(group.members)) {2836 if (!group || !Array.isArray(group.members)) {
@@ -2834,7 +2899,7 @@ function findPersonaByName(name) {
2834 }2899 }
28352900
2836 for (const persona of Object.entries(power_user.personas)) {2901 for (const persona of Object.entries(power_user.personas)) {
2837 if (persona[1].toLowerCase() === name.toLowerCase()) {2902 if (equalsIgnoreCaseAndAccents(persona[1], name)) {
2838 return persona[0];2903 return persona[0];
2839 }2904 }
2840 }2905 }
@@ -2877,7 +2942,9 @@ async function deleteMessagesByNameCallback(_, name) {
2877 return;2942 return;
2878 }2943 }
28792944
2880 name = name.trim();2945 // Search for a matching character to get the real name, or take the name provided
2946 const character = findChar({ name: name });
2947 name = character?.name || name;
28812948
2882 const messagesToDelete = [];2949 const messagesToDelete = [];
2883 chat.forEach((value) => {2950 chat.forEach((value) => {
@@ -2906,60 +2973,34 @@ async function deleteMessagesByNameCallback(_, name) {
2906 return '';2973 return '';
2907}2974}
29082975
2909function findCharacterIndex(name) {
2910 const matchTypes = [
2911 (a, b) => a === b,
2912 (a, b) => a.startsWith(b),
2913 (a, b) => a.includes(b),
2914 ];
2915
2916 const exactAvatarMatch = characters.findIndex(x => x.avatar === name);
2917
2918 if (exactAvatarMatch !== -1) {
2919 return exactAvatarMatch;
2920 }
2921
2922 for (const matchType of matchTypes) {
2923 const index = characters.findIndex(x => matchType(x.name.toLowerCase(), name.toLowerCase()));
2924 if (index !== -1) {
2925 return index;
2926 }
2927 }
2928
2929 return -1;
2930}
2931
2932async function goToCharacterCallback(_, name) {2976async function goToCharacterCallback(_, name) {
2933 if (!name) {2977 if (!name) {
2934 console.warn('WARN: No character name provided for /go command');2978 console.warn('WARN: No character name provided for /go command');
2935 return;2979 return;
2936 }2980 }
29372981
2938 name = name.trim();2982 const character = findChar({ name: name });
2939 const characterIndex = findCharacterIndex(name);2983 if (character) {
29402984 const chid = getCharIndex(character);
2941 if (characterIndex !== -1) {2985 await openChat(new String(chid));
2942 await openChat(new String(characterIndex));2986 setActiveCharacter(character.avatar);
2943 setActiveCharacter(characters[characterIndex]?.avatar);
2944 setActiveGroup(null);2987 setActiveGroup(null);
2945 return characters[characterIndex]?.name;2988 return character.name;
2946 } else {2989 }
2947 const group = groups.find(it => it.name.toLowerCase() == name.toLowerCase());2990 const group = groups.find(it => equalsIgnoreCaseAndAccents(it.name, name));
2948 if (group) {2991 if (group) {
2949 await openGroupById(group.id);2992 await openGroupById(group.id);
2950 setActiveCharacter(null);2993 setActiveCharacter(null);
2951 setActiveGroup(group.id);2994 setActiveGroup(group.id);
2952 return group.name;2995 return group.name;
2953 } else {
2954 console.warn(`No matches found for name "${name}"`);
2955 return '';
2956 }
2957 }2996 }
2997 console.warn(`No matches found for name "${name}"`);
2998 return '';
2958}2999}
29593000
2960async function openChat(id) {3001async function openChat(chid) {
2961 resetSelectedGroup();3002 resetSelectedGroup();
2962 setCharacterId(id);3003 setCharacterId(chid);
2963 await delay(1);3004 await delay(1);
2964 await reloadCurrentChat();3005 await reloadCurrentChat();
2965}3006}
@@ -3105,6 +3146,79 @@ async function setNarratorName(_, text) {
3105 return '';3146 return '';
3106}3147}
31073148
3149/**
3150 * Checks if an argument is a string array (or undefined), and if not, throws an error
3151 * @param {string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined} arg The named argument to check
3152 * @param {string} name The name of the argument for the error message
3153 * @param {object} [options={}] - The optional arguments
3154 * @param {boolean} [options.allowUndefined=false] - Whether the argument can be undefined
3155 * @throws {Error} If the argument is not an array
3156 * @returns {string[]}
3157 */
3158export function validateArrayArgString(arg, name, { allowUndefined = true } = {}) {
3159 if (arg === undefined) {
3160 if (allowUndefined) return undefined;
3161 throw new Error(`Argument "${name}" is undefined, but must be a string array`);
3162 }
3163 if (!Array.isArray(arg)) throw new Error(`Argument "${name}" must be an array`);
3164 if (!arg.every(x => typeof x === 'string')) throw new Error(`Argument "${name}" must be an array of strings`);
3165 return arg;
3166}
3167
3168/**
3169 * Checks if an argument is a string or closure array (or undefined), and if not, throws an error
3170 * @param {string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined} arg The named argument to check
3171 * @param {string} name The name of the argument for the error message
3172 * @param {object} [options={}] - The optional arguments
3173 * @param {boolean} [options.allowUndefined=false] - Whether the argument can be undefined
3174 * @throws {Error} If the argument is not an array of strings or closures
3175 * @returns {(string|SlashCommandClosure)[]}
3176 */
3177export function validateArrayArg(arg, name, { allowUndefined = true } = {}) {
3178 if (arg === undefined) {
3179 if (allowUndefined) return [];
3180 throw new Error(`Argument "${name}" is undefined, but must be an array of strings or closures`);
3181 }
3182 if (!Array.isArray(arg)) throw new Error(`Argument "${name}" must be an array`);
3183 if (!arg.every(x => typeof x === 'string' || x instanceof SlashCommandClosure)) throw new Error(`Argument "${name}" must be an array of strings or closures`);
3184 return arg;
3185}
3186
3187
3188/**
3189 * Retrieves the name and avatar information for a message
3190 *
3191 * The name of the character will always have precendence over the one given as argument. If you want to specify a different name for the message,
3192 * explicitly implement this in the code using this.
3193 *
3194 * @param {object?} character - The character object to get the avatar data for
3195 * @param {string?} name - The name to get the avatar data for
3196 * @returns {{name: string, force_avatar: string, original_avatar: string}} An object containing the name for the message, forced avatar URL, and original avatar
3197 */
3198export function getNameAndAvatarForMessage(character, name = null) {
3199 const isNeutralCharacter = !character && name2 === neutralCharacterName && name === neutralCharacterName;
3200 const currentChar = characters[this_chid];
3201
3202 let force_avatar, original_avatar;
3203 if (character?.avatar === currentChar?.avatar || isNeutralCharacter) {
3204 // If the targeted character is the currently selected one in a solo chat, we don't need to force any avatars
3205 }
3206 else if (character && character.avatar !== 'none') {
3207 force_avatar = getThumbnailUrl('avatar', character.avatar);
3208 original_avatar = character.avatar;
3209 }
3210 else {
3211 force_avatar = default_avatar;
3212 original_avatar = default_avatar;
3213 }
3214
3215 return {
3216 name: character?.name || name,
3217 force_avatar: force_avatar,
3218 original_avatar: original_avatar,
3219 };
3220}
3221
3108export async function sendMessageAs(args, text) {3222export async function sendMessageAs(args, text) {
3109 if (!text) {3223 if (!text) {
3110 return '';3224 return '';
@@ -3143,26 +3257,18 @@ export async function sendMessageAs(args, text) {
3143 const isSystem = bias && !removeMacros(mesText).length;3257 const isSystem = bias && !removeMacros(mesText).length;
3144 const compact = isTrueBoolean(args?.compact);3258 const compact = isTrueBoolean(args?.compact);
31453259
3146 const character = characters.find(x => x.avatar === name) ?? characters.find(x => x.name === name);3260 const character = findChar({ name: name });
3147 let force_avatar, original_avatar;
3148
3149 const chatCharacter = this_chid !== undefined ? characters[this_chid] : null;
3150 const isNeutralCharacter = !chatCharacter && name2 === neutralCharacterName && name === neutralCharacterName;
31513261
3152 if (chatCharacter === character || isNeutralCharacter) {3262 const avatarCharacter = args.avatar ? findChar({ name: args.avatar }) : character;
3153 // If the targeted character is the currently selected one in a solo chat, we don't need to force any avatars3263 if (args.avatar && !avatarCharacter) {
3154 }3264 toastr.warning(`Character for avatar ${args.avatar} not found`);
3155 else if (character && character.avatar !== 'none') {3265 return '';
3156 force_avatar = getThumbnailUrl('avatar', character.avatar);
3157 original_avatar = character.avatar;
3158 }
3159 else {
3160 force_avatar = default_avatar;
3161 original_avatar = default_avatar;
3162 }3266 }
31633267
3268 const { name: avatarCharName, force_avatar, original_avatar } = getNameAndAvatarForMessage(avatarCharacter, name);
3269
3164 const message = {3270 const message = {
3165 name: name,3271 name: character?.name || name || avatarCharName,
3166 is_user: false,3272 is_user: false,
3167 is_system: isSystem,3273 is_system: isSystem,
3168 send_date: getMessageTimeStamp(),3274 send_date: getMessageTimeStamp(),
public/scripts/slash-commands/SlashCommand.js+2 -2
@@ -15,13 +15,13 @@ import { SlashCommandScope } from './SlashCommandScope.js';
15 * _abortController:SlashCommandAbortController,15 * _abortController:SlashCommandAbortController,
16 * _debugController:SlashCommandDebugController,16 * _debugController:SlashCommandDebugController,
17 * _hasUnnamedArgument:boolean,17 * _hasUnnamedArgument:boolean,
18 * [id:string]:string|SlashCommandClosure|(string|SlashCommandClosure)[],18 * [id:string]:string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined,
19 * }} NamedArguments19 * }} NamedArguments
20 */20 */
2121
22/**22/**
23 * Alternative object for local JSDocs, where you don't need existing pipe, scope, etc. arguments23 * Alternative object for local JSDocs, where you don't need existing pipe, scope, etc. arguments
24 * @typedef {{[id:string]:string|SlashCommandClosure|(string|SlashCommandClosure)[]}} NamedArgumentsCapture24 * @typedef {{[id:string]:string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined}} NamedArgumentsCapture
25 */25 */
2626
27/**27/**
public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js+14 -2
@@ -2,7 +2,7 @@ import { chat_metadata, characters, substituteParams, chat, extension_prompt_rol
2import { extension_settings } from '../extensions.js';2import { extension_settings } from '../extensions.js';
3import { getGroupMembers, groups } from '../group-chats.js';3import { getGroupMembers, groups } from '../group-chats.js';
4import { power_user } from '../power-user.js';4import { power_user } from '../power-user.js';
5import { searchCharByName, getTagsList, tags } from '../tags.js';5import { searchCharByName, getTagsList, tags, tag_map } from '../tags.js';
6import { world_names } from '../world-info.js';6import { world_names } from '../world-info.js';
7import { SlashCommandClosure } from './SlashCommandClosure.js';7import { SlashCommandClosure } from './SlashCommandClosure.js';
8import { SlashCommandEnumValue, enumTypes } from './SlashCommandEnumValue.js';8import { SlashCommandEnumValue, enumTypes } from './SlashCommandEnumValue.js';
@@ -182,6 +182,18 @@ export const commonEnumProviders = {
182 personas: () => Object.values(power_user.personas).map(persona => new SlashCommandEnumValue(persona, null, enumTypes.name, enumIcons.persona)),182 personas: () => Object.values(power_user.personas).map(persona => new SlashCommandEnumValue(persona, null, enumTypes.name, enumIcons.persona)),
183183
184 /**184 /**
185 * All possible tags, or only those that have been assigned
186 *
187 * @param {('all' | 'assigned')} [mode='all'] - Which types of tags to show
188 * @returns {() => SlashCommandEnumValue[]}
189 */
190 tags: (mode = 'all') => () => {
191 let assignedTags = mode === 'assigned' ? new Set(Object.values(tag_map).flat()) : new Set();
192 return tags.filter(tag => mode === 'all' || (mode === 'assigned' && assignedTags.has(tag.id)))
193 .map(tag => new SlashCommandEnumValue(tag.name, null, enumTypes.command, enumIcons.tag));
194 },
195
196 /**
185 * All possible tags for a given char/group entity197 * All possible tags for a given char/group entity
186 *198 *
187 * @param {('all' | 'existing' | 'not-existing')?} [mode='all'] - Which types of tags to show199 * @param {('all' | 'existing' | 'not-existing')?} [mode='all'] - Which types of tags to show
@@ -193,7 +205,7 @@ export const commonEnumProviders = {
193 if (charName instanceof SlashCommandClosure) throw new Error('Argument \'name\' does not support closures');205 if (charName instanceof SlashCommandClosure) throw new Error('Argument \'name\' does not support closures');
194 const key = searchCharByName(substituteParams(charName), { suppressLogging: true });206 const key = searchCharByName(substituteParams(charName), { suppressLogging: true });
195 const assigned = key ? getTagsList(key) : [];207 const assigned = key ? getTagsList(key) : [];
196 return tags.filter(it => !key || mode === 'all' || mode === 'existing' && assigned.includes(it) || mode === 'not-existing' && !assigned.includes(it))208 return tags.filter(it => mode === 'all' || mode === 'existing' && assigned.includes(it) || mode === 'not-existing' && !assigned.includes(it))
197 .map(tag => new SlashCommandEnumValue(tag.name, null, enumTypes.command, enumIcons.tag));209 .map(tag => new SlashCommandEnumValue(tag.name, null, enumTypes.command, enumIcons.tag));
198 },210 },
199211
public/scripts/tags.js+8 -8
@@ -15,7 +15,7 @@ import {
15import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, FilterHelper } from './filters.js';15import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, FilterHelper } from './filters.js';
1616
17import { groupCandidatesFilter, groups, selected_group } from './group-chats.js';17import { groupCandidatesFilter, groups, selected_group } from './group-chats.js';
18import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, flashHighlight, equalsIgnoreCaseAndAccents, includesIgnoreCaseAndAccents, removeFromArray, getFreeName, debounce } from './utils.js';18import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, flashHighlight, equalsIgnoreCaseAndAccents, includesIgnoreCaseAndAccents, removeFromArray, getFreeName, debounce, findChar } from './utils.js';
19import { power_user } from './power-user.js';19import { power_user } from './power-user.js';
20import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';20import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
21import { SlashCommand } from './slash-commands/SlashCommand.js';21import { SlashCommand } from './slash-commands/SlashCommand.js';
@@ -50,7 +50,6 @@ export {
50 removeTagFromMap,50 removeTagFromMap,
51};51};
5252
53/** @typedef {import('../scripts/popup.js').Popup} Popup */
54/** @typedef {import('../script.js').Character} Character */53/** @typedef {import('../script.js').Character} Character */
5554
56const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter';55const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter';
@@ -507,7 +506,7 @@ export function getTagKeyForEntityElement(element) {
507 */506 */
508export function searchCharByName(charName, { suppressLogging = false } = {}) {507export function searchCharByName(charName, { suppressLogging = false } = {}) {
509 const entity = charName508 const entity = charName
510 ? (characters.find(x => x.name === charName) || groups.find(x => x.name == charName))509 ? (findChar({ name: charName }) || groups.find(x => equalsIgnoreCaseAndAccents(x.name, charName)))
511 : (selected_group ? groups.find(x => x.id == selected_group) : characters[this_chid]);510 : (selected_group ? groups.find(x => x.id == selected_group) : characters[this_chid]);
512 const key = getTagKeyForEntity(entity);511 const key = getTagKeyForEntity(entity);
513 if (!key) {512 if (!key) {
@@ -1861,8 +1860,9 @@ function registerTagsSlashCommands() {
1861 return String(result);1860 return String(result);
1862 },1861 },
1863 namedArgumentList: [1862 namedArgumentList: [
1864 SlashCommandNamedArgument.fromProps({ name: 'name',1863 SlashCommandNamedArgument.fromProps({
1865 description: 'Character name',1864 name: 'name',
1865 description: 'Character name - or unique character identifier (avatar key)',
1866 typeList: [ARGUMENT_TYPE.STRING],1866 typeList: [ARGUMENT_TYPE.STRING],
1867 defaultValue: '{{char}}',1867 defaultValue: '{{char}}',
1868 enumProvider: commonEnumProviders.characters(),1868 enumProvider: commonEnumProviders.characters(),
@@ -1907,7 +1907,7 @@ function registerTagsSlashCommands() {
1907 },1907 },
1908 namedArgumentList: [1908 namedArgumentList: [
1909 SlashCommandNamedArgument.fromProps({ name: 'name',1909 SlashCommandNamedArgument.fromProps({ name: 'name',
1910 description: 'Character name',1910 description: 'Character name - or unique character identifier (avatar key)',
1911 typeList: [ARGUMENT_TYPE.STRING],1911 typeList: [ARGUMENT_TYPE.STRING],
1912 defaultValue: '{{char}}',1912 defaultValue: '{{char}}',
1913 enumProvider: commonEnumProviders.characters(),1913 enumProvider: commonEnumProviders.characters(),
@@ -1950,7 +1950,7 @@ function registerTagsSlashCommands() {
1950 namedArgumentList: [1950 namedArgumentList: [
1951 SlashCommandNamedArgument.fromProps({1951 SlashCommandNamedArgument.fromProps({
1952 name: 'name',1952 name: 'name',
1953 description: 'Character name',1953 description: 'Character name - or unique character identifier (avatar key)',
1954 typeList: [ARGUMENT_TYPE.STRING],1954 typeList: [ARGUMENT_TYPE.STRING],
1955 defaultValue: '{{char}}',1955 defaultValue: '{{char}}',
1956 enumProvider: commonEnumProviders.characters(),1956 enumProvider: commonEnumProviders.characters(),
@@ -1993,7 +1993,7 @@ function registerTagsSlashCommands() {
1993 namedArgumentList: [1993 namedArgumentList: [
1994 SlashCommandNamedArgument.fromProps({1994 SlashCommandNamedArgument.fromProps({
1995 name: 'name',1995 name: 'name',
1996 description: 'Character name',1996 description: 'Character name - or unique character identifier (avatar key)',
1997 typeList: [ARGUMENT_TYPE.STRING],1997 typeList: [ARGUMENT_TYPE.STRING],
1998 defaultValue: '{{char}}',1998 defaultValue: '{{char}}',
1999 enumProvider: commonEnumProviders.characters(),1999 enumProvider: commonEnumProviders.characters(),
public/scripts/utils.js+74 -1
@@ -1,10 +1,12 @@
1import { getContext } from './extensions.js';1import { getContext } from './extensions.js';
2import { getRequestHeaders } from '../script.js';2import { characters, getRequestHeaders, this_chid } from '../script.js';
3import { isMobile } from './RossAscends-mods.js';3import { isMobile } from './RossAscends-mods.js';
4import { collapseNewlines } from './power-user.js';4import { collapseNewlines } from './power-user.js';
5import { debounce_timeout } from './constants.js';5import { debounce_timeout } from './constants.js';
6import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';6import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
7import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';7import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
8import { getTagsList } from './tags.js';
9import { groups, selected_group } from './group-chats.js';
810
9/**11/**
10 * Pagination status string template.12 * Pagination status string template.
@@ -2110,3 +2112,74 @@ export async function showFontAwesomePicker(customList = null) {
2110 }2112 }
2111 return null;2113 return null;
2112}2114}
2115
2116/**
2117 * Finds a character by name, with optional filtering and precedence for avatars
2118 * @param {object} [options={}] - The options for the search
2119 * @param {string?} [options.name=null] - The name to search for
2120 * @param {boolean} [options.allowAvatar=true] - Whether to allow searching by avatar
2121 * @param {boolean} [options.insensitive=true] - Whether the search should be case insensitive
2122 * @param {string[]?} [options.filteredByTags=null] - Tags to filter characters by
2123 * @param {boolean} [options.preferCurrentChar=true] - Whether to prefer the current character(s)
2124 * @param {boolean} [options.quiet=false] - Whether to suppress warnings
2125 * @returns {any?} - The found character or null if not found
2126 */
2127export function findChar({ name = null, allowAvatar = true, insensitive = true, filteredByTags = null, preferCurrentChar = true, quiet = false } = {}) {
2128 const matches = (char) => !name || (allowAvatar && char.avatar === name) || (insensitive ? equalsIgnoreCaseAndAccents(char.name, name) : char.name === name);
2129
2130 // Filter characters by tags if provided
2131 let filteredCharacters = characters;
2132 if (filteredByTags) {
2133 filteredCharacters = characters.filter(char => {
2134 const charTags = getTagsList(char.avatar, false);
2135 return filteredByTags.every(tagName => charTags.some(x => x.name == tagName));
2136 });
2137 }
2138
2139 // Get the current character(s)
2140 /** @type {any[]} */
2141 const currentChars = selected_group ? groups.find(group => group.id === selected_group)?.members.map(member => filteredCharacters.find(char => char.avatar === member))
2142 : filteredCharacters.filter(char => characters[this_chid]?.avatar === char.avatar);
2143
2144 // If we have a current char and prefer it, return that if it matches
2145 if (preferCurrentChar) {
2146 const preferredCharSearch = currentChars.filter(matches);
2147 if (preferredCharSearch.length > 1) {
2148 if (!quiet) toastr.warning('Multiple characters found for given conditions.');
2149 else console.warn('Multiple characters found for given conditions. Returning the first match.');
2150 }
2151 if (preferredCharSearch.length) {
2152 return preferredCharSearch[0];
2153 }
2154 }
2155
2156 // If allowAvatar is true, search by avatar first
2157 if (allowAvatar && name) {
2158 const characterByAvatar = filteredCharacters.find(char => char.avatar === name);
2159 if (characterByAvatar) {
2160 return characterByAvatar;
2161 }
2162 }
2163
2164 // Search for matching characters by name
2165 const matchingCharacters = name ? filteredCharacters.filter(matches) : filteredCharacters;
2166 if (matchingCharacters.length > 1) {
2167 if (!quiet) toastr.warning('Multiple characters found for given conditions.');
2168 else console.warn('Multiple characters found for given conditions. Returning the first match.');
2169 }
2170
2171 return matchingCharacters[0] || null;
2172}
2173
2174/**
2175 * Gets the index of a character based on the character object
2176 * @param {object} char - The character object to find the index for
2177 * @throws {Error} If the character is not found
2178 * @returns {number} The index of the character in the characters array
2179 */
2180export function getCharIndex(char) {
2181 if (!char) throw new Error('Character is undefined');
2182 const index = characters.findIndex(c => c.avatar === char.avatar);
2183 if (index === -1) throw new Error(`Character not found: ${char.avatar}`);
2184 return index;
2185}