Refactor: move persona slash commands

e0f3a22b80c1d56b8127abf4649f9e5fa255b8f0

Wolfsblvt <wolfsblvt@gmail.com>

2 files changed, +185 -164Ignore whitespace
public/scripts/personas.js+184 -1
@@ -22,7 +22,7 @@ import {
2222} from '../script.js';
2323import { persona_description_positions, power_user } from './power-user.js';
2424import { getTokenCountAsync } from './tokenizers.js';
2525import { PAGINATION_TEMPLATE, clearInfoBlock, debounce, delay, download, ensureImageFormatSupported, flashHighlight, getBase64Async, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, parseJsonFile, setInfoBlock } from './utils.js';
2626import { debounce_timeout } from './constants.js';
2727import { FILTER_TYPES, FilterHelper } from './filters.js';
2828import { groups, selected_group } from './group-chats.js';
@@ -32,6 +32,11 @@ import { openWorldInfoEditor, world_names } from './world-info.js';
3232import { renderTemplateAsync } from './templates.js';
3333import { saveMetadataDebounced } from './extensions.js';
3434import { accountStorage } from './util/AccountStorage.js';
35+import { SlashCommand } from './slash-commands/SlashCommand.js';
36+import { SlashCommandNamedArgument, ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
37+import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
38+import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
39+import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
3540
3641/**
3742 * @typedef {object} PersonaConnection A connection between a character and a character or group entity
@@ -1605,8 +1610,186 @@ async function migrateNonPersonaUser() {
16051610 await getUserAvatars(true, user_avatar);
16061611}
16071612
1613+
1614+/**
1615+ * Locks or unlocks the persona of the current chat.
1616+ * @param {{type: string}} _args Named arguments
1617+ * @param {string} value The value to set the lock to
1618+ * @returns {Promise<string>} The value of the lock after setting
1619+ */
1620+async function lockPersonaCallback(_args, value) {
1621+ const type = /** @type {PersonaLockType} */ (_args.type ?? 'chat');
1622+
1623+ if (!['chat', 'character', 'default'].includes(type)) {
1624+ toastr.warning(t`Unknown lock type "${type}"`, t`Persona Management`);
1625+ return '';
1626+ }
1627+
1628+ if (!value) {
1629+ return String(isPersonaLocked(type));
1630+ }
1631+
1632+ if (['toggle', 't'].includes(value.trim().toLowerCase())) {
1633+ const result = await togglePersonaLock(type);
1634+ return String(result);
1635+ }
1636+
1637+ if (isTrueBoolean(value)) {
1638+ await setPersonaLockState(true, type);
1639+ return 'true';
1640+ }
1641+
1642+ if (isFalseBoolean(value)) {
1643+ await setPersonaLockState(false, type);
1644+ return 'false';
1645+
1646+ }
1647+
1648+ return '';
1649+}
1650+
1651+/**
1652+ * Sets a persona name and optionally an avatar.
1653+ * @param {{mode: 'lookup' | 'temp' | 'all'}} namedArgs Named arguments
1654+ * @param {string} name Name to set
1655+ * @returns {string}
1656+ */
1657+function setNameCallback({ mode = 'all' }, name) {
1658+ if (!name) {
1659+ toastr.warning('You must specify a name to change to');
1660+ return '';
1661+ }
1662+
1663+ if (!['lookup', 'temp', 'all'].includes(mode)) {
1664+ toastr.warning('Mode must be one of "lookup", "temp" or "all"');
1665+ return '';
1666+ }
1667+
1668+ name = name.trim();
1669+
1670+ // If the name matches a persona avatar, or a name, auto-select it
1671+ if (['lookup', 'all'].includes(mode)) {
1672+ let persona = Object.entries(power_user.personas).find(([avatar, _]) => avatar === name)?.[1];
1673+ if (!persona) persona = Object.entries(power_user.personas).find(([_, personaName]) => personaName.toLowerCase() === name.toLowerCase())?.[1];
1674+ if (persona) {
1675+ autoSelectPersona(persona);
1676+ retriggerFirstMessageOnEmptyChat();
1677+ return '';
1678+ } else if (mode === 'lookup') {
1679+ toastr.warning(`Persona ${name} not found`);
1680+ return '';
1681+ }
1682+ }
1683+
1684+ if (['temp', 'all'].includes(mode)) {
1685+ // Otherwise, set just the name
1686+ setUserName(name); //this prevented quickReply usage
1687+ retriggerFirstMessageOnEmptyChat();
1688+ }
1689+
1690+ return '';
1691+}
1692+
1693+function syncCallback() {
1694+ $('#sync_name_button').trigger('click');
1695+ return '';
1696+}
1697+
1698+function registerPersonaSlashCommands() {
1699+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1700+ name: 'persona-lock',
1701+ callback: lockPersonaCallback,
1702+ returns: 'The current lock state for the given type',
1703+ helpString: 'Locks/unlocks a persona (name and avatar) to the current chat. Gets the current lock state for the given type if no state is provided.',
1704+ namedArgumentList: [
1705+ SlashCommandNamedArgument.fromProps({
1706+ name: 'type',
1707+ description: 'The type of the lock, where it should apply to',
1708+ typeList: [ARGUMENT_TYPE.STRING],
1709+ defaultValue: 'chat',
1710+ enumList: [
1711+ new SlashCommandEnumValue('chat', 'Lock the persona to the current chat.'),
1712+ new SlashCommandEnumValue('character', 'Lock this persona to the currently selected character. If the setting is enabled, mutliple personas can be locked to the same character.'),
1713+ new SlashCommandEnumValue('default', 'Lock this persona as the default persona for all new chats.'),
1714+ ],
1715+ }),
1716+ ],
1717+ unnamedArgumentList: [
1718+ SlashCommandArgument.fromProps({
1719+ description: 'state',
1720+ typeList: [ARGUMENT_TYPE.STRING],
1721+ enumProvider: commonEnumProviders.boolean('onOffToggle'),
1722+ }),
1723+ ],
1724+ }));
1725+ // TODO: Legacy command. Might be removed in the future and replaced by /persona-lock with aliases.
1726+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1727+ name: 'lock',
1728+ /** @type {(args: { type: string }, value: string) => Promise<string>} */
1729+ callback: (args, value) => {
1730+ if (!value) {
1731+ value = 'toggle';
1732+ toastr.warning(t`Using /lock without a provided state to toggle the persona is deprecated. Please use /persona-lock instead.
1733+ In the future this command with no state provided will return the current state, instead of toggling it.`, t`Deprecation Warning`);
1734+ }
1735+ return lockPersonaCallback(args, value);
1736+ },
1737+ returns: 'The current lock state for the given type',
1738+ aliases: ['bind'],
1739+ helpString: 'Locks/unlocks a persona (name and avatar) to the current chat. Gets the current lock state for the given type if no state is provided.',
1740+ namedArgumentList: [
1741+ SlashCommandNamedArgument.fromProps({
1742+ name: 'type',
1743+ description: 'The type of the lock, where it should apply to',
1744+ typeList: [ARGUMENT_TYPE.STRING],
1745+ defaultValue: 'chat',
1746+ enumList: [
1747+ new SlashCommandEnumValue('chat', 'Lock the persona to the current chat.'),
1748+ new SlashCommandEnumValue('character', 'Lock this persona to the currently selected character. If the setting is enabled, mutliple personas can be locked to the same character.'),
1749+ new SlashCommandEnumValue('default', 'Lock this persona as the default persona for all new chats.'),
1750+ ],
1751+ }),
1752+ ],
1753+ unnamedArgumentList: [
1754+ SlashCommandArgument.fromProps({
1755+ description: 'state',
1756+ typeList: [ARGUMENT_TYPE.STRING],
1757+ defaultValue: 'toggle',
1758+ enumProvider: commonEnumProviders.boolean('onOffToggle'),
1759+ }),
1760+ ],
1761+ }));
1762+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1763+ name: 'persona-set',
1764+ callback: setNameCallback,
1765+ aliases: ['persona', 'name'],
1766+ namedArgumentList: [
1767+ new SlashCommandNamedArgument(
1768+ 'mode', 'The mode for persona selection. ("lookup" = search for existing persona, "temp" = create a temporary name, set a temporary name, "all" = allow both in the same command)',
1769+ [ARGUMENT_TYPE.STRING], false, false, 'all', ['lookup', 'temp', 'all'],
1770+ ),
1771+ ],
1772+ unnamedArgumentList: [
1773+ SlashCommandArgument.fromProps({
1774+ description: 'persona name',
1775+ typeList: [ARGUMENT_TYPE.STRING],
1776+ isRequired: true,
1777+ enumProvider: commonEnumProviders.personas,
1778+ }),
1779+ ],
1780+ helpString: 'Selects the given persona with its name and avatar (by name or avatar url). If no matching persona exists, applies a temporary name.',
1781+ }));
1782+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1783+ name: 'persona-sync',
1784+ aliases: ['sync'],
1785+ callback: syncCallback,
1786+ helpString: 'Syncs the user persona in user-attributed messages in the current chat.',
1787+ }));
1788+}
1789+
16081790export async function initPersonas() {
16091791 await migrateNonPersonaUser();
1792+ registerPersonaSlashCommands();
16101793 $('#persona_delete_button').on('click', deleteUserAvatar);
16111794 $('#lock_persona_default').on('click', () => togglePersonaLock('default'));
16121795 $('#lock_user_name').on('click', () => togglePersonaLock('chat'));
public/scripts/slash-commands.js+1 -163
@@ -38,7 +38,6 @@ import {
3838 setCharacterId,
3939 setCharacterName,
4040 setExtensionPrompt,
41- setUserName,
4241 showMoreMessages,
4342 stopGeneration,
4443 substituteParams,
@@ -55,7 +54,7 @@ import { getContext, saveMetadataDebounced } from './extensions.js';
5554import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
5655import { findGroupMemberId, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group } from './group-chats.js';
5756import { chat_completion_sources, oai_settings, promptManager } from './openai.js';
5857import { autoSelectPersona, isPersonaLocked, retriggerFirstMessageOnEmptyChat, setPersonaLockState, togglePersonaLock, user_avatar } from './personas.js';
5958import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
6059import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
6160import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';
@@ -75,7 +74,6 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom
7574import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
7675import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
7776import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';
78-import { t } from './i18n.js';
7977import { accountStorage } from './util/AccountStorage.js';
8078export {
8179 executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
@@ -125,93 +123,6 @@ export function initDefaultSlashCommands() {
125123 helpString: 'Get help on macros, chat formatting and commands.',
126124 }));
127125 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
128- name: 'persona',
129- callback: setNameCallback,
130- aliases: ['name'],
131- namedArgumentList: [
132- new SlashCommandNamedArgument(
133- 'mode', 'The mode for persona selection. ("lookup" = search for existing persona, "temp" = create a temporary name, set a temporary name, "all" = allow both in the same command)',
134- [ARGUMENT_TYPE.STRING], false, false, 'all', ['lookup', 'temp', 'all'],
135- ),
136- ],
137- unnamedArgumentList: [
138- SlashCommandArgument.fromProps({
139- description: 'persona name',
140- typeList: [ARGUMENT_TYPE.STRING],
141- isRequired: true,
142- enumProvider: commonEnumProviders.personas,
143- }),
144- ],
145- helpString: 'Selects the given persona with its name and avatar (by name or avatar url). If no matching persona exists, applies a temporary name.',
146- }));
147- SlashCommandParser.addCommandObject(SlashCommand.fromProps({
148- name: 'sync',
149- callback: syncCallback,
150- helpString: 'Syncs the user persona in user-attributed messages in the current chat.',
151- }));
152- SlashCommandParser.addCommandObject(SlashCommand.fromProps({
153- name: 'persona-lock',
154- callback: lockPersonaCallback,
155- returns: 'The current lock state for the given type',
156- helpString: 'Locks/unlocks a persona (name and avatar) to the current chat. Gets the current lock state for the given type if no state is provided.',
157- namedArgumentList: [
158- SlashCommandNamedArgument.fromProps({
159- name: 'type',
160- description: 'The type of the lock, where it should apply to',
161- typeList: [ARGUMENT_TYPE.STRING],
162- defaultValue: 'chat',
163- enumList: [
164- new SlashCommandEnumValue('chat', 'Lock the persona to the current chat.'),
165- new SlashCommandEnumValue('character', 'Lock this persona to the currently selected character. If the setting is enabled, mutliple personas can be locked to the same character.'),
166- new SlashCommandEnumValue('default', 'Lock this persona as the default persona for all new chats.'),
167- ],
168- }),
169- ],
170- unnamedArgumentList: [
171- SlashCommandArgument.fromProps({
172- description: 'state',
173- typeList: [ARGUMENT_TYPE.STRING],
174- enumProvider: commonEnumProviders.boolean('onOffToggle'),
175- }),
176- ],
177- }));
178- // TODO: Legacy command. Might be removed in the future and replaced by /persona-lock with aliases.
179- SlashCommandParser.addCommandObject(SlashCommand.fromProps({
180- name: 'lock',
181- callback: (args, value) => {
182- if (!value) {
183- value = 'toggle';
184- toastr.warning(t`Using /lock without a provided state to toggle the persona is deprecated. Please use /persona-lock instead.
185- In the future this command with no state provided will return the current state, instead of toggling it.`, t`Deprecation Warning`);
186- }
187- return lockPersonaCallback(args, value);
188- },
189- returns: 'The current lock state for the given type',
190- aliases: ['bind'],
191- helpString: 'Locks/unlocks a persona (name and avatar) to the current chat. Gets the current lock state for the given type if no state is provided.',
192- namedArgumentList: [
193- SlashCommandNamedArgument.fromProps({
194- name: 'type',
195- description: 'The type of the lock, where it should apply to',
196- typeList: [ARGUMENT_TYPE.STRING],
197- defaultValue: 'chat',
198- enumList: [
199- new SlashCommandEnumValue('chat', 'Lock the persona to the current chat.'),
200- new SlashCommandEnumValue('character', 'Lock this persona to the currently selected character. If the setting is enabled, mutliple personas can be locked to the same character.'),
201- new SlashCommandEnumValue('default', 'Lock this persona as the default persona for all new chats.'),
202- ],
203- }),
204- ],
205- unnamedArgumentList: [
206- SlashCommandArgument.fromProps({
207- description: 'state',
208- typeList: [ARGUMENT_TYPE.STRING],
209- defaultValue: 'toggle',
210- enumProvider: commonEnumProviders.boolean('onOffToggle'),
211- }),
212- ],
213- }));
214- SlashCommandParser.addCommandObject(SlashCommand.fromProps({
215126 name: 'bg',
216127 callback: setBackgroundCallback,
217128 aliases: ['background'],
@@ -3487,37 +3398,6 @@ export async function generateSystemMessage(_, prompt) {
34873398 return '';
34883399}
34893400
3490-function syncCallback() {
3491- $('#sync_name_button').trigger('click');
3492- return '';
3493-}
3494-
3495-async function lockPersonaCallback(_args, value) {
3496- const type = _args.type ?? 'chat';
3497-
3498- if (!value) {
3499- return String(isPersonaLocked(type));
3500- }
3501-
3502- if (['toggle', 't'].includes(value.trim().toLowerCase())) {
3503- const result = await togglePersonaLock(type);
3504- return String(result);
3505- }
3506-
3507- if (isTrueBoolean(value)) {
3508- await setPersonaLockState(true, type);
3509- return 'true';
3510- }
3511-
3512- if (isFalseBoolean(value)) {
3513- await setPersonaLockState(false, type);
3514- return 'false';
3515-
3516- }
3517-
3518- return '';
3519-}
3520-
35213401function setStoryModeCallback() {
35223402 $('#chat_display').val(chat_styles.DOCUMENT).trigger('change');
35233403 return '';
@@ -3533,48 +3413,6 @@ function setFlatModeCallback() {
35333413 return '';
35343414}
35353415
3536-/**
3537- * Sets a persona name and optionally an avatar.
3538- * @param {{mode: 'lookup' | 'temp' | 'all'}} namedArgs Named arguments
3539- * @param {string} name Name to set
3540- * @returns {string}
3541- */
3542-function setNameCallback({ mode = 'all' }, name) {
3543- if (!name) {
3544- toastr.warning('You must specify a name to change to');
3545- return '';
3546- }
3547-
3548- if (!['lookup', 'temp', 'all'].includes(mode)) {
3549- toastr.warning('Mode must be one of "lookup", "temp" or "all"');
3550- return '';
3551- }
3552-
3553- name = name.trim();
3554-
3555- // If the name matches a persona avatar, or a name, auto-select it
3556- if (['lookup', 'all'].includes(mode)) {
3557- let persona = Object.entries(power_user.personas).find(([avatar, _]) => avatar === name)?.[1];
3558- if (!persona) persona = Object.entries(power_user.personas).find(([_, personaName]) => personaName.toLowerCase() === name.toLowerCase())?.[1];
3559- if (persona) {
3560- autoSelectPersona(persona);
3561- retriggerFirstMessageOnEmptyChat();
3562- return '';
3563- } else if (mode === 'lookup') {
3564- toastr.warning(`Persona ${name} not found`);
3565- return '';
3566- }
3567- }
3568-
3569- if (['temp', 'all'].includes(mode)) {
3570- // Otherwise, set just the name
3571- setUserName(name); //this prevented quickReply usage
3572- retriggerFirstMessageOnEmptyChat();
3573- }
3574-
3575- return '';
3576-}
3577-
35783416async function setNarratorName(_, text) {
35793417 const name = text || NARRATOR_NAME_DEFAULT;
35803418 chat_metadata[NARRATOR_NAME_KEY] = name;