Enhanced `/persona-sync` Command with Range, Name Filter, and Quiet Mode (#5460) * Add range and silent parameters to /persona-sync command with optional confirmation suppression - Add optional start/end range parameters to syncUserNameToPersona() function - Add silent parameter to suppress confirmation popup when needed - Update /persona-sync slash command to accept range argument (index or range string) and named silent argument - Parse range using stringToRange() utility, default to full chat if not provided - Update confirmation message to reflect whether syncing all messages or specified * Add `from` named argument to /persona-sync command for filtering by persona name - Add `from` named argument to filter messages by persona name (case-insensitive) - Rename `silent` argument to `quiet` with inverted default (true) for consistency - Add userMessageNamesEnumProvider() to provide autocomplete for existing user message names in chat - Update syncUserNameToPersona() to accept nameFilter parameter and filter messages accordingly - Update confirmation message to reflect name filtering when * Add async/await wrapper to sync_name_button click handler for proper promise handling Function now has arguments, so using just the function as the event is shown as wrong usage * Post-merge imports fix * Use canonical command name in examples --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -24,7 +24,30 @@ import { | ||
| 24 | 24 | } from '../script.js'; |
| 25 | 25 | import { persona_description_positions, power_user } from './power-user.js'; |
| 26 | 26 | import { getTokenCountAsync } from './tokenizers.js'; |
| 27 | -import { PAGINATION_TEMPLATE, clearInfoBlock, debounce, delay, download, ensureImageFormatSupported, flashHighlight, getBase64Async, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, parseJsonFile, setInfoBlock, localizePagination, renderPaginationDropdown, paginationDropdownChangeHandler, addLongPressEvent, uuidv4 } from './utils.js'; | |
| 27 | +import { | |
| 28 | + PAGINATION_TEMPLATE, | |
| 29 | + clearInfoBlock, | |
| 30 | + debounce, | |
| 31 | + delay, | |
| 32 | + download, | |
| 33 | + ensureImageFormatSupported, | |
| 34 | + flashHighlight, | |
| 35 | + getBase64Async, | |
| 36 | + getCharIndex, | |
| 37 | + isFalseBoolean, | |
| 38 | + isTrueBoolean, | |
| 39 | + onlyUnique, | |
| 40 | + parseJsonFile, | |
| 41 | + setInfoBlock, | |
| 42 | + localizePagination, | |
| 43 | + renderPaginationDropdown, | |
| 44 | + paginationDropdownChangeHandler, | |
| 45 | + addLongPressEvent, | |
| 46 | + stringToRange, | |
| 47 | + sortIgnoreCaseAndAccents, | |
| 48 | + equalsIgnoreCaseAndAccents, | |
| 49 | + uuidv4, | |
| 50 | +} from './utils.js'; | |
| 28 | 51 | import { debounce_timeout } from './constants.js'; |
| 29 | 52 | import { FILTER_TYPES, FilterHelper } from './filters.js'; |
| 30 | 53 | import { groups, selected_group } from './group-chats.js'; |
| @@ -36,8 +59,8 @@ import { saveMetadataDebounced } from './extensions.js'; | ||
| 36 | 59 | import { accountStorage } from './util/AccountStorage.js'; |
| 37 | 60 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| 38 | 61 | import { SlashCommandNamedArgument, ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js'; |
| 39 | 62 | import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 40 | 63 | import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js'; |
| 41 | 64 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 42 | 65 | import { isFirefox } from './browser-fixes.js'; |
| 43 | 66 | |
| @@ -1760,15 +1783,36 @@ async function onPersonasRestoreInput(e) { | ||
| 1760 | 1783 | $('#personas_restore_input').val(''); |
| 1761 | 1784 | } |
| 1762 | 1785 | |
| 1763 | -async function syncUserNameToPersona() { | |
| 1786 | +/** | |
| 1764 | - const confirmation = await Popup.show.confirm(t`Are you sure?`, t`All user-sent messages in this chat will be attributed to ${name1}.`); | |
| 1787 | + * Synchronizes user-sent messages in the chat to the current persona. | |
| 1765 | - | |
| 1788 | + * @param {object} [options={}] - Optional parameters | |
| 1766 | - if (!confirmation) { | |
| 1789 | + * @param {number} [options.start=0] - Start index of the message range (inclusive) | |
| 1767 | - return; | |
| 1790 | + * @param {number} [options.end=chat.length - 1] - End index of the message range (inclusive) | |
| 1791 | + * @param {boolean} [options.quiet=false] - If true, skips the confirmation popup | |
| 1792 | + * @param {string} [options.nameFilter=''] - Filter messages by name (case-insensitive) | |
| 1793 | + * @returns {Promise<void>} | |
| 1794 | + */ | |
| 1795 | +async function syncUserNameToPersona({ start = 0, end = chat.length - 1, quiet = false, nameFilter = '' } = {}) { | |
| 1796 | + const isRangeAll = start === 0 && end === chat.length - 1; | |
| 1797 | + const hasNameFilter = nameFilter?.trim(); | |
| 1798 | + const confirmMessage = isRangeAll && !hasNameFilter | |
| 1799 | + ? t`All user-sent messages in this chat will be attributed to ${name1}.` | |
| 1800 | + : isRangeAll && hasNameFilter | |
| 1801 | + ? t`User-sent messages with name "${nameFilter}" will be attributed to ${name1}.` | |
| 1802 | + : !isRangeAll && !hasNameFilter | |
| 1803 | + ? t`User-sent messages in the specified range will be attributed to ${name1}.` | |
| 1804 | + : t`User-sent messages with name "${nameFilter}" in the specified range will be attributed to ${name1}.`; | |
| 1805 | + | |
| 1806 | + if (!quiet) { | |
| 1807 | + const confirmation = await Popup.show.confirm(t`Are you sure?`, confirmMessage); | |
| 1808 | + if (!confirmation) { | |
| 1809 | + return; | |
| 1810 | + } | |
| 1768 | 1811 | } |
| 1769 | 1812 | |
| 1770 | - for (const mes of chat) { | |
| 1813 | + for (let i = start; i <= end; i++) { | |
| 1771 | - if (mes.is_user) { | |
| 1814 | + const mes = chat[i]; | |
| 1815 | + if (mes?.is_user && (!hasNameFilter || equalsIgnoreCaseAndAccents(mes.name, nameFilter))) { | |
| 1772 | 1816 | mes.name = name1; |
| 1773 | 1817 | mes.force_avatar = getThumbnailUrl('persona', user_avatar); |
| 1774 | 1818 | } |
| @@ -1931,11 +1975,37 @@ async function setNameCallback({ mode = 'all' }, name) { | ||
| 1931 | 1975 | return ''; |
| 1932 | 1976 | } |
| 1933 | 1977 | |
| 1934 | 1978 | async function syncCallback(args, value) { |
| 1935 | - $('#sync_name_button').trigger('click'); | |
| 1979 | + const range = value ? stringToRange(value, 0, chat.length - 1) : null; | |
| 1980 | + | |
| 1981 | + if (value && !range) { | |
| 1982 | + console.warn(`WARN: Invalid range provided for /persona-sync command: ${value}`); | |
| 1983 | + return ''; | |
| 1984 | + } | |
| 1985 | + | |
| 1986 | + const quiet = !isFalseBoolean(args?.quiet); | |
| 1987 | + const nameFilter = typeof args?.from === 'string' ? args.from.trim() : ''; | |
| 1988 | + const start = range ? range.start : 0; | |
| 1989 | + const end = range ? range.end : chat.length - 1; | |
| 1990 | + | |
| 1991 | + await syncUserNameToPersona({ start, end, quiet, nameFilter }); | |
| 1992 | + | |
| 1936 | 1993 | return ''; |
| 1937 | 1994 | } |
| 1938 | 1995 | |
| 1996 | +/** | |
| 1997 | + * Returns all unique user message names in the current chat for enum autocomplete. | |
| 1998 | + * @returns {SlashCommandEnumValue[]} | |
| 1999 | + */ | |
| 2000 | +function userMessageNamesEnumProvider() { | |
| 2001 | + return chat | |
| 2002 | + .filter(mes => mes.is_user) | |
| 2003 | + .map(mes => mes.name) | |
| 2004 | + .filter(onlyUnique) | |
| 2005 | + .sort(sortIgnoreCaseAndAccents) | |
| 2006 | + .map(name => new SlashCommandEnumValue(name, null, enumTypes.name, enumIcons.persona)); | |
| 2007 | +} | |
| 2008 | + | |
| 1939 | 2009 | function registerPersonaSlashCommands() { |
| 1940 | 2010 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 1941 | 2011 | name: 'persona-lock', |
| @@ -1988,7 +2058,50 @@ function registerPersonaSlashCommands() { | ||
| 1988 | 2058 | name: 'persona-sync', |
| 1989 | 2059 | aliases: ['sync'], |
| 1990 | 2060 | callback: syncCallback, |
| 1991 | - helpString: 'Syncs the user persona in user-attributed messages in the current chat.', | |
| 2061 | + namedArgumentList: [ | |
| 2062 | + SlashCommandNamedArgument.fromProps({ | |
| 2063 | + name: 'from', | |
| 2064 | + description: t`only sync messages from a certain persona name`, | |
| 2065 | + typeList: [ARGUMENT_TYPE.STRING], | |
| 2066 | + enumProvider: userMessageNamesEnumProvider, | |
| 2067 | + }), | |
| 2068 | + SlashCommandNamedArgument.fromProps({ | |
| 2069 | + name: 'quiet', | |
| 2070 | + description: t`suppress the confirmation popup`, | |
| 2071 | + typeList: [ARGUMENT_TYPE.BOOLEAN], | |
| 2072 | + enumList: commonEnumProviders.boolean('trueFalse')(), | |
| 2073 | + defaultValue: 'true', | |
| 2074 | + }), | |
| 2075 | + ], | |
| 2076 | + unnamedArgumentList: [ | |
| 2077 | + SlashCommandArgument.fromProps({ | |
| 2078 | + description: t`message index (starts with 0) or range, syncs all user messages if not provided`, | |
| 2079 | + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE], | |
| 2080 | + defaultValue: '0-{{lastMessageId}}', | |
| 2081 | + }), | |
| 2082 | + ], | |
| 2083 | + helpString: ` | |
| 2084 | + <div> | |
| 2085 | + ${t`Syncs the user persona (name and avatar) in user-attributed messages in the current chat.`} | |
| 2086 | + </div> | |
| 2087 | + <div> | |
| 2088 | + ${t`If <code>from</code> is set, only messages with that specific persona name will be synced. Useful when multiple personas have been used in the same chat.`} | |
| 2089 | + </div> | |
| 2090 | + <div> | |
| 2091 | + ${t`If <code>quiet</code> is set to <code>false</code>, a confirmation popup will be shown before syncing.`} | |
| 2092 | + </div> | |
| 2093 | + <div> | |
| 2094 | + <strong>${t`Examples:`}</strong> | |
| 2095 | + <ul> | |
| 2096 | + <li><pre><code>/persona-sync</code></pre> ${t`- Sync all user messages`}</li> | |
| 2097 | + <li><pre><code>/persona-sync 5</code></pre> ${t`- Sync only message 5`}</li> | |
| 2098 | + <li><pre><code>/persona-sync 0-10</code></pre> ${t`- Sync messages 0 through 10`}</li> | |
| 2099 | + <li><pre><code>/persona-sync from=OldPersona 0-20</code></pre> ${t`- Sync only messages with name "OldPersona" in range 0-20`}</li> | |
| 2100 | + <li><pre><code>/persona-sync quiet=false</code></pre> ${t`- Sync all with confirmation popup`}</li> | |
| 2101 | + <li><pre><code>/persona-sync from=TempName quiet=false 5-15</code></pre> ${t`- Sync messages with name "TempName" in range 5-15 with confirmation`}</li> | |
| 2102 | + </ul> | |
| 2103 | + </div> | |
| 2104 | + `, | |
| 1992 | 2105 | })); |
| 1993 | 2106 | } |
| 1994 | 2107 | |
| @@ -2046,7 +2159,7 @@ export async function initPersonas() { | ||
| 2046 | 2159 | debouncedPersonaSearch(searchQuery); |
| 2047 | 2160 | }); |
| 2048 | 2161 | |
| 2049 | 2162 | $('#sync_name_button').on('click', async () => await syncUserNameToPersona()); |
| 2050 | 2163 | $('#avatar_upload_file').on('change', changeUserAvatar); |
| 2051 | 2164 | |
| 2052 | 2165 | $(document).on('click', '#user_avatar_block .avatar-container', async function () { |