Merge branch 'staging' into swipe-nums-for-all-mes

e32b70104518491bdd3da994835106a01007e0c7

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

12 files changed, +414 -181Ignore whitespace
public/index.html+34 -32
@@ -3248,25 +3248,26 @@
3248 </div>3248 </div>
3249 </div>3249 </div>
3250 <div id="InstructSettingsColumn" class="flex-container flexNoGap flexFlowColumn flex1">3250 <div id="InstructSettingsColumn" class="flex-container flexNoGap flexFlowColumn flex1">
3251 <h4 class="standoutHeader title_restorable justifySpaceBetween">
3252 <div class="flex-container">
3253 <span data-i18n="Instruct Template">Instruct Template</span>
3254 <a href="https://docs.sillytavern.app/usage/core-concepts/instructmode/" class="notes-link" target="_blank">
3255 <span class="fa-solid fa-circle-question note-link-span"></span>
3256 </a>
3257 </div>
3258 <div class="flex-container">
3259 <label for="instruct_bind_to_context" class="checkbox_label flex1" title="Bind to Context&#10If enabled, Context templates will be automatically selected based on selected Instruct template name or by preference." data-i18n="[title]instruct_bind_to_context">
3260 <input id="instruct_bind_to_context" type="checkbox" style="display:none;" />
3261 <small><i class="fa-solid fa-link menu_button margin0"></i></small>
3262 </label>
3263 <label id="instruct_enabled_label"for="instruct_enabled" class="checkbox_label flex1" title="Enable Instruct Mode" data-i18n="[title]instruct_enabled">
3264 <input id="instruct_enabled" type="checkbox" style="display:none;" />
3265 <small><i class="fa-solid fa-power-off menu_button margin0"></i></small>
3266 </label>
3267 </div>
3268 </h4>
3251 <div id="instructSettingsBlock">3269 <div id="instructSettingsBlock">
3252 <h4 class="standoutHeader title_restorable justifySpaceBetween">3270
3253 <div class="flex-container">
3254 <span data-i18n="Instruct Template">Instruct Template</span>
3255 <a href="https://docs.sillytavern.app/usage/core-concepts/instructmode/" class="notes-link" target="_blank">
3256 <span class="fa-solid fa-circle-question note-link-span"></span>
3257 </a>
3258 </div>
3259 <div class="flex-container">
3260 <label for="instruct_bind_to_context" class="checkbox_label flex1" title="Bind to Context&#10If enabled, Context templates will be automatically selected based on selected Instruct template name or by preference." data-i18n="[title]instruct_bind_to_context">
3261 <input id="instruct_bind_to_context" type="checkbox" style="display:none;" />
3262 <small><i class="fa-solid fa-link menu_button margin0"></i></small>
3263 </label>
3264 <label for="instruct_enabled" class="checkbox_label flex1" title="Enable Instruct Mode" data-i18n="[title]instruct_enabled">
3265 <input id="instruct_enabled" type="checkbox" style="display:none;" />
3266 <small><i class="fa-solid fa-power-off menu_button margin0"></i></small>
3267 </label>
3268 </div>
3269 </h4>
32703271
3271 <div class="flex-container" title="Select your current Instruct Template" data-i18n="[title]Select your current Instruct Template">3272 <div class="flex-container" title="Select your current Instruct Template" data-i18n="[title]Select your current Instruct Template">
3272 <select id="instruct_presets" data-preset-manager-for="instruct" class="flex1 text_pole"></select>3273 <select id="instruct_presets" data-preset-manager-for="instruct" class="flex1 text_pole"></select>
@@ -3431,21 +3432,22 @@
3431 </div>3432 </div>
3432 </div>3433 </div>
3433 <div id="SystemPromptColumn" class="flex-container flexNoGap flexFlowColumn flex1">3434 <div id="SystemPromptColumn" class="flex-container flexNoGap flexFlowColumn flex1">
3435 <h4 class="standoutHeader title_restorable justifySpaceBetween">
3436 <div class="flex-container">
3437 <span data-i18n="System Prompt">System Prompt</span>
3438 <a href="https://docs.sillytavern.app/usage/core-concepts/advancedformatting/#system-prompt" class="notes-link" target="_blank">
3439 <span class="fa-solid fa-circle-question note-link-span"></span>
3440 </a>
3441 </div>
3442 <div class="flex-container">
3443 <label id="sysprompt_enabled_label" for="sysprompt_enabled" class="checkbox_label flex1" title="Enable System Prompt" data-i18n="[title]sysprompt_enabled">
3444 <input id="sysprompt_enabled" type="checkbox" style="display:none;" />
3445 <small><i class="fa-solid fa-power-off menu_button margin0"></i></small>
3446 </label>
3447 </div>
3448 </h4>
3434 <div id="SystemPromptBlock">3449 <div id="SystemPromptBlock">
3435 <h4 class="standoutHeader title_restorable justifySpaceBetween">3450
3436 <div class="flex-container">
3437 <span data-i18n="System Prompt">System Prompt</span>
3438 <a href="https://docs.sillytavern.app/usage/core-concepts/advancedformatting/#system-prompt" class="notes-link" target="_blank">
3439 <span class="fa-solid fa-circle-question note-link-span"></span>
3440 </a>
3441 </div>
3442 <div class="flex-container">
3443 <label for="sysprompt_enabled" class="checkbox_label flex1" title="Enable System Prompt" data-i18n="[title]sysprompt_enabled">
3444 <input id="sysprompt_enabled" type="checkbox" style="display:none;" />
3445 <small><i class="fa-solid fa-power-off menu_button margin0"></i></small>
3446 </label>
3447 </div>
3448 </h4>
34493451
3450 <div class="flex-container" title="Select your current System Prompt" data-i18n="[title]Select your current System Prompt">3452 <div class="flex-container" title="Select your current System Prompt" data-i18n="[title]Select your current System Prompt">
3451 <select id="sysprompt_select" data-preset-manager-for="sysprompt" class="flex1 text_pole"></select>3453 <select id="sysprompt_select" data-preset-manager-for="sysprompt" class="flex1 text_pole"></select>
public/script.js+14 -2
@@ -231,7 +231,7 @@ import { MacrosParser, evaluateMacros, getLastMessageId } from './scripts/macros
231import { currentUser, setUserControls } from './scripts/user.js';231import { currentUser, setUserControls } from './scripts/user.js';
232import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';232import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';
233import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';233import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';
234import { ScraperManager } from './scripts/scrapers.js';234import { initScrapers, ScraperManager } from './scripts/scrapers.js';
235import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js';235import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js';
236import { SlashCommand } from './scripts/slash-commands/SlashCommand.js';236import { SlashCommand } from './scripts/slash-commands/SlashCommand.js';
237import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './scripts/slash-commands/SlashCommandArgument.js';237import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './scripts/slash-commands/SlashCommandArgument.js';
@@ -960,6 +960,7 @@ async function firstLoadInit() {
960 initCfg();960 initCfg();
961 initLogprobs();961 initLogprobs();
962 initInputMarkdown();962 initInputMarkdown();
963 await initScrapers();
963 doDailyExtensionUpdatesCheck();964 doDailyExtensionUpdatesCheck();
964 await hideLoader();965 await hideLoader();
965 await fixViewport();966 await fixViewport();
@@ -5439,6 +5440,7 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
5439 getMessage = getMessage.substring(0, getMessage.indexOf('<|endoftext|>'));5440 getMessage = getMessage.substring(0, getMessage.indexOf('<|endoftext|>'));
5440 }5441 }
5441 const isInstruct = power_user.instruct.enabled && main_api !== 'openai';5442 const isInstruct = power_user.instruct.enabled && main_api !== 'openai';
5443 const isNotEmpty = (str) => str && str.trim() !== '';
5442 if (isInstruct && power_user.instruct.stop_sequence) {5444 if (isInstruct && power_user.instruct.stop_sequence) {
5443 if (getMessage.indexOf(power_user.instruct.stop_sequence) != -1) {5445 if (getMessage.indexOf(power_user.instruct.stop_sequence) != -1) {
5444 getMessage = getMessage.substring(0, getMessage.indexOf(power_user.instruct.stop_sequence));5446 getMessage = getMessage.substring(0, getMessage.indexOf(power_user.instruct.stop_sequence));
@@ -5446,7 +5448,7 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
5446 }5448 }
5447 // Hana: Only use the first sequence (should be <|model|>)5449 // Hana: Only use the first sequence (should be <|model|>)
5448 // of the prompt before <|user|> (as KoboldAI Lite does it).5450 // of the prompt before <|user|> (as KoboldAI Lite does it).
5449 if (isInstruct && power_user.instruct.input_sequence) {5451 if (isInstruct && isNotEmpty(power_user.instruct.input_sequence)) {
5450 if (getMessage.indexOf(power_user.instruct.input_sequence) != -1) {5452 if (getMessage.indexOf(power_user.instruct.input_sequence) != -1) {
5451 getMessage = getMessage.substring(0, getMessage.indexOf(power_user.instruct.input_sequence));5453 getMessage = getMessage.substring(0, getMessage.indexOf(power_user.instruct.input_sequence));
5452 }5454 }
@@ -8497,12 +8499,22 @@ const CONNECT_API_MAP = {
8497 selected: 'novel',8499 selected: 'novel',
8498 button: '#api_button_novel',8500 button: '#api_button_novel',
8499 },8501 },
8502 'koboldcpp': {
8503 selected: 'textgenerationwebui',
8504 button: '#api_button_textgenerationwebui',
8505 type: textgen_types.KOBOLDCPP,
8506 },
8500 // KoboldCpp alias8507 // KoboldCpp alias
8501 'kcpp': {8508 'kcpp': {
8502 selected: 'textgenerationwebui',8509 selected: 'textgenerationwebui',
8503 button: '#api_button_textgenerationwebui',8510 button: '#api_button_textgenerationwebui',
8504 type: textgen_types.KOBOLDCPP,8511 type: textgen_types.KOBOLDCPP,
8505 },8512 },
8513 'openai': {
8514 selected: 'openai',
8515 button: '#api_button_openai',
8516 source: chat_completion_sources.OPENAI,
8517 },
8506 // OpenAI alias8518 // OpenAI alias
8507 'oai': {8519 'oai': {
8508 selected: 'openai',8520 selected: 'openai',
public/scripts/RossAscends-mods.js+17 -16
@@ -56,22 +56,25 @@ let counterNonce = Date.now();
5656
57const observerConfig = { childList: true, subtree: true };57const observerConfig = { childList: true, subtree: true };
58const countTokensDebounced = debounce(RA_CountCharTokens, debounce_timeout.relaxed);58const countTokensDebounced = debounce(RA_CountCharTokens, debounce_timeout.relaxed);
59const countTokensShortDebounced = debounce(RA_CountCharTokens, debounce_timeout.short);
60const checkStatusDebounced = debounce(RA_checkOnlineStatus, debounce_timeout.short);
5961
60const observer = new MutationObserver(function (mutations) {62const observer = new MutationObserver(function (mutations) {
61 mutations.forEach(function (mutation) {63 mutations.forEach(function (mutation) {
64 if (!(mutation.target instanceof HTMLElement)) {
65 return;
66 }
62 if (mutation.target.classList.contains('online_status_text')) {67 if (mutation.target.classList.contains('online_status_text')) {
63 RA_checkOnlineStatus();68 checkStatusDebounced();
64 } else if (mutation.target.parentNode === SelectedCharacterTab) {69 } else if (mutation.target.parentNode === SelectedCharacterTab) {
65 setTimeout(RA_CountCharTokens, 200);70 countTokensShortDebounced();
66 } else if (mutation.target.classList.contains('mes_text')) {71 } else if (mutation.target.classList.contains('mes_text')) {
67 if (mutation.target instanceof HTMLElement) {72 for (const element of mutation.target.getElementsByTagName('math')) {
68 for (const element of mutation.target.getElementsByTagName('math')) {73 element.childNodes.forEach(function (child) {
69 element.childNodes.forEach(function (child) {74 if (child.nodeType === Node.TEXT_NODE) {
70 if (child.nodeType === Node.TEXT_NODE) {75 child.textContent = '';
71 child.textContent = '';76 }
72 }77 });
73 });
74 }
75 }78 }
76 }79 }
77 });80 });
@@ -159,8 +162,8 @@ export function shouldSendOnEnter() {
159export function humanizedDateTime() {162export function humanizedDateTime() {
160 const now = new Date(Date.now());163 const now = new Date(Date.now());
161 const dt = {164 const dt = {
162 year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate(),165 year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate(),
163 hour: now.getHours(), minute: now.getMinutes(), second: now.getSeconds(),166 hour: now.getHours(), minute: now.getMinutes(), second: now.getSeconds(),
164 };167 };
165 for (const key in dt) {168 for (const key in dt) {
166 dt[key] = dt[key].toString().padStart(2, '0');169 dt[key] = dt[key].toString().padStart(2, '0');
@@ -725,9 +728,7 @@ export function addSafariPatch() {
725728
726export function initRossMods() {729export function initRossMods() {
727 // initial status check730 // initial status check
728 setTimeout(() => {731 checkStatusDebounced();
729 RA_checkOnlineStatus();
730 }, 100);
731732
732 if (power_user.auto_load_chat) {733 if (power_user.auto_load_chat) {
733 RA_autoloadchat();734 RA_autoloadchat();
@@ -752,7 +753,7 @@ export function initRossMods() {
752 setTimeout(() => RA_autoconnect(PrevAPI), 100);753 setTimeout(() => RA_autoconnect(PrevAPI), 100);
753 });754 });
754755
755 $('#api_button').click(function () { setTimeout(RA_checkOnlineStatus, 100); });756 $('#api_button').on('click', () => checkStatusDebounced());
756757
757 //toggle pin class when lock toggle clicked758 //toggle pin class when lock toggle clicked
758 $(RPanelPin).on('click', function () {759 $(RPanelPin).on('click', function () {
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+219 -108
@@ -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,32 +2589,27 @@ 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;
25162593
2517 if (character && character.avatar !== 'none') {2594 setCharacterName(name);
2518 force_avatar = getThumbnailUrl('avatar', character.avatar);
2519 original_avatar = character.avatar;
2520 }
2521 else {
2522 force_avatar = default_avatar;
2523 original_avatar = default_avatar;
2524 }
2525
2526 setCharacterName(character.name);
25272595
2528 const restoreCharacter = () => {2596 const restoreCharacter = () => {
2529 if (String(this_chid) !== String(chId)) {2597 if (String(this_chid) !== String(chId)) {
2530 return;2598 return;
2531 }2599 }
25322600
2533 setCharacterId(prevChId);2601 if (prevChId !== undefined) {
2534 setCharacterName(characters[prevChId].name);2602 setCharacterId(prevChId);
2603 setCharacterName(characters[prevChId].name);
2604 } else {
2605 setCharacterId(undefined);
2606 setCharacterName(neutralCharacterName);
2607 }
25352608
2536 // Only force the new avatar if the character name is the same2609 // Only force the new avatar if the character name is the same
2537 // This skips if an error was fired2610 // This skips if an error was fired
2538 const lastMessage = chat[chat.length - 1];2611 const lastMessage = chat[chat.length - 1];
2539 if (lastMessage && lastMessage?.name === character.name) {2612 if (lastMessage && lastMessage?.name === name) {
2540 lastMessage.force_avatar = force_avatar;2613 lastMessage.force_avatar = force_avatar;
2541 lastMessage.original_avatar = original_avatar;2614 lastMessage.original_avatar = original_avatar;
2542 }2615 }
@@ -2547,7 +2620,7 @@ async function askCharacter(args, text) {
2547 // Run generate and restore previous character2620 // Run generate and restore previous character
2548 try {2621 try {
2549 eventSource.once(event_types.MESSAGE_RECEIVED, restoreCharacter);2622 eventSource.once(event_types.MESSAGE_RECEIVED, restoreCharacter);
2550 toastr.info(`Asking ${character.name} something...`);2623 toastr.info(`Asking ${name} something...`);
2551 askResult = await Generate('ask_command');2624 askResult = await Generate('ask_command');
2552 } catch (error) {2625 } catch (error) {
2553 restoreCharacter();2626 restoreCharacter();
@@ -2741,26 +2814,23 @@ async function removeGroupMemberCallback(_, arg) {
2741 return '';2814 return '';
2742}2815}
27432816
2744async function addGroupMemberCallback(_, arg) {2817async function addGroupMemberCallback(_, name) {
2745 if (!selected_group) {2818 if (!selected_group) {
2746 toastr.warning('Cannot run /memberadd command outside of a group chat.');2819 toastr.warning('Cannot run /memberadd command outside of a group chat.');
2747 return '';2820 return '';
2748 }2821 }
27492822
2750 if (!arg) {2823 if (!name) {
2751 console.warn('WARN: No argument provided for /memberadd command');2824 console.warn('WARN: No argument provided for /memberadd command');
2752 return '';2825 return '';
2753 }2826 }
27542827
2755 arg = arg.trim();2828 const character = findChar({ name: name, preferCurrentChar: false });
2756 const chid = findCharacterIndex(arg);2829 if (!character) {
27572830 console.warn(`WARN: No character found for argument ${name}`);
2758 if (chid === -1) {
2759 console.warn(`WARN: No character found for argument ${arg}`);
2760 return '';2831 return '';
2761 }2832 }
27622833
2763 const character = characters[chid];
2764 const group = groups.find(x => x.id === selected_group);2834 const group = groups.find(x => x.id === selected_group);
27652835
2766 if (!group || !Array.isArray(group.members)) {2836 if (!group || !Array.isArray(group.members)) {
@@ -2829,7 +2899,7 @@ function findPersonaByName(name) {
2829 }2899 }
28302900
2831 for (const persona of Object.entries(power_user.personas)) {2901 for (const persona of Object.entries(power_user.personas)) {
2832 if (persona[1].toLowerCase() === name.toLowerCase()) {2902 if (equalsIgnoreCaseAndAccents(persona[1], name)) {
2833 return persona[0];2903 return persona[0];
2834 }2904 }
2835 }2905 }
@@ -2872,7 +2942,9 @@ async function deleteMessagesByNameCallback(_, name) {
2872 return;2942 return;
2873 }2943 }
28742944
2875 name = name.trim();2945 // Search for a matching character to get the real name, or take the name provided
2946 const character = findChar({ name: name });
2947 name = character?.name || name;
28762948
2877 const messagesToDelete = [];2949 const messagesToDelete = [];
2878 chat.forEach((value) => {2950 chat.forEach((value) => {
@@ -2901,60 +2973,34 @@ async function deleteMessagesByNameCallback(_, name) {
2901 return '';2973 return '';
2902}2974}
29032975
2904function findCharacterIndex(name) {
2905 const matchTypes = [
2906 (a, b) => a === b,
2907 (a, b) => a.startsWith(b),
2908 (a, b) => a.includes(b),
2909 ];
2910
2911 const exactAvatarMatch = characters.findIndex(x => x.avatar === name);
2912
2913 if (exactAvatarMatch !== -1) {
2914 return exactAvatarMatch;
2915 }
2916
2917 for (const matchType of matchTypes) {
2918 const index = characters.findIndex(x => matchType(x.name.toLowerCase(), name.toLowerCase()));
2919 if (index !== -1) {
2920 return index;
2921 }
2922 }
2923
2924 return -1;
2925}
2926
2927async function goToCharacterCallback(_, name) {2976async function goToCharacterCallback(_, name) {
2928 if (!name) {2977 if (!name) {
2929 console.warn('WARN: No character name provided for /go command');2978 console.warn('WARN: No character name provided for /go command');
2930 return;2979 return;
2931 }2980 }
29322981
2933 name = name.trim();2982 const character = findChar({ name: name });
2934 const characterIndex = findCharacterIndex(name);2983 if (character) {
29352984 const chid = getCharIndex(character);
2936 if (characterIndex !== -1) {2985 await openChat(new String(chid));
2937 await openChat(new String(characterIndex));2986 setActiveCharacter(character.avatar);
2938 setActiveCharacter(characters[characterIndex]?.avatar);
2939 setActiveGroup(null);2987 setActiveGroup(null);
2940 return characters[characterIndex]?.name;2988 return character.name;
2941 } else {2989 }
2942 const group = groups.find(it => it.name.toLowerCase() == name.toLowerCase());2990 const group = groups.find(it => equalsIgnoreCaseAndAccents(it.name, name));
2943 if (group) {2991 if (group) {
2944 await openGroupById(group.id);2992 await openGroupById(group.id);
2945 setActiveCharacter(null);2993 setActiveCharacter(null);
2946 setActiveGroup(group.id);2994 setActiveGroup(group.id);
2947 return group.name;2995 return group.name;
2948 } else {
2949 console.warn(`No matches found for name "${name}"`);
2950 return '';
2951 }
2952 }2996 }
2997 console.warn(`No matches found for name "${name}"`);
2998 return '';
2953}2999}
29543000
2955async function openChat(id) {3001async function openChat(chid) {
2956 resetSelectedGroup();3002 resetSelectedGroup();
2957 setCharacterId(id);3003 setCharacterId(chid);
2958 await delay(1);3004 await delay(1);
2959 await reloadCurrentChat();3005 await reloadCurrentChat();
2960}3006}
@@ -3100,6 +3146,79 @@ async function setNarratorName(_, text) {
3100 return '';3146 return '';
3101}3147}
31023148
3149/**
3150 * Checks if an argument is a string array (or undefined), and if not, throws an error
3151 * @param {string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined} arg The named argument to check
3152 * @param {string} name The name of the argument for the error message
3153 * @param {object} [options={}] - The optional arguments
3154 * @param {boolean} [options.allowUndefined=false] - Whether the argument can be undefined
3155 * @throws {Error} If the argument is not an array
3156 * @returns {string[]}
3157 */
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
3103export async function sendMessageAs(args, text) {3222export async function sendMessageAs(args, text) {
3104 if (!text) {3223 if (!text) {
3105 return '';3224 return '';
@@ -3138,26 +3257,18 @@ export async function sendMessageAs(args, text) {
3138 const isSystem = bias && !removeMacros(mesText).length;3257 const isSystem = bias && !removeMacros(mesText).length;
3139 const compact = isTrueBoolean(args?.compact);3258 const compact = isTrueBoolean(args?.compact);
31403259
3141 const character = characters.find(x => x.avatar === name) ?? characters.find(x => x.name === name);3260 const character = findChar({ name: name });
3142 let force_avatar, original_avatar;
3143
3144 const chatCharacter = this_chid !== undefined ? characters[this_chid] : null;
3145 const isNeutralCharacter = !chatCharacter && name2 === neutralCharacterName && name === neutralCharacterName;
31463261
3147 if (chatCharacter === character || isNeutralCharacter) {3262 const avatarCharacter = args.avatar ? findChar({ name: args.avatar }) : character;
3148 // If the targeted character is the currently selected one in a solo chat, we don't need to force any avatars3263 if (args.avatar && !avatarCharacter) {
3149 }3264 toastr.warning(`Character for avatar ${args.avatar} not found`);
3150 else if (character && character.avatar !== 'none') {3265 return '';
3151 force_avatar = getThumbnailUrl('avatar', character.avatar);
3152 original_avatar = character.avatar;
3153 }
3154 else {
3155 force_avatar = default_avatar;
3156 original_avatar = default_avatar;
3157 }3266 }
31583267
3268 const { name: avatarCharName, force_avatar, original_avatar } = getNameAndAvatarForMessage(avatarCharacter, name);
3269
3159 const message = {3270 const message = {
3160 name: name,3271 name: character?.name || name || avatarCharName,
3161 is_user: false,3272 is_user: false,
3162 is_system: isSystem,3273 is_system: isSystem,
3163 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}
public/style.css+6 -0
@@ -995,6 +995,7 @@ flex-flow: column;
995 bottom: 0;995 bottom: 0;
996}996}
997997
998
998.swipes-counter {999.swipes-counter {
999 color: var(--SmartThemeBodyColor);1000 color: var(--SmartThemeBodyColor);
1000 font-size: 12px;1001 font-size: 12px;
@@ -2646,6 +2647,11 @@ select option:not(:checked) {
2646 color: var(--active) !important;2647 color: var(--active) !important;
2647}2648}
26482649
2650#instruct_enabled_label .menu_button:not(.toggleEnabled),
2651#sysprompt_enabled_label .menu_button:not(.toggleEnabled) {
2652 color: Red;
2653}
2654
2649.displayBlock {2655.displayBlock {
2650 display: block !important;2656 display: block !important;
2651}2657}