Refactor: move persona slash commands

e0f3a22b80c1d56b8127abf4649f9e5fa255b8f0

Wolfsblvt <wolfsblvt@gmail.com>

2 files changed, +185 -164Showing whitespace changes
public/scripts/personas.js+184 -1
@@ -22,7 +22,7 @@ import {
22} from '../script.js';22} from '../script.js';
23import { persona_description_positions, power_user } from './power-user.js';23import { persona_description_positions, power_user } from './power-user.js';
24import { getTokenCountAsync } from './tokenizers.js';24import { getTokenCountAsync } from './tokenizers.js';
25import { PAGINATION_TEMPLATE, clearInfoBlock, debounce, delay, download, ensureImageFormatSupported, flashHighlight, getBase64Async, getCharIndex, onlyUnique, parseJsonFile, setInfoBlock } from './utils.js';25import { PAGINATION_TEMPLATE, clearInfoBlock, debounce, delay, download, ensureImageFormatSupported, flashHighlight, getBase64Async, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, parseJsonFile, setInfoBlock } from './utils.js';
26import { debounce_timeout } from './constants.js';26import { debounce_timeout } from './constants.js';
27import { FILTER_TYPES, FilterHelper } from './filters.js';27import { FILTER_TYPES, FilterHelper } from './filters.js';
28import { groups, selected_group } from './group-chats.js';28import { groups, selected_group } from './group-chats.js';
@@ -32,6 +32,11 @@ import { openWorldInfoEditor, world_names } from './world-info.js';
32import { renderTemplateAsync } from './templates.js';32import { renderTemplateAsync } from './templates.js';
33import { saveMetadataDebounced } from './extensions.js';33import { saveMetadataDebounced } from './extensions.js';
34import { accountStorage } from './util/AccountStorage.js';34import { accountStorage } from './util/AccountStorage.js';
35import { SlashCommand } from './slash-commands/SlashCommand.js';
36import { SlashCommandNamedArgument, ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
37import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
38import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
39import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
3540
36/**41/**
37 * @typedef {object} PersonaConnection A connection between a character and a character or group entity42 * @typedef {object} PersonaConnection A connection between a character and a character or group entity
@@ -1605,8 +1610,186 @@ async function migrateNonPersonaUser() {
1605 await getUserAvatars(true, user_avatar);1610 await getUserAvatars(true, user_avatar);
1606}1611}
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 */
1620async 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 */
1657function 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
1693function syncCallback() {
1694 $('#sync_name_button').trigger('click');
1695 return '';
1696}
1697
1698function 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
1608export async function initPersonas() {1790export async function initPersonas() {
1609 await migrateNonPersonaUser();1791 await migrateNonPersonaUser();
1792 registerPersonaSlashCommands();
1610 $('#persona_delete_button').on('click', deleteUserAvatar);1793 $('#persona_delete_button').on('click', deleteUserAvatar);
1611 $('#lock_persona_default').on('click', () => togglePersonaLock('default'));1794 $('#lock_persona_default').on('click', () => togglePersonaLock('default'));
1612 $('#lock_user_name').on('click', () => togglePersonaLock('chat'));1795 $('#lock_user_name').on('click', () => togglePersonaLock('chat'));
public/scripts/slash-commands.js+1 -163
@@ -38,7 +38,6 @@ import {
38 setCharacterId,38 setCharacterId,
39 setCharacterName,39 setCharacterName,
40 setExtensionPrompt,40 setExtensionPrompt,
41 setUserName,
42 showMoreMessages,41 showMoreMessages,
43 stopGeneration,42 stopGeneration,
44 substituteParams,43 substituteParams,
@@ -55,7 +54,7 @@ import { getContext, saveMetadataDebounced } from './extensions.js';
55import { getRegexedString, regex_placement } from './extensions/regex/engine.js';54import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
56import { findGroupMemberId, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group } from './group-chats.js';55import { findGroupMemberId, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group } from './group-chats.js';
57import { chat_completion_sources, oai_settings, promptManager } from './openai.js';56import { chat_completion_sources, oai_settings, promptManager } from './openai.js';
58import { autoSelectPersona, isPersonaLocked, retriggerFirstMessageOnEmptyChat, setPersonaLockState, togglePersonaLock, user_avatar } from './personas.js';57import { user_avatar } from './personas.js';
59import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';58import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
60import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';59import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
61import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';60import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';
@@ -75,7 +74,6 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom
75import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';74import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
76import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';75import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
77import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';76import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';
78import { t } from './i18n.js';
79import { accountStorage } from './util/AccountStorage.js';77import { accountStorage } from './util/AccountStorage.js';
80export {78export {
81 executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,79 executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
@@ -125,93 +123,6 @@ export function initDefaultSlashCommands() {
125 helpString: 'Get help on macros, chat formatting and commands.',123 helpString: 'Get help on macros, chat formatting and commands.',
126 }));124 }));
127 SlashCommandParser.addCommandObject(SlashCommand.fromProps({125 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({
215 name: 'bg',126 name: 'bg',
216 callback: setBackgroundCallback,127 callback: setBackgroundCallback,
217 aliases: ['background'],128 aliases: ['background'],
@@ -3487,37 +3398,6 @@ export async function generateSystemMessage(_, prompt) {
3487 return '';3398 return '';
3488}3399}
34893400
3490function syncCallback() {
3491 $('#sync_name_button').trigger('click');
3492 return '';
3493}
3494
3495async 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
3521function setStoryModeCallback() {3401function setStoryModeCallback() {
3522 $('#chat_display').val(chat_styles.DOCUMENT).trigger('change');3402 $('#chat_display').val(chat_styles.DOCUMENT).trigger('change');
3523 return '';3403 return '';
@@ -3533,48 +3413,6 @@ function setFlatModeCallback() {
3533 return '';3413 return '';
3534}3414}
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 */
3542function 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
3578async function setNarratorName(_, text) {3416async function setNarratorName(_, text) {
3579 const name = text || NARRATOR_NAME_DEFAULT;3417 const name = text || NARRATOR_NAME_DEFAULT;
3580 chat_metadata[NARRATOR_NAME_KEY] = name;3418 chat_metadata[NARRATOR_NAME_KEY] = name;