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 @@
32483248 </div>
32493249 </div>
32503250 <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>
32513269 <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
32713272 <div class="flex-container" title="Select your current Instruct Template" data-i18n="[title]Select your current Instruct Template">
32723273 <select id="instruct_presets" data-preset-manager-for="instruct" class="flex1 text_pole"></select>
@@ -3431,21 +3432,22 @@
34313432 </div>
34323433 </div>
34333434 <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>
34343449 <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
34503452 <div class="flex-container" title="Select your current System Prompt" data-i18n="[title]Select your current System Prompt">
34513453 <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
231231import { currentUser, setUserControls } from './scripts/user.js';
232232import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';
233233import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';
234234import { initScrapers, ScraperManager } from './scripts/scrapers.js';
235235import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js';
236236import { SlashCommand } from './scripts/slash-commands/SlashCommand.js';
237237import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './scripts/slash-commands/SlashCommandArgument.js';
@@ -960,6 +960,7 @@ async function firstLoadInit() {
960960 initCfg();
961961 initLogprobs();
962962 initInputMarkdown();
963+ await initScrapers();
963964 doDailyExtensionUpdatesCheck();
964965 await hideLoader();
965966 await fixViewport();
@@ -5439,6 +5440,7 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
54395440 getMessage = getMessage.substring(0, getMessage.indexOf('<|endoftext|>'));
54405441 }
54415442 const isInstruct = power_user.instruct.enabled && main_api !== 'openai';
5443+ const isNotEmpty = (str) => str && str.trim() !== '';
54425444 if (isInstruct && power_user.instruct.stop_sequence) {
54435445 if (getMessage.indexOf(power_user.instruct.stop_sequence) != -1) {
54445446 getMessage = getMessage.substring(0, getMessage.indexOf(power_user.instruct.stop_sequence));
@@ -5446,7 +5448,7 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
54465448 }
54475449 // Hana: Only use the first sequence (should be <|model|>)
54485450 // of the prompt before <|user|> (as KoboldAI Lite does it).
54495451 if (isInstruct && isNotEmpty(power_user.instruct.input_sequence)) {
54505452 if (getMessage.indexOf(power_user.instruct.input_sequence) != -1) {
54515453 getMessage = getMessage.substring(0, getMessage.indexOf(power_user.instruct.input_sequence));
54525454 }
@@ -8497,12 +8499,22 @@ const CONNECT_API_MAP = {
84978499 selected: 'novel',
84988500 button: '#api_button_novel',
84998501 },
8502+ 'koboldcpp': {
8503+ selected: 'textgenerationwebui',
8504+ button: '#api_button_textgenerationwebui',
8505+ type: textgen_types.KOBOLDCPP,
8506+ },
85008507 // KoboldCpp alias
85018508 'kcpp': {
85028509 selected: 'textgenerationwebui',
85038510 button: '#api_button_textgenerationwebui',
85048511 type: textgen_types.KOBOLDCPP,
85058512 },
8513+ 'openai': {
8514+ selected: 'openai',
8515+ button: '#api_button_openai',
8516+ source: chat_completion_sources.OPENAI,
8517+ },
85068518 // OpenAI alias
85078519 'oai': {
85088520 selected: 'openai',
public/scripts/RossAscends-mods.js+17 -16
@@ -56,22 +56,25 @@ let counterNonce = Date.now();
5656
5757const observerConfig = { childList: true, subtree: true };
5858const countTokensDebounced = debounce(RA_CountCharTokens, debounce_timeout.relaxed);
59+const countTokensShortDebounced = debounce(RA_CountCharTokens, debounce_timeout.short);
60+const checkStatusDebounced = debounce(RA_checkOnlineStatus, debounce_timeout.short);
5961
6062const observer = new MutationObserver(function (mutations) {
6163 mutations.forEach(function (mutation) {
64+ if (!(mutation.target instanceof HTMLElement)) {
65+ return;
66+ }
6267 if (mutation.target.classList.contains('online_status_text')) {
6368 RA_checkOnlineStatuscheckStatusDebounced();
6469 } else if (mutation.target.parentNode === SelectedCharacterTab) {
6570 setTimeoutcountTokensShortDebounced(RA_CountCharTokens, 200);
6671 } 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- }
7578 }
7679 }
7780 });
@@ -159,8 +162,8 @@ export function shouldSendOnEnter() {
159162export function humanizedDateTime() {
160163 const now = new Date(Date.now());
161164 const dt = {
162165 year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate(),
163166 hour: now.getHours(), minute: now.getMinutes(), second: now.getSeconds(),
164167 };
165168 for (const key in dt) {
166169 dt[key] = dt[key].toString().padStart(2, '0');
@@ -725,9 +728,7 @@ export function addSafariPatch() {
725728
726729export function initRossMods() {
727730 // initial status check
728- setTimeout(() => {
731+ checkStatusDebounced();
729- RA_checkOnlineStatus();
730- }, 100);
731732
732733 if (power_user.auto_load_chat) {
733734 RA_autoloadchat();
@@ -752,7 +753,7 @@ export function initRossMods() {
752753 setTimeout(() => RA_autoconnect(PrevAPI), 100);
753754 });
754755
755756 $('#api_button').clickon(function'click', () {=> setTimeoutcheckStatusDebounced(RA_checkOnlineStatus, 100); });
756757
757758 //toggle pin class when lock toggle clicked
758759 $(RPanelPin).on('click', function () {
public/scripts/extensions/expressions/index.js+9 -3
@@ -2,7 +2,7 @@ import { callPopup, eventSource, event_types, generateRaw, getRequestHeaders, ma
22import { dragElement, isMobile } from '../../RossAscends-mods.js';
33import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
44import { loadMovingUIState, power_user } from '../../power-user.js';
55import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition, findChar } from '../../utils.js';
66import { hideMutedSprites } from '../../group-chats.js';
77import { isJsonSchemaSupported } from '../../textgen-settings.js';
88import { debounce_timeout } from '../../constants.js';
@@ -2105,14 +2105,20 @@ function migrateSettings() {
21052105 }));
21062106 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
21072107 name: 'lastsprite',
21082108 callback: (_, valuename) => lastExpression[String(value).trim()] ?? '',{
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+ },
21092114 returns: 'the last set sprite / expression for the named character.',
21102115 unnamedArgumentList: [
21112116 SlashCommandArgument.fromProps({
21122117 description: 'characterCharacter name - or unique character identifier (avatar key)',
21132118 typeList: [ARGUMENT_TYPE.STRING],
21142119 isRequired: true,
21152120 enumProvider: commonEnumProviders.characters('character'),
2121+ forceEnum: true,
21162122 }),
21172123 ],
21182124 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({
441441 description: 'character name',
442442 typeList: [ARGUMENT_TYPE.STRING],
443443 enumProvider: commonEnumProviders.characters('character'),
444+ forceEnum: true,
444445 }),
445446 SlashCommandNamedArgument.fromProps({
446447 name: 'group',
public/scripts/scrapers.js+16 -7
@@ -13,6 +13,7 @@ import { isValidUrl } from './utils.js';
1313 * @property {string} description
1414 * @property {string} iconClass
1515 * @property {boolean} iconAvailable
16+ * @property {() => Promise<void>} [init=null]
1617 * @property {() => Promise<boolean>} isAvailable
1718 * @property {() => Promise<File[]>} scrape
1819 */
@@ -36,12 +37,16 @@ export class ScraperManager {
3637 * Register a scraper to be used by the Data Bank.
3738 * @param {Scraper} scraper Instance of a scraper to register
3839 */
3940 static async registerDataBankScraper(scraper) {
4041 if (ScraperManager.#scrapers.some(s => s.id === scraper.id)) {
4142 console.warn(`Scraper with ID ${scraper.id} already registered`);
4243 return;
4344 }
4445
46+ if (scraper.init) {
47+ await scraper.init();
48+ }
49+
4550 ScraperManager.#scrapers.push(scraper);
4651 }
4752
@@ -462,7 +467,9 @@ class YouTubeScraper {
462467 this.description = 'Download a transcript from a YouTube video.';
463468 this.iconClass = 'fa-brands fa-youtube';
464469 this.iconAvailable = true;
470+ }
465471
472+ async init() {
466473 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
467474 name: 'yt-script',
468475 callback: async (args, url) => {
@@ -564,9 +571,11 @@ class YouTubeScraper {
564571 }
565572}
566573
567-ScraperManager.registerDataBankScraper(new FileScraper());
574+export async function initScrapers() {
568575 await ScraperManager.registerDataBankScraper(new NotepadFileScraper());
569576 await ScraperManager.registerDataBankScraper(new WebScraperNotepad());
570577 await ScraperManager.registerDataBankScraper(new MediaWikiScraperWebScraper());
571578 await ScraperManager.registerDataBankScraper(new FandomScraperMediaWikiScraper());
572579 await ScraperManager.registerDataBankScraper(new YouTubeScraperFandomScraper());
580+ await ScraperManager.registerDataBankScraper(new YouTubeScraper());
581+}
public/scripts/slash-commands.js+219 -108
@@ -55,7 +55,7 @@ import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockStat
5555import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
5656import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
5757import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';
5858import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
5959import { registerVariableCommands, resolveVariable } from './variables.js';
6060import { background_settings } from './backgrounds.js';
6161import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
@@ -68,7 +68,6 @@ import { SlashCommandNamedArgumentAssignment } from './slash-commands/SlashComma
6868import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
6969import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
7070import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
71-import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js';
7271import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
7372import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
7473export {
@@ -174,20 +173,96 @@ export function initDefaultSlashCommands() {
174173 `,
175174 }));
176175 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({
177243 name: 'sendas',
178244 callback: sendMessageAs,
179245 namedArgumentList: [
180246 SlashCommandNamedArgument.fromProps({
181247 name: 'name',
182248 description: 'Character name - or unique character identifier (avatar key)',
183249 typeList: [ARGUMENT_TYPE.STRING],
184250 isRequired: true,
185251 enumProvider: commonEnumProviders.characters('character'),
186252 forceEnum: false,
187253 }),
188254 new 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+ }),
191266 SlashCommandNamedArgument.fromProps({
192267 name: 'at',
193268 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() {
211286 <pre><code>/sendas name="Chloe" Hello, guys!</code></pre>
212287 will send "Hello, guys!" from "Chloe".
213288 </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>
214293 </ul>
215294 </div>
216295 <div>
@@ -381,12 +460,14 @@ export function initDefaultSlashCommands() {
381460 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
382461 name: 'go',
383462 callback: goToCharacterCallback,
463+ returns: 'The character/group name',
384464 unnamedArgumentList: [
385465 SlashCommandArgument.fromProps({
386- description: 'name',
466+ description: 'Character name - or unique character identifier (avatar key)',
387467 typeList: [ARGUMENT_TYPE.STRING],
388468 isRequired: true,
389469 enumProvider: commonEnumProviders.characters('all'),
470+ forceEnum: true,
390471 }),
391472 ],
392473 helpString: 'Opens up a chat with the character or group by its name',
@@ -432,7 +513,7 @@ export function initDefaultSlashCommands() {
432513 namedArgumentList: [
433514 SlashCommandNamedArgument.fromProps({
434515 name: 'name',
435516 description: 'characterCharacter name - or unique character identifier (avatar key)',
436517 typeList: [ARGUMENT_TYPE.STRING],
437518 isRequired: true,
438519 enumProvider: commonEnumProviders.characters('character'),
@@ -451,7 +532,7 @@ export function initDefaultSlashCommands() {
451532 namedArgumentList: [],
452533 unnamedArgumentList: [
453534 SlashCommandArgument.fromProps({
454- description: 'name',
535+ description: 'Character name - or unique character identifier (avatar key)',
455536 typeList: [ARGUMENT_TYPE.STRING],
456537 isRequired: true,
457538 enumProvider: commonEnumProviders.characters('character'),
@@ -618,7 +699,7 @@ export function initDefaultSlashCommands() {
618699 aliases: ['addmember', 'memberadd'],
619700 unnamedArgumentList: [
620701 SlashCommandArgument.fromProps({
621702 description: 'characterCharacter name - or unique character identifier (avatar key)',
622703 typeList: [ARGUMENT_TYPE.STRING],
623704 isRequired: true,
624705 enumProvider: () => selected_group ? commonEnumProviders.characters('character')() : [],
@@ -856,7 +937,7 @@ export function initDefaultSlashCommands() {
856937 ),
857938 SlashCommandNamedArgument.fromProps({
858939 name: 'name',
859940 description: 'in-prompt character name for instruct mode (or unique character identifier (avatar key), which will be used as name)',
860941 typeList: [ARGUMENT_TYPE.STRING],
861942 defaultValue: 'System',
862943 enumProvider: () => [...commonEnumProviders.characters('character')(), new SlashCommandEnumValue('System', null, enumTypes.enum, enumIcons.assistant)],
@@ -2293,7 +2374,8 @@ async function generateCallback(args, value) {
22932374
22942375 setEphemeralStopStrings(resolveVariable(args?.stop));
22952376 const name = args?.name;
22962377 const resultchar = await generateQuietPromptfindChar(value, quietToLoud, false,{ '',name: name, length});
2378+ const result = await generateQuietPrompt(value, quietToLoud, false, '', char?.name ?? name, length);
22972379 return result;
22982380 } catch (err) {
22992381 console.error('Error on /gen generation', err);
@@ -2481,26 +2563,22 @@ async function askCharacter(args, text) {
24812563 return '';
24822564 }
24832565
2484- let name = '';
2566+ if (!args.name) {
2485-
2567+ 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- }
24932569 }
24942570
24952571 const prevChId = this_chid;
24962572
24972573 // 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) {
25002576 toastr.error('Character not found.');
25012577 return '';
25022578 }
25032579
2580+ const chId = getCharIndex(character);
2581+
25042582 if (text) {
25052583 const mesText = getRegexedString(text.trim(), regex_placement.SLASH_COMMAND);
25062584 // 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) {
25112589 // Override character and send a user message
25122590 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
25282596 const restoreCharacter = () => {
25292597 if (String(this_chid) !== String(chId)) {
25302598 return;
25312599 }
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
25362609 // Only force the new avatar if the character name is the same
25372610 // This skips if an error was fired
25382611 const lastMessage = chat[chat.length - 1];
25392612 if (lastMessage && lastMessage?.name === character.name) {
25402613 lastMessage.force_avatar = force_avatar;
25412614 lastMessage.original_avatar = original_avatar;
25422615 }
@@ -2547,7 +2620,7 @@ async function askCharacter(args, text) {
25472620 // Run generate and restore previous character
25482621 try {
25492622 eventSource.once(event_types.MESSAGE_RECEIVED, restoreCharacter);
25502623 toastr.info(`Asking ${character.name} something...`);
25512624 askResult = await Generate('ask_command');
25522625 } catch (error) {
25532626 restoreCharacter();
@@ -2741,26 +2814,23 @@ async function removeGroupMemberCallback(_, arg) {
27412814 return '';
27422815}
27432816
27442817async function addGroupMemberCallback(_, argname) {
27452818 if (!selected_group) {
27462819 toastr.warning('Cannot run /memberadd command outside of a group chat.');
27472820 return '';
27482821 }
27492822
27502823 if (!argname) {
27512824 console.warn('WARN: No argument provided for /memberadd command');
27522825 return '';
27532826 }
27542827
2755- arg = arg.trim();
2828+ const character = findChar({ name: name, preferCurrentChar: false });
2756- const chid = findCharacterIndex(arg);
2829+ if (!character) {
2757-
2830+ console.warn(`WARN: No character found for argument ${name}`);
2758- if (chid === -1) {
2759- console.warn(`WARN: No character found for argument ${arg}`);
27602831 return '';
27612832 }
27622833
2763- const character = characters[chid];
27642834 const group = groups.find(x => x.id === selected_group);
27652835
27662836 if (!group || !Array.isArray(group.members)) {
@@ -2829,7 +2899,7 @@ function findPersonaByName(name) {
28292899 }
28302900
28312901 for (const persona of Object.entries(power_user.personas)) {
28322902 if (equalsIgnoreCaseAndAccents(persona[1].toLowerCase() ===, name.toLowerCase()) {
28332903 return persona[0];
28342904 }
28352905 }
@@ -2872,7 +2942,9 @@ async function deleteMessagesByNameCallback(_, name) {
28722942 return;
28732943 }
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
28772949 const messagesToDelete = [];
28782950 chat.forEach((value) => {
@@ -2901,60 +2973,34 @@ async function deleteMessagesByNameCallback(_, name) {
29012973 return '';
29022974}
29032975
2904-function 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-
29272976async function goToCharacterCallback(_, name) {
29282977 if (!name) {
29292978 console.warn('WARN: No character name provided for /go command');
29302979 return;
29312980 }
29322981
2933- name = name.trim();
2982+ const character = findChar({ name: name });
2934- const characterIndex = findCharacterIndex(name);
2983+ if (character) {
2935-
2984+ 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);
29392987 setActiveGroup(null);
29402988 return characters[characterIndex]?character.name;
29412989 } else {
29422990 const group = groups.find(it => equalsIgnoreCaseAndAccents(it.name.toLowerCase() ==, name.toLowerCase());
29432991 if (group) {
29442992 await openGroupById(group.id);
29452993 setActiveCharacter(null);
29462994 setActiveGroup(group.id);
29472995 return group.name;
2948- } else {
2949- console.warn(`No matches found for name "${name}"`);
2950- return '';
2951- }
29522996 }
2997+ console.warn(`No matches found for name "${name}"`);
2998+ return '';
29532999}
29543000
29553001async function openChat(idchid) {
29563002 resetSelectedGroup();
29573003 setCharacterId(idchid);
29583004 await delay(1);
29593005 await reloadCurrentChat();
29603006}
@@ -3100,6 +3146,79 @@ async function setNarratorName(_, text) {
31003146 return '';
31013147}
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+ */
3158+export 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+ */
3177+export 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+ */
3198+export 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+
31033222export async function sendMessageAs(args, text) {
31043223 if (!text) {
31053224 return '';
@@ -3138,26 +3257,18 @@ export async function sendMessageAs(args, text) {
31383257 const isSystem = bias && !removeMacros(mesText).length;
31393258 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 avatars
3263+ 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;
31573266 }
31583267
3268+ const { name: avatarCharName, force_avatar, original_avatar } = getNameAndAvatarForMessage(avatarCharacter, name);
3269+
31593270 const message = {
3160- name: name,
3271+ name: character?.name || name || avatarCharName,
31613272 is_user: false,
31623273 is_system: isSystem,
31633274 send_date: getMessageTimeStamp(),
public/scripts/slash-commands/SlashCommand.js+2 -2
@@ -15,13 +15,13 @@ import { SlashCommandScope } from './SlashCommandScope.js';
1515 * _abortController:SlashCommandAbortController,
1616 * _debugController:SlashCommandDebugController,
1717 * _hasUnnamedArgument:boolean,
1818 * [id:string]:string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined,
1919 * }} NamedArguments
2020 */
2121
2222/**
2323 * Alternative object for local JSDocs, where you don't need existing pipe, scope, etc. arguments
2424 * @typedef {{[id:string]:string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined}} NamedArgumentsCapture
2525 */
2626
2727/**
public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js+14 -2
@@ -2,7 +2,7 @@ import { chat_metadata, characters, substituteParams, chat, extension_prompt_rol
22import { extension_settings } from '../extensions.js';
33import { getGroupMembers, groups } from '../group-chats.js';
44import { power_user } from '../power-user.js';
55import { searchCharByName, getTagsList, tags, tag_map } from '../tags.js';
66import { world_names } from '../world-info.js';
77import { SlashCommandClosure } from './SlashCommandClosure.js';
88import { SlashCommandEnumValue, enumTypes } from './SlashCommandEnumValue.js';
@@ -182,6 +182,18 @@ export const commonEnumProviders = {
182182 personas: () => Object.values(power_user.personas).map(persona => new SlashCommandEnumValue(persona, null, enumTypes.name, enumIcons.persona)),
183183
184184 /**
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+ /**
185197 * All possible tags for a given char/group entity
186198 *
187199 * @param {('all' | 'existing' | 'not-existing')?} [mode='all'] - Which types of tags to show
@@ -193,7 +205,7 @@ export const commonEnumProviders = {
193205 if (charName instanceof SlashCommandClosure) throw new Error('Argument \'name\' does not support closures');
194206 const key = searchCharByName(substituteParams(charName), { suppressLogging: true });
195207 const assigned = key ? getTagsList(key) : [];
196208 return tags.filter(it => !key || mode === 'all' || mode === 'existing' && assigned.includes(it) || mode === 'not-existing' && !assigned.includes(it))
197209 .map(tag => new SlashCommandEnumValue(tag.name, null, enumTypes.command, enumIcons.tag));
198210 },
199211
public/scripts/tags.js+8 -8
@@ -15,7 +15,7 @@ import {
1515import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, FilterHelper } from './filters.js';
1616
1717import { groupCandidatesFilter, groups, selected_group } from './group-chats.js';
1818import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, flashHighlight, equalsIgnoreCaseAndAccents, includesIgnoreCaseAndAccents, removeFromArray, getFreeName, debounce, findChar } from './utils.js';
1919import { power_user } from './power-user.js';
2020import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
2121import { SlashCommand } from './slash-commands/SlashCommand.js';
@@ -50,7 +50,6 @@ export {
5050 removeTagFromMap,
5151};
5252
53-/** @typedef {import('../scripts/popup.js').Popup} Popup */
5453/** @typedef {import('../script.js').Character} Character */
5554
5655const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter';
@@ -507,7 +506,7 @@ export function getTagKeyForEntityElement(element) {
507506 */
508507export function searchCharByName(charName, { suppressLogging = false } = {}) {
509508 const entity = charName
510509 ? (characters.findfindChar(x =>{ x.name ===: charName }) || groups.find(x => equalsIgnoreCaseAndAccents(x.name ==, charName)))
511510 : (selected_group ? groups.find(x => x.id == selected_group) : characters[this_chid]);
512511 const key = getTagKeyForEntity(entity);
513512 if (!key) {
@@ -1861,8 +1860,9 @@ function registerTagsSlashCommands() {
18611860 return String(result);
18621861 },
18631862 namedArgumentList: [
18641863 SlashCommandNamedArgument.fromProps({ name: 'name',
18651864 descriptionname: 'Character name',
1865+ description: 'Character name - or unique character identifier (avatar key)',
18661866 typeList: [ARGUMENT_TYPE.STRING],
18671867 defaultValue: '{{char}}',
18681868 enumProvider: commonEnumProviders.characters(),
@@ -1907,7 +1907,7 @@ function registerTagsSlashCommands() {
19071907 },
19081908 namedArgumentList: [
19091909 SlashCommandNamedArgument.fromProps({ name: 'name',
19101910 description: 'Character name - or unique character identifier (avatar key)',
19111911 typeList: [ARGUMENT_TYPE.STRING],
19121912 defaultValue: '{{char}}',
19131913 enumProvider: commonEnumProviders.characters(),
@@ -1950,7 +1950,7 @@ function registerTagsSlashCommands() {
19501950 namedArgumentList: [
19511951 SlashCommandNamedArgument.fromProps({
19521952 name: 'name',
19531953 description: 'Character name - or unique character identifier (avatar key)',
19541954 typeList: [ARGUMENT_TYPE.STRING],
19551955 defaultValue: '{{char}}',
19561956 enumProvider: commonEnumProviders.characters(),
@@ -1993,7 +1993,7 @@ function registerTagsSlashCommands() {
19931993 namedArgumentList: [
19941994 SlashCommandNamedArgument.fromProps({
19951995 name: 'name',
19961996 description: 'Character name - or unique character identifier (avatar key)',
19971997 typeList: [ARGUMENT_TYPE.STRING],
19981998 defaultValue: '{{char}}',
19991999 enumProvider: commonEnumProviders.characters(),
public/scripts/utils.js+74 -1
@@ -1,10 +1,12 @@
11import { getContext } from './extensions.js';
22import { characters, getRequestHeaders, this_chid } from '../script.js';
33import { isMobile } from './RossAscends-mods.js';
44import { collapseNewlines } from './power-user.js';
55import { debounce_timeout } from './constants.js';
66import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
77import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
8+import { getTagsList } from './tags.js';
9+import { groups, selected_group } from './group-chats.js';
810
911/**
1012 * Pagination status string template.
@@ -2110,3 +2112,74 @@ export async function showFontAwesomePicker(customList = null) {
21102112 }
21112113 return null;
21122114}
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+ */
2127+export 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+ */
2180+export 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;
995995 bottom: 0;
996996}
997997
998+
998999.swipes-counter {
9991000 color: var(--SmartThemeBodyColor);
10001001 font-size: 12px;
@@ -2646,6 +2647,11 @@ select option:not(:checked) {
26462647 color: var(--active) !important;
26472648}
26482649
2650+#instruct_enabled_label .menu_button:not(.toggleEnabled),
2651+#sysprompt_enabled_label .menu_button:not(.toggleEnabled) {
2652+ color: Red;
2653+}
2654+
26492655.displayBlock {
26502656 display: block !important;
26512657}