Merge pull request #3664 from SillyTavern/hide-name Add "name" argument to /hide and /unhide

c4021525ac561bcf13c240d0aa07436322c3e8ca

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

Signed
4 files changed, +126 -32Showing whitespace changes
public/scripts/chats.js+3 -1
@@ -130,9 +130,10 @@ function getConverter(type) {
130130 * @param {number} start Starting message ID
131131 * @param {number} end Ending message ID (inclusive)
132132 * @param {boolean} unhide If true, unhide the messages instead.
133+ * @param {string} nameFitler Optional name filter
133134 * @returns {Promise<void>}
134135 */
135136export async function hideChatMessageRange(start, end, unhide, nameFitler = null) {
136137 if (isNaN(start)) return;
137138 if (!end) end = start;
138139 const hide = !unhide;
@@ -140,6 +141,7 @@ export async function hideChatMessageRange(start, end, unhide) {
140141 for (let messageId = start; messageId <= end; messageId++) {
141142 const message = chat[messageId];
142143 if (!message) continue;
144+ if (nameFitler && message.name !== nameFitler) continue;
143145
144146 message.is_system = hide;
145147
public/scripts/slash-commands.js+34 -22
@@ -667,11 +667,21 @@ export function initDefaultSlashCommands() {
667667 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
668668 name: 'hide',
669669 callback: hideMessageCallback,
670+ namedArgumentList: [
671+ SlashCommandNamedArgument.fromProps({
672+ name: 'name',
673+ description: 'only hide messages from a certain character or persona',
674+ typeList: [ARGUMENT_TYPE.STRING],
675+ enumProvider: commonEnumProviders.messageNames,
676+ isRequired: false,
677+ acceptsMultiple: false,
678+ }),
679+ ],
670680 unnamedArgumentList: [
671681 SlashCommandArgument.fromProps({
672682 description: 'message index (starts with 0) or range, defaults to the last message index if not provided',
673683 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE],
674684 isRequired: truefalse,
675685 enumProvider: commonEnumProviders.messages(),
676686 }),
677687 ],
@@ -680,11 +690,21 @@ export function initDefaultSlashCommands() {
680690 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
681691 name: 'unhide',
682692 callback: unhideMessageCallback,
693+ namedArgumentList: [
694+ SlashCommandNamedArgument.fromProps({
695+ name: 'name',
696+ description: 'only unhide messages from a certain character or persona',
697+ typeList: [ARGUMENT_TYPE.STRING],
698+ enumProvider: commonEnumProviders.messageNames,
699+ isRequired: false,
700+ acceptsMultiple: false,
701+ }),
702+ ],
683703 unnamedArgumentList: [
684704 SlashCommandArgument.fromProps({
685705 description: 'message index (starts with 0) or range, defaults to the last message index if not provided',
686706 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE],
687707 isRequired: truefalse,
688708 enumProvider: commonEnumProviders.messages(),
689709 }),
690710 ],
@@ -3034,37 +3054,29 @@ async function askCharacter(args, text) {
30343054 return await slashCommandReturnHelper.doReturn(args.return ?? 'pipe', message, { objectToStringFunc: x => x.mes });
30353055}
30363056
30373057async function hideMessageCallback(_args, argvalue) {
3038- if (!arg) {
3058+ const range = value ? stringToRange(value, 0, chat.length - 1) : { start: chat.length - 1, end: chat.length - 1 };
3039- console.warn('WARN: No argument provided for /hide command');
3040- return '';
3041- }
3042-
3043- const range = stringToRange(arg, 0, chat.length - 1);
30443059
30453060 if (!range) {
30463061 console.warn(`WARN: Invalid range provided for /hide command: ${argvalue}`);
3047- return '';
3048- }
3049-
3050- await hideChatMessageRange(range.start, range.end, false);
30513062 return '';
30523063 }
30533064
3054-async function unhideMessageCallback(_, arg) {
3065+ const nameFilter = String(args.name ?? '').trim();
3055- if (!arg) {
3066+ await hideChatMessageRange(range.start, range.end, false, nameFilter);
3056- console.warn('WARN: No argument provided for /unhide command');
30573067 return '';
30583068}
30593069
3060- const range = stringToRange(arg, 0, chat.length - 1);
3070+async function unhideMessageCallback(args, value) {
3071+ const range = value ? stringToRange(value, 0, chat.length - 1) : { start: chat.length - 1, end: chat.length - 1 };
30613072
30623073 if (!range) {
30633074 console.warn(`WARN: Invalid range provided for /unhide command: ${argvalue}`);
30643075 return '';
30653076 }
30663077
3067- await hideChatMessageRange(range.start, range.end, true);
3078+ const nameFilter = String(args.name ?? '').trim();
3079+ await hideChatMessageRange(range.start, range.end, true, nameFilter);
30683080 return '';
30693081}
30703082
public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js+19 -3
@@ -3,6 +3,7 @@ import { 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';
6+import { onlyUniqueJson, sortIgnoreCaseAndAccents } from '../utils.js';
67import { world_names } from '../world-info.js';
78import { SlashCommandClosure } from './SlashCommandClosure.js';
89import { SlashCommandEnumValue, enumTypes } from './SlashCommandEnumValue.js';
@@ -251,15 +252,30 @@ export const commonEnumProviders = {
251252 * @param {boolean} [options.allowVars=false] - Whether to add enum option for variable names
252253 * @returns {(executor:SlashCommandExecutor, scope:SlashCommandScope) => SlashCommandEnumValue[]}
253254 */
254255 messages: ({ allowIdAfter = false, allowVars = false } = {}) => (_executor, scope) => {
256+ const nameFilter = executor.namedArgumentList.find(it => it.name == 'name')?.value || '';
255257 return [
256258 ...chat.map((message, index) => new SlashCommandEnumValue(String(index), `${message.name}: ${message.mes}`, enumTypes.number, message.is_user ? enumIcons.user : message.is_system ? enumIcons.system : enumIcons.assistant)).filter(value => !nameFilter || value.description.startsWith(`${nameFilter}:`)),
257259 ...allowIdAfter ? [new SlashCommandEnumValue(String(chat.length), '>> After Last Message >>', enumTypes.enum, '➕')] : [],
258260 ...allowVars ? commonEnumProviders.variables('all')(_executor, scope) : [],
259261 ];
260262 },
261263
262264 /**
265+ * All names used in the current chat.
266+ *
267+ * @returns {SlashCommandEnumValue[]}
268+ */
269+ messageNames: () => chat
270+ .map(message => ({
271+ name: message.name,
272+ icon: message.is_user ? enumIcons.user : enumIcons.assistant,
273+ }))
274+ .filter(onlyUniqueJson)
275+ .sort((a, b) => sortIgnoreCaseAndAccents(a.name, b.name))
276+ .map(name => new SlashCommandEnumValue(name.name, null, null, name.icon)),
277+
278+ /**
263279 * All existing worlds / lorebooks
264280 *
265281 * @returns {SlashCommandEnumValue[]}
public/scripts/utils.js+70 -6
@@ -6,7 +6,7 @@ import {
66} from '../lib.js';
77
88import { getContext } from './extensions.js';
99import { characters, getRequestHeaders, this_chid, user_avatar } from '../script.js';
1010import { isMobile } from './RossAscends-mods.js';
1111import { collapseNewlines, power_user } from './power-user.js';
1212import { debounce_timeout } from './constants.js';
@@ -14,7 +14,7 @@ import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
1414import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
1515import { getTagsList } from './tags.js';
1616import { groups, selected_group } from './group-chats.js';
1717import { getCurrentLocale, t } from './i18n.js';
1818
1919/**
2020 * Pagination status string template.
@@ -197,6 +197,17 @@ export function onlyUnique(value, index, array) {
197197}
198198
199199/**
200+ * Determines if a value is unique in an array of objects.
201+ * @param {any} value Current value.
202+ * @param {number} index Current index.
203+ * @param {any[]} array The array being processed.
204+ * @returns {boolean} True if the value is unique, false otherwise.
205+ */
206+export function onlyUniqueJson(value, index, array) {
207+ return array.map(v => JSON.stringify(v)).indexOf(JSON.stringify(value)) === index;
208+}
209+
210+/**
200211 * Removes the first occurrence of a specified item from an array
201212 *
202213 * @param {*[]} array - The array from which to remove the item
@@ -1799,8 +1810,9 @@ export function runAfterAnimation(control, callback, timeout = 500) {
17991810 *
18001811 * @param {string} a - The first string to compare.
18011812 * @param {string} b - The second string to compare.
18021813 * @param {(a:string,b:string)=>booleanT} comparisonFunction - The function to use for the comparison.
18031814 * @returns {*T} - The result of the comparison.
1815+ * @template T
18041816 */
18051817export function compareIgnoreCaseAndAccents(a, b, comparisonFunction) {
18061818 if (!a || !b) return comparisonFunction(a, b); // Return the comparison result if either string is empty
@@ -1838,6 +1850,16 @@ export function equalsIgnoreCaseAndAccents(a, b) {
18381850}
18391851
18401852/**
1853+ * Performs a case-insensitive and accent-insensitive sort.
1854+ * @param {string} a - The first string to compare
1855+ * @param {string} b - The second string to compare
1856+ * @returns {number} -1 if a < b, 1 if a > b, 0 if a === b
1857+ */
1858+export function sortIgnoreCaseAndAccents(a, b) {
1859+ return compareIgnoreCaseAndAccents(a, b, (a, b) => a?.localeCompare(b));
1860+}
1861+
1862+/**
18411863 * @typedef {object} Select2Option The option object for select2 controls
18421864 * @property {string} id - The unique ID inside this select
18431865 * @property {string} text - The text for this option
@@ -2197,6 +2219,48 @@ export async function showFontAwesomePicker(customList = null) {
21972219}
21982220
21992221/**
2222+ * Finds a persona by name, with optional filtering and precedence for avatars
2223+ * @param {object} [options={}] - The options for the search
2224+ * @param {string?} [options.name=null] - The name to search for
2225+ * @param {boolean} [options.allowAvatar=true] - Whether to allow searching by avatar
2226+ * @param {boolean} [options.insensitive=true] - Whether the search should be case insensitive
2227+ * @param {boolean} [options.preferCurrentPersona=true] - Whether to prefer the current persona(s)
2228+ * @param {boolean} [options.quiet=false] - Whether to suppress warnings
2229+ * @returns {PersonaViewModel} The persona object
2230+ * @typedef {object} PersonaViewModel
2231+ * @property {string} avatar - The avatar of the persona
2232+ * @property {string} name - The name of the persona
2233+ */
2234+export function findPersona({ name = null, allowAvatar = true, insensitive = true, preferCurrentPersona = true, quiet = false } = {}) {
2235+ /** @type {PersonaViewModel[]} */
2236+ const personas = Object.entries(power_user.personas).map(([avatar, name]) => ({ avatar, name }));
2237+ const matches = (/** @type {PersonaViewModel} */ persona) => !name || (allowAvatar && persona.avatar === name) || (insensitive ? equalsIgnoreCaseAndAccents(persona.name, name) : persona.name === name);
2238+
2239+ // If we have a current persona and prefer it, return that if it matches
2240+ const currentPersona = personas.find(a => a.avatar === user_avatar);
2241+ if (preferCurrentPersona && currentPersona && matches(currentPersona)) {
2242+ return currentPersona;
2243+ }
2244+
2245+ // If allowAvatar is true, search by avatar first
2246+ if (allowAvatar && name) {
2247+ const personaByAvatar = personas.find(a => a.avatar === name);
2248+ if (personaByAvatar && matches(personaByAvatar)) {
2249+ return personaByAvatar;
2250+ }
2251+ }
2252+
2253+ // Search for matching personas by name
2254+ const matchingPersonas = personas.filter(a => matches(a));
2255+ if (matchingPersonas.length > 1) {
2256+ if (!quiet) toastr.warning(t`Multiple personas found for given conditions.`);
2257+ else console.warn(t`Multiple personas found for given conditions. Returning the first match.`);
2258+ }
2259+
2260+ return matchingPersonas[0] || null;
2261+}
2262+
2263+/**
22002264 * Finds a character by name, with optional filtering and precedence for avatars
22012265 * @param {object} [options={}] - The options for the search
22022266 * @param {string?} [options.name=null] - The name to search for
@@ -2228,8 +2292,8 @@ export function findChar({ name = null, allowAvatar = true, insensitive = true,
22282292 if (preferCurrentChar) {
22292293 const preferredCharSearch = currentChars.filter(matches);
22302294 if (preferredCharSearch.length > 1) {
22312295 if (!quiet) toastr.warning('t`Multiple characters found for given conditions.'`);
22322296 else console.warn('t`Multiple characters found for given conditions. Returning the first match.'`);
22332297 }
22342298 if (preferredCharSearch.length) {
22352299 return preferredCharSearch[0];