Merge pull request #3664 from SillyTavern/hide-name Add "name" argument to /hide and /unhide
Signed| @@ -130,9 +130,10 @@ function getConverter(type) { | ||
| 130 | 130 | * @param {number} start Starting message ID |
| 131 | 131 | * @param {number} end Ending message ID (inclusive) |
| 132 | 132 | * @param {boolean} unhide If true, unhide the messages instead. |
| 133 | + * @param {string} nameFitler Optional name filter | |
| 133 | 134 | * @returns {Promise<void>} |
| 134 | 135 | */ |
| 135 | 136 | export async function hideChatMessageRange(start, end, unhide, nameFitler = null) { |
| 136 | 137 | if (isNaN(start)) return; |
| 137 | 138 | if (!end) end = start; |
| 138 | 139 | const hide = !unhide; |
| @@ -140,6 +141,7 @@ export async function hideChatMessageRange(start, end, unhide) { | ||
| 140 | 141 | for (let messageId = start; messageId <= end; messageId++) { |
| 141 | 142 | const message = chat[messageId]; |
| 142 | 143 | if (!message) continue; |
| 144 | + if (nameFitler && message.name !== nameFitler) continue; | |
| 143 | 145 | |
| 144 | 146 | message.is_system = hide; |
| 145 | 147 | |
| @@ -667,11 +667,21 @@ export function initDefaultSlashCommands() { | ||
| 667 | 667 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 668 | 668 | name: 'hide', |
| 669 | 669 | 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 | + ], | |
| 670 | 680 | unnamedArgumentList: [ |
| 671 | 681 | SlashCommandArgument.fromProps({ |
| 672 | 682 | description: 'message index (starts with 0) or range, defaults to the last message index if not provided', |
| 673 | 683 | typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE], |
| 674 | 684 | isRequired: truefalse, |
| 675 | 685 | enumProvider: commonEnumProviders.messages(), |
| 676 | 686 | }), |
| 677 | 687 | ], |
| @@ -680,11 +690,21 @@ export function initDefaultSlashCommands() { | ||
| 680 | 690 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 681 | 691 | name: 'unhide', |
| 682 | 692 | 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 | + ], | |
| 683 | 703 | unnamedArgumentList: [ |
| 684 | 704 | SlashCommandArgument.fromProps({ |
| 685 | 705 | description: 'message index (starts with 0) or range, defaults to the last message index if not provided', |
| 686 | 706 | typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE], |
| 687 | 707 | isRequired: truefalse, |
| 688 | 708 | enumProvider: commonEnumProviders.messages(), |
| 689 | 709 | }), |
| 690 | 710 | ], |
| @@ -3034,37 +3054,29 @@ async function askCharacter(args, text) { | ||
| 3034 | 3054 | return await slashCommandReturnHelper.doReturn(args.return ?? 'pipe', message, { objectToStringFunc: x => x.mes }); |
| 3035 | 3055 | } |
| 3036 | 3056 | |
| 3037 | 3057 | async 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); | |
| 3044 | 3059 | |
| 3045 | 3060 | if (!range) { |
| 3046 | 3061 | console.warn(`WARN: Invalid range provided for /hide command: ${argvalue}`); |
| 3047 | 3062 | return ''; |
| 3048 | 3063 | } |
| 3049 | 3064 | |
| 3050 | - await hideChatMessageRange(range.start, range.end, false); | |
| 3065 | + const nameFilter = String(args.name ?? '').trim(); | |
| 3066 | + await hideChatMessageRange(range.start, range.end, false, nameFilter); | |
| 3051 | 3067 | return ''; |
| 3052 | 3068 | } |
| 3053 | 3069 | |
| 3054 | 3070 | async function unhideMessageCallback(_args, argvalue) { |
| 3055 | - if (!arg) { | |
| 3071 | + const range = value ? stringToRange(value, 0, chat.length - 1) : { start: chat.length - 1, end: chat.length - 1 }; | |
| 3056 | - console.warn('WARN: No argument provided for /unhide command'); | |
| 3057 | - return ''; | |
| 3058 | - } | |
| 3059 | - | |
| 3060 | - const range = stringToRange(arg, 0, chat.length - 1); | |
| 3061 | 3072 | |
| 3062 | 3073 | if (!range) { |
| 3063 | 3074 | console.warn(`WARN: Invalid range provided for /unhide command: ${argvalue}`); |
| 3064 | 3075 | return ''; |
| 3065 | 3076 | } |
| 3066 | 3077 | |
| 3067 | - await hideChatMessageRange(range.start, range.end, true); | |
| 3078 | + const nameFilter = String(args.name ?? '').trim(); | |
| 3079 | + await hideChatMessageRange(range.start, range.end, true, nameFilter); | |
| 3068 | 3080 | return ''; |
| 3069 | 3081 | } |
| 3070 | 3082 | |
| @@ -3,6 +3,7 @@ import { extension_settings } from '../extensions.js'; | ||
| 3 | 3 | import { getGroupMembers, groups } from '../group-chats.js'; |
| 4 | 4 | import { power_user } from '../power-user.js'; |
| 5 | 5 | import { searchCharByName, getTagsList, tags, tag_map } from '../tags.js'; |
| 6 | +import { onlyUniqueJson, sortIgnoreCaseAndAccents } from '../utils.js'; | |
| 6 | 7 | import { world_names } from '../world-info.js'; |
| 7 | 8 | import { SlashCommandClosure } from './SlashCommandClosure.js'; |
| 8 | 9 | import { SlashCommandEnumValue, enumTypes } from './SlashCommandEnumValue.js'; |
| @@ -251,15 +252,30 @@ export const commonEnumProviders = { | ||
| 251 | 252 | * @param {boolean} [options.allowVars=false] - Whether to add enum option for variable names |
| 252 | 253 | * @returns {(executor:SlashCommandExecutor, scope:SlashCommandScope) => SlashCommandEnumValue[]} |
| 253 | 254 | */ |
| 254 | 255 | messages: ({ allowIdAfter = false, allowVars = false } = {}) => (_executor, scope) => { |
| 256 | + const nameFilter = executor.namedArgumentList.find(it => it.name == 'name')?.value || ''; | |
| 255 | 257 | return [ |
| 256 | 258 | ...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}:`)), |
| 257 | 259 | ...allowIdAfter ? [new SlashCommandEnumValue(String(chat.length), '>> After Last Message >>', enumTypes.enum, '➕')] : [], |
| 258 | 260 | ...allowVars ? commonEnumProviders.variables('all')(_executor, scope) : [], |
| 259 | 261 | ]; |
| 260 | 262 | }, |
| 261 | 263 | |
| 262 | 264 | /** |
| 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 | + /** | |
| 263 | 279 | * All existing worlds / lorebooks |
| 264 | 280 | * |
| 265 | 281 | * @returns {SlashCommandEnumValue[]} |
| @@ -6,7 +6,7 @@ import { | ||
| 6 | 6 | } from '../lib.js'; |
| 7 | 7 | |
| 8 | 8 | import { getContext } from './extensions.js'; |
| 9 | 9 | import { characters, getRequestHeaders, this_chid, user_avatar } from '../script.js'; |
| 10 | 10 | import { isMobile } from './RossAscends-mods.js'; |
| 11 | 11 | import { collapseNewlines, power_user } from './power-user.js'; |
| 12 | 12 | import { debounce_timeout } from './constants.js'; |
| @@ -14,7 +14,7 @@ import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js'; | ||
| 14 | 14 | import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; |
| 15 | 15 | import { getTagsList } from './tags.js'; |
| 16 | 16 | import { groups, selected_group } from './group-chats.js'; |
| 17 | 17 | import { getCurrentLocale, t } from './i18n.js'; |
| 18 | 18 | |
| 19 | 19 | /** |
| 20 | 20 | * Pagination status string template. |
| @@ -197,6 +197,17 @@ export function onlyUnique(value, index, array) { | ||
| 197 | 197 | } |
| 198 | 198 | |
| 199 | 199 | /** |
| 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 | +/** | |
| 200 | 211 | * Removes the first occurrence of a specified item from an array |
| 201 | 212 | * |
| 202 | 213 | * @param {*[]} array - The array from which to remove the item |
| @@ -1799,8 +1810,9 @@ export function runAfterAnimation(control, callback, timeout = 500) { | ||
| 1799 | 1810 | * |
| 1800 | 1811 | * @param {string} a - The first string to compare. |
| 1801 | 1812 | * @param {string} b - The second string to compare. |
| 1802 | 1813 | * @param {(a:string,b:string)=>booleanT} comparisonFunction - The function to use for the comparison. |
| 1803 | 1814 | * @returns {*T} - The result of the comparison. |
| 1815 | + * @template T | |
| 1804 | 1816 | */ |
| 1805 | 1817 | export function compareIgnoreCaseAndAccents(a, b, comparisonFunction) { |
| 1806 | 1818 | 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) { | ||
| 1838 | 1850 | } |
| 1839 | 1851 | |
| 1840 | 1852 | /** |
| 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 | +/** | |
| 1841 | 1863 | * @typedef {object} Select2Option The option object for select2 controls |
| 1842 | 1864 | * @property {string} id - The unique ID inside this select |
| 1843 | 1865 | * @property {string} text - The text for this option |
| @@ -2197,6 +2219,48 @@ export async function showFontAwesomePicker(customList = null) { | ||
| 2197 | 2219 | } |
| 2198 | 2220 | |
| 2199 | 2221 | /** |
| 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 | +/** | |
| 2200 | 2264 | * Finds a character by name, with optional filtering and precedence for avatars |
| 2201 | 2265 | * @param {object} [options={}] - The options for the search |
| 2202 | 2266 | * @param {string?} [options.name=null] - The name to search for |
| @@ -2228,8 +2292,8 @@ export function findChar({ name = null, allowAvatar = true, insensitive = true, | ||
| 2228 | 2292 | if (preferCurrentChar) { |
| 2229 | 2293 | const preferredCharSearch = currentChars.filter(matches); |
| 2230 | 2294 | if (preferredCharSearch.length > 1) { |
| 2231 | 2295 | if (!quiet) toastr.warning('t`Multiple characters found for given conditions.'`); |
| 2232 | 2296 | else console.warn('t`Multiple characters found for given conditions. Returning the first match.'`); |
| 2233 | 2297 | } |
| 2234 | 2298 | if (preferredCharSearch.length) { |
| 2235 | 2299 | return preferredCharSearch[0]; |