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>

0ac31c8fcdc91818ff6f5238ee41ac1ce3f7b385

Wolfsblvt <wolfsblvt@gmail.com>

Signed
1 files changed, +127 -14Ignore whitespace
public/scripts/personas.js+127 -14
@@ -24,7 +24,30 @@ import {
24} from '../script.js';24} from '../script.js';
25import { persona_description_positions, power_user } from './power-user.js';25import { persona_description_positions, power_user } from './power-user.js';
26import { getTokenCountAsync } from './tokenizers.js';26import { getTokenCountAsync } from './tokenizers.js';
27import { PAGINATION_TEMPLATE, clearInfoBlock, debounce, delay, download, ensureImageFormatSupported, flashHighlight, getBase64Async, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, parseJsonFile, setInfoBlock, localizePagination, renderPaginationDropdown, paginationDropdownChangeHandler, addLongPressEvent, uuidv4 } from './utils.js';27import {
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';
28import { debounce_timeout } from './constants.js';51import { debounce_timeout } from './constants.js';
29import { FILTER_TYPES, FilterHelper } from './filters.js';52import { FILTER_TYPES, FilterHelper } from './filters.js';
30import { groups, selected_group } from './group-chats.js';53import { groups, selected_group } from './group-chats.js';
@@ -36,8 +59,8 @@ import { saveMetadataDebounced } from './extensions.js';
36import { accountStorage } from './util/AccountStorage.js';59import { accountStorage } from './util/AccountStorage.js';
37import { SlashCommand } from './slash-commands/SlashCommand.js';60import { SlashCommand } from './slash-commands/SlashCommand.js';
38import { SlashCommandNamedArgument, ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';61import { SlashCommandNamedArgument, ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
39import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';62import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
40import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';63import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
41import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';64import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
42import { isFirefox } from './browser-fixes.js';65import { isFirefox } from './browser-fixes.js';
4366
@@ -1760,15 +1783,36 @@ async function onPersonasRestoreInput(e) {
1760 $('#personas_restore_input').val('');1783 $('#personas_restore_input').val('');
1761}1784}
17621785
1763async 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.
17651788 * @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 */
1795async 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 }
17691812
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 mes.name = name1;1816 mes.name = name1;
1773 mes.force_avatar = getThumbnailUrl('persona', user_avatar);1817 mes.force_avatar = getThumbnailUrl('persona', user_avatar);
1774 }1818 }
@@ -1931,11 +1975,37 @@ async function setNameCallback({ mode = 'all' }, name) {
1931 return '';1975 return '';
1932}1976}
19331977
1934function syncCallback() {1978async 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 return '';1993 return '';
1937}1994}
19381995
1996/**
1997 * Returns all unique user message names in the current chat for enum autocomplete.
1998 * @returns {SlashCommandEnumValue[]}
1999 */
2000function 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
1939function registerPersonaSlashCommands() {2009function registerPersonaSlashCommands() {
1940 SlashCommandParser.addCommandObject(SlashCommand.fromProps({2010 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1941 name: 'persona-lock',2011 name: 'persona-lock',
@@ -1988,7 +2058,50 @@ function registerPersonaSlashCommands() {
1988 name: 'persona-sync',2058 name: 'persona-sync',
1989 aliases: ['sync'],2059 aliases: ['sync'],
1990 callback: syncCallback,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}
19942107
@@ -2046,7 +2159,7 @@ export async function initPersonas() {
2046 debouncedPersonaSearch(searchQuery);2159 debouncedPersonaSearch(searchQuery);
2047 });2160 });
20482161
2049 $('#sync_name_button').on('click', syncUserNameToPersona);2162 $('#sync_name_button').on('click', async () => await syncUserNameToPersona());
2050 $('#avatar_upload_file').on('change', changeUserAvatar);2163 $('#avatar_upload_file').on('change', changeUserAvatar);
20512164
2052 $(document).on('click', '#user_avatar_block .avatar-container', async function () {2165 $(document).on('click', '#user_avatar_block .avatar-container', async function () {