diff --git a/public/scripts/chats.js b/public/scripts/chats.js index 4584bfc8a..a6094b7f7 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -130,9 +130,10 @@ function getConverter(type) { * @param {number} start Starting message ID * @param {number} end Ending message ID (inclusive) * @param {boolean} unhide If true, unhide the messages instead. + * @param {string} nameFitler Optional name filter * @returns {Promise} */ -export async function hideChatMessageRange(start, end, unhide) { +export async function hideChatMessageRange(start, end, unhide, nameFitler = null) { if (isNaN(start)) return; if (!end) end = start; const hide = !unhide; @@ -140,6 +141,7 @@ export async function hideChatMessageRange(start, end, unhide) { for (let messageId = start; messageId <= end; messageId++) { const message = chat[messageId]; if (!message) continue; + if (nameFitler && message.name !== nameFitler) continue; message.is_system = hide; diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index a35fb174b..70d5d7187 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -667,11 +667,21 @@ export function initDefaultSlashCommands() { SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'hide', callback: hideMessageCallback, + namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'name', + description: 'only hide messages from a certain character or persona', + typeList: [ARGUMENT_TYPE.STRING], + enumProvider: commonEnumProviders.messageNames, + isRequired: false, + acceptsMultiple: false, + }), + ], unnamedArgumentList: [ SlashCommandArgument.fromProps({ - description: 'message index (starts with 0) or range', + description: 'message index (starts with 0) or range, defaults to the last message index if not provided', typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE], - isRequired: true, + isRequired: false, enumProvider: commonEnumProviders.messages(), }), ], @@ -680,11 +690,21 @@ export function initDefaultSlashCommands() { SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'unhide', callback: unhideMessageCallback, + namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'name', + description: 'only unhide messages from a certain character or persona', + typeList: [ARGUMENT_TYPE.STRING], + enumProvider: commonEnumProviders.messageNames, + isRequired: false, + acceptsMultiple: false, + }), + ], unnamedArgumentList: [ SlashCommandArgument.fromProps({ - description: 'message index (starts with 0) or range', + description: 'message index (starts with 0) or range, defaults to the last message index if not provided', typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE], - isRequired: true, + isRequired: false, enumProvider: commonEnumProviders.messages(), }), ], @@ -3034,37 +3054,37 @@ async function askCharacter(args, text) { return await slashCommandReturnHelper.doReturn(args.return ?? 'pipe', message, { objectToStringFunc: x => x.mes }); } -async function hideMessageCallback(_, arg) { - if (!arg) { - console.warn('WARN: No argument provided for /hide command'); - return ''; +async function hideMessageCallback(args, value) { + if (!value) { + console.log('No range provided. Hiding the last message.'); } - const range = stringToRange(arg, 0, chat.length - 1); + const range = value ? stringToRange(value, 0, chat.length - 1) : { start: chat.length - 1, end: chat.length - 1 }; if (!range) { - console.warn(`WARN: Invalid range provided for /hide command: ${arg}`); + console.warn(`WARN: Invalid range provided for /hide command: ${value}`); return ''; } - await hideChatMessageRange(range.start, range.end, false); + const nameFilter = String(args.name ?? '').trim(); + await hideChatMessageRange(range.start, range.end, false, nameFilter); return ''; } -async function unhideMessageCallback(_, arg) { - if (!arg) { - console.warn('WARN: No argument provided for /unhide command'); - return ''; +async function unhideMessageCallback(args, value) { + if (!value) { + console.log('No range provided. Unhiding the last message'); } - const range = stringToRange(arg, 0, chat.length - 1); + const range = value ? stringToRange(value, 0, chat.length - 1) : { start: chat.length - 1, end: chat.length - 1 }; if (!range) { - console.warn(`WARN: Invalid range provided for /unhide command: ${arg}`); + console.warn(`WARN: Invalid range provided for /unhide command: ${value}`); return ''; } - await hideChatMessageRange(range.start, range.end, true); + const nameFilter = String(args.name ?? '').trim(); + await hideChatMessageRange(range.start, range.end, true, nameFilter); return ''; } diff --git a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js index 88fe8ffd6..3b858865a 100644 --- a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js +++ b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js @@ -3,6 +3,7 @@ import { extension_settings } from '../extensions.js'; import { getGroupMembers, groups } from '../group-chats.js'; import { power_user } from '../power-user.js'; import { searchCharByName, getTagsList, tags, tag_map } from '../tags.js'; +import { onlyUnique } from '../utils.js'; import { world_names } from '../world-info.js'; import { SlashCommandClosure } from './SlashCommandClosure.js'; import { SlashCommandEnumValue, enumTypes } from './SlashCommandEnumValue.js'; @@ -251,14 +252,22 @@ export const commonEnumProviders = { * @param {boolean} [options.allowVars=false] - Whether to add enum option for variable names * @returns {(executor:SlashCommandExecutor, scope:SlashCommandScope) => SlashCommandEnumValue[]} */ - messages: ({ allowIdAfter = false, allowVars = false } = {}) => (_, scope) => { + messages: ({ allowIdAfter = false, allowVars = false } = {}) => (executor, scope) => { + const nameFilter = executor.namedArgumentList.find(it => it.name == 'name')?.value || ''; return [ - ...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)), + ...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}:`)), ...allowIdAfter ? [new SlashCommandEnumValue(String(chat.length), '>> After Last Message >>', enumTypes.enum, '➕')] : [], - ...allowVars ? commonEnumProviders.variables('all')(_, scope) : [], + ...allowVars ? commonEnumProviders.variables('all')(executor, scope) : [], ]; }, + /** + * All names used in the current chat. + * + * @returns {SlashCommandEnumValue[]} + */ + messageNames: () => chat.map((message) => message.name).filter(onlyUnique).sort(Intl.Collator().compare).map(name => new SlashCommandEnumValue(name)), + /** * All existing worlds / lorebooks * diff --git a/public/scripts/utils.js b/public/scripts/utils.js index c65192795..bbdc33691 100644 --- a/public/scripts/utils.js +++ b/public/scripts/utils.js @@ -6,7 +6,7 @@ import { } from '../lib.js'; import { getContext } from './extensions.js'; -import { characters, getRequestHeaders, this_chid } from '../script.js'; +import { characters, getRequestHeaders, this_chid, user_avatar } from '../script.js'; import { isMobile } from './RossAscends-mods.js'; import { collapseNewlines, power_user } from './power-user.js'; import { debounce_timeout } from './constants.js'; @@ -2196,6 +2196,48 @@ export async function showFontAwesomePicker(customList = null) { return null; } +/** + * Finds a persona by name, with optional filtering and precedence for avatars + * @param {object} [options={}] - The options for the search + * @param {string?} [options.name=null] - The name to search for + * @param {boolean} [options.allowAvatar=true] - Whether to allow searching by avatar + * @param {boolean} [options.insensitive=true] - Whether the search should be case insensitive + * @param {boolean} [options.preferCurrentPersona=true] - Whether to prefer the current persona(s) + * @param {boolean} [options.quiet=false] - Whether to suppress warnings + * @returns {PersonaViewModel} The persona object + * @typedef {object} PersonaViewModel + * @property {string} avatar - The avatar of the persona + * @property {string} name - The name of the persona + */ +export function findPersona({ name = null, allowAvatar = true, insensitive = true, preferCurrentPersona = true, quiet = false } = {}) { + /** @type {PersonaViewModel[]} */ + const personas = Object.entries(power_user.personas).map(([avatar, name]) => ({ avatar, name })); + const matches = (/** @type {PersonaViewModel} */ persona) => !name || (allowAvatar && persona.avatar === name) || (insensitive ? equalsIgnoreCaseAndAccents(persona.name, name) : persona.name === name); + + // If we have a current persona and prefer it, return that if it matches + const currentPersona = personas.find(a => a.avatar === user_avatar); + if (preferCurrentPersona && currentPersona && matches(currentPersona)) { + return currentPersona; + } + + // If allowAvatar is true, search by avatar first + if (allowAvatar && name) { + const personaByAvatar = personas.find(a => a.avatar === name); + if (personaByAvatar && matches(personaByAvatar)) { + return personaByAvatar; + } + } + + // Search for matching personas by name + const matchingPersonas = personas.filter(a => matches(a)); + if (matchingPersonas.length > 1) { + if (!quiet) toastr.warning('Multiple personas found for given conditions.'); + else console.warn('Multiple personas found for given conditions. Returning the first match.'); + } + + return matchingPersonas[0] || null; +} + /** * Finds a character by name, with optional filtering and precedence for avatars * @param {object} [options={}] - The options for the search