diff --git a/public/css/popup.css b/public/css/popup.css index 62c29fcf9..bdd530a66 100644 --- a/public/css/popup.css +++ b/public/css/popup.css @@ -109,7 +109,6 @@ dialog { .menu_button.popup-button-ok { background-color: var(--crimson70a); - cursor: pointer; } .menu_button.popup-button-ok:hover { @@ -132,3 +131,17 @@ dialog { filter: brightness(1.3) saturate(1.3); } +.popup .popup-button-close { + position: absolute; + top: -6px; + right: -6px; + width: 24px; + height: 24px; + font-size: 20px; + padding: 2px 3px 3px 2px; + + filter: brightness(0.8); + + /* Fix weird animation issue with font-scaling during popup open */ + backface-visibility: hidden; +} diff --git a/public/index.html b/public/index.html index 53bbb548f..5c847998f 100644 --- a/public/index.html +++ b/public/index.html @@ -4855,10 +4855,11 @@ +
diff --git a/public/jsconfig.json b/public/jsconfig.json index aad133bc6..f362b9731 100644 --- a/public/jsconfig.json +++ b/public/jsconfig.json @@ -3,6 +3,7 @@ "checkJs": true, "target": "ESNext", "module": "ESNext", + "moduleResolution": "node", "allowUmdGlobalAccess": true, "allowSyntheticDefaultImports": true }, diff --git a/public/script.js b/public/script.js index 29df8a38e..62c8eb23a 100644 --- a/public/script.js +++ b/public/script.js @@ -239,6 +239,8 @@ import { initCustomSelectedSamplers, validateDisabledSamplers } from './scripts/ import { DragAndDropHandler } from './scripts/dragdrop.js'; import { INTERACTABLE_CONTROL_CLASS, initKeyboard } from './scripts/keyboard.js'; import { initDynamicStyles } from './scripts/dynamic-styles.js'; +import { SlashCommandEnumValue, enumTypes } from './scripts/slash-commands/SlashCommandEnumValue.js'; +import { enumIcons } from './scripts/slash-commands/SlashCommandCommonEnumsProvider.js'; //exporting functions and vars for mods export { @@ -2122,7 +2124,7 @@ export function addCopyToCodeBlocks(messageElement) { hljs.highlightElement(codeBlocks.get(i)); if (navigator.clipboard !== undefined) { const copyButton = document.createElement('i'); - copyButton.classList.add('fa-solid', 'fa-copy', 'code-copy'); + copyButton.classList.add('fa-solid', 'fa-copy', 'code-copy', 'interactable'); copyButton.title = 'Copy code'; codeBlocks.get(i).appendChild(copyButton); copyButton.addEventListener('pointerup', function (event) { @@ -2376,6 +2378,10 @@ export function substituteParamsExtended(content, additionalMacro = {}) { * @returns {string} The string with substituted parameters. */ export function substituteParams(content, _name1, _name2, _original, _group, _replaceCharacterCard = true, additionalMacro = {}) { + if (!content) { + return ''; + } + const environment = {}; if (typeof _original === 'string') { @@ -8817,6 +8823,9 @@ jQuery(async function () { return ''; } + // Collect all unique API names in an array + const uniqueAPIs = [...new Set(Object.values(CONNECT_API_MAP).map(x => x.selected))]; + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'dupe', callback: DupeChar, @@ -8825,16 +8834,15 @@ jQuery(async function () { SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'api', callback: connectAPISlash, - namedArgumentList: [], unnamedArgumentList: [ - new SlashCommandArgument( - 'API to connect to', - [ARGUMENT_TYPE.STRING], - true, - false, - null, - Object.keys(CONNECT_API_MAP), - ), + SlashCommandArgument.fromProps({ + description: 'API to connect to', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: false, + enumList: Object.entries(CONNECT_API_MAP).map(([api, { selected }]) => + new SlashCommandEnumValue(api, selected, enumTypes.getBasedOnIndex(uniqueAPIs.findIndex(x => x === selected)), + selected[0].toUpperCase() ?? enumIcons.default)), + }), ], helpString: `
@@ -8923,11 +8931,12 @@ jQuery(async function () { name: 'instruct', callback: selectInstructCallback, returns: 'current preset', - namedArgumentList: [], unnamedArgumentList: [ - new SlashCommandArgument( - 'name', [ARGUMENT_TYPE.STRING], false, - ), + SlashCommandArgument.fromProps({ + description: 'instruct preset name', + typeList: [ARGUMENT_TYPE.STRING], + enumProvider: () => instruct_presets.map(preset => new SlashCommandEnumValue(preset.name, null, enumTypes.enum, enumIcons.preset)), + }), ], helpString: `
@@ -8958,9 +8967,11 @@ jQuery(async function () { callback: selectContextCallback, returns: 'template name', unnamedArgumentList: [ - new SlashCommandArgument( - 'name', [ARGUMENT_TYPE.STRING], false, - ), + SlashCommandArgument.fromProps({ + description: 'context preset name', + typeList: [ARGUMENT_TYPE.STRING], + enumProvider: () => context_presets.map(preset => new SlashCommandEnumValue(preset.name, null, enumTypes.enum, enumIcons.preset)), + }), ], helpString: 'Selects context template by name. Gets the current template if no name is provided', })); diff --git a/public/scripts/authors-note.js b/public/scripts/authors-note.js index f9643a8d6..31a5719e1 100644 --- a/public/scripts/authors-note.js +++ b/public/scripts/authors-note.js @@ -38,6 +38,7 @@ const chara_note_position = { function setNoteTextCommand(_, text) { $('#extension_floating_prompt').val(text).trigger('input'); toastr.success('Author\'s Note text updated'); + return ''; } function setNoteDepthCommand(_, text) { @@ -50,6 +51,7 @@ function setNoteDepthCommand(_, text) { $('#extension_floating_depth').val(Math.abs(value)).trigger('input'); toastr.success('Author\'s Note depth updated'); + return ''; } function setNoteIntervalCommand(_, text) { @@ -62,6 +64,7 @@ function setNoteIntervalCommand(_, text) { $('#extension_floating_interval').val(Math.abs(value)).trigger('input'); toastr.success('Author\'s Note frequency updated'); + return ''; } function setNotePositionCommand(_, text) { @@ -79,6 +82,7 @@ function setNotePositionCommand(_, text) { $(`input[name="extension_floating_position"][value="${position}"]`).prop('checked', true).trigger('input'); toastr.info('Author\'s Note position updated'); + return ''; } function updateSettings() { diff --git a/public/scripts/autocomplete/AutoComplete.js b/public/scripts/autocomplete/AutoComplete.js index a7f8ceb0c..df3bcf75c 100644 --- a/public/scripts/autocomplete/AutoComplete.js +++ b/public/scripts/autocomplete/AutoComplete.js @@ -750,8 +750,10 @@ export class AutoComplete { } // autocomplete shown or not, cursor anywhere switch (evt.key) { + // The first is a non-breaking space, the second is a regular space. + case ' ': case ' ': { - if (evt.ctrlKey) { + if (evt.ctrlKey || evt.altKey) { if (this.isActive && this.isReplaceable) { // ctrl-space to toggle details for selected item this.toggleDetails(); @@ -759,6 +761,8 @@ export class AutoComplete { // ctrl-space to force show autocomplete this.show(false, true); } + evt.preventDefault(); + evt.stopPropagation(); return; } break; diff --git a/public/scripts/autocomplete/AutoCompleteOption.js b/public/scripts/autocomplete/AutoCompleteOption.js index 7a12d74b2..abfcf3ff6 100644 --- a/public/scripts/autocomplete/AutoCompleteOption.js +++ b/public/scripts/autocomplete/AutoCompleteOption.js @@ -143,6 +143,11 @@ export class AutoCompleteOption { } li.append(specs); } + const stopgap = document.createElement('span'); { + stopgap.classList.add('stopgap'); + stopgap.textContent = ''; + li.append(stopgap); + } const help = document.createElement('span'); { help.classList.add('help'); const content = document.createElement('span'); { diff --git a/public/scripts/backgrounds.js b/public/scripts/backgrounds.js index 8a3979c45..fcd603789 100644 --- a/public/scripts/backgrounds.js +++ b/public/scripts/backgrounds.js @@ -95,7 +95,7 @@ function onLockBackgroundClick(e) { if (!chatName) { toastr.warning('Select a chat to lock the background for it'); - return; + return ''; } const relativeBgImage = getUrlParameter(this); @@ -103,6 +103,7 @@ function onLockBackgroundClick(e) { saveBackgroundMetadata(relativeBgImage); setCustomBackground(); highlightLockedBackground(); + return ''; } function onUnlockBackgroundClick(e) { @@ -110,6 +111,7 @@ function onUnlockBackgroundClick(e) { removeBackgroundMetadata(); unsetCustomBackground(); highlightLockedBackground(); + return ''; } function hasCustomBackground() { @@ -319,7 +321,7 @@ async function autoBackgroundCommand() { const options = bgTitles.map(x => ({ element: x, text: x.innerText.trim() })).filter(x => x.text.length > 0); if (options.length == 0) { toastr.warning('No backgrounds to choose from. Please upload some images to the "backgrounds" folder.'); - return; + return ''; } const list = options.map(option => `- ${option.text}`).join('\n'); @@ -330,11 +332,12 @@ async function autoBackgroundCommand() { if (bestMatch.length == 0) { toastr.warning('No match found. Please try again.'); - return; + return ''; } console.debug('Automatically choosing background:', bestMatch); bestMatch[0].item.element.click(); + return ''; } export async function getBackgrounds() { diff --git a/public/scripts/chats.js b/public/scripts/chats.js index 32a188c20..83069aaab 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -35,7 +35,7 @@ import { extractTextFromOffice, } from './utils.js'; import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js'; -import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js'; +import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; import { ScraperManager } from './scrapers.js'; import { DragAndDropHandler } from './dragdrop.js'; @@ -566,7 +566,7 @@ export function isExternalMediaAllowed() { return !power_user.forbid_external_media; } -function enlargeMessageImage() { +async function enlargeMessageImage() { const mesBlock = $(this).closest('.mes'); const mesId = mesBlock.attr('mesid'); const message = chat[mesId]; @@ -580,14 +580,28 @@ function enlargeMessageImage() { const img = document.createElement('img'); img.classList.add('img_enlarged'); img.src = imgSrc; + const imgHolder = document.createElement('div'); + imgHolder.classList.add('img_enlarged_holder'); + imgHolder.append(img); const imgContainer = $('
'); - imgContainer.prepend(img); + imgContainer.prepend(imgHolder); imgContainer.addClass('img_enlarged_container'); imgContainer.find('code').addClass('txt').text(title); const titleEmpty = !title || title.trim().length === 0; imgContainer.find('pre').toggle(!titleEmpty); addCopyToCodeBlocks(imgContainer); - callGenericPopup(imgContainer, POPUP_TYPE.TEXT, '', { wide: true, large: true }); + + const popup = new Popup(imgContainer, POPUP_TYPE.DISPLAY, '', { large: true, transparent: true }); + + popup.dlg.style.width = 'unset'; + popup.dlg.style.height = 'unset'; + + img.addEventListener('click', () => { + const shouldZoom = !img.classList.contains('zoomed'); + img.classList.toggle('zoomed', shouldZoom); + }); + + await popup.show(); } async function deleteMessageImage() { diff --git a/public/scripts/extensions/attachments/index.js b/public/scripts/extensions/attachments/index.js index d43d1d0a7..ca7ef7ba6 100644 --- a/public/scripts/extensions/attachments/index.js +++ b/public/scripts/extensions/attachments/index.js @@ -2,6 +2,10 @@ import { deleteAttachment, getDataBankAttachments, getDataBankAttachmentsForSour import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js'; import { SlashCommand } from '../../slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; +import { SlashCommandClosure } from '../../slash-commands/SlashCommandClosure.js'; +import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; +import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js'; +import { SlashCommandExecutor } from '../../slash-commands/SlashCommandExecutor.js'; import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; /** @@ -196,9 +200,31 @@ jQuery(async () => { const buttons = await renderExtensionTemplateAsync('attachments', 'buttons', {}); $('#extensionsMenu').prepend(buttons); + /** A collection of local enum providers for this context of data bank */ + const localEnumProviders = { + /** + * All attachments in the data bank based on the source argument. If not provided, defaults to 'chat'. + * @param {'name' | 'url'} returnField - Whether the enum should return the 'name' field or the 'url' + * @param {'chat' | 'character' | 'global' | ''} fallbackSource - The source to use if the source argument is not provided. Empty string to use all sources. + * */ + attachments: (returnField = 'name', fallbackSource = 'chat') => (/** @type {SlashCommandExecutor} */ executor) => { + const source = executor.namedArgumentList.find(it => it.name == 'source')?.value ?? fallbackSource; + if (source instanceof SlashCommandClosure) throw new Error('Argument \'source\' does not support closures'); + const attachments = getAttachments(source); + + return attachments.map(attachment => new SlashCommandEnumValue( + returnField === 'name' ? attachment.name : attachment.url, + `${enumIcons.getStateIcon(!extension_settings.disabled_attachments.includes(attachment.url))} [${source}] ${returnField === 'url' ? attachment.name : attachment.url}`, + enumTypes.enum, enumIcons.file)); + }, + }; + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'db', - callback: () => document.getElementById('manageAttachments')?.click(), + callback: () => { + document.getElementById('manageAttachments')?.click(); + return ''; + }, aliases: ['databank', 'data-bank'], helpString: 'Open the data bank', })); @@ -224,7 +250,13 @@ jQuery(async () => { new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, '', TYPES), ], unnamedArgumentList: [ - new SlashCommandArgument('The name or URL of the attachment.', ARGUMENT_TYPE.STRING, true, false), + SlashCommandArgument.fromProps({ + description: 'The name or URL of the attachment.', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + acceptsMultiple: false, + enumProvider: localEnumProviders.attachments('name', ''), + }), ], returns: ARGUMENT_TYPE.STRING, })); @@ -251,8 +283,18 @@ jQuery(async () => { helpString: 'Update an attachment in the Data Bank, preserving its name. Returns a new URL of the attachment.', namedArgumentList: [ new SlashCommandNamedArgument('source', 'The source for the attachment.', ARGUMENT_TYPE.STRING, false, false, 'chat', TYPES), - new SlashCommandNamedArgument('name', 'The name of the attachment.', ARGUMENT_TYPE.STRING, false, false), - new SlashCommandNamedArgument('url', 'The URL of the attachment to update.', ARGUMENT_TYPE.STRING, false, false), + SlashCommandNamedArgument.fromProps({ + name: 'name', + description: 'The name of the attachment.', + typeList: [ARGUMENT_TYPE.STRING], + enumProvider: localEnumProviders.attachments('name'), + }), + SlashCommandNamedArgument.fromProps({ + name: 'url', + description: 'The URL of the attachment.', + typeList: [ARGUMENT_TYPE.STRING], + enumProvider: localEnumProviders.attachments('url'), + }), ], unnamedArgumentList: [ new SlashCommandArgument('The content of the file attachment.', ARGUMENT_TYPE.STRING, true, false), @@ -269,7 +311,12 @@ jQuery(async () => { new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, '', TYPES), ], unnamedArgumentList: [ - new SlashCommandArgument('The name or URL of the attachment.', ARGUMENT_TYPE.STRING, true, false), + SlashCommandArgument.fromProps({ + description: 'The name or URL of the attachment.', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: localEnumProviders.attachments('name', ''), + }), ], })); @@ -282,7 +329,12 @@ jQuery(async () => { new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, '', TYPES), ], unnamedArgumentList: [ - new SlashCommandArgument('The name or URL of the attachment.', ARGUMENT_TYPE.STRING, true, false), + SlashCommandArgument.fromProps({ + description: 'The name or URL of the attachment.', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: localEnumProviders.attachments('name', ''), + }), ], })); @@ -295,7 +347,12 @@ jQuery(async () => { new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, 'chat', TYPES), ], unnamedArgumentList: [ - new SlashCommandArgument('The name or URL of the attachment.', ARGUMENT_TYPE.STRING, true, false), + SlashCommandArgument.fromProps({ + description: 'The name or URL of the attachment.', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: localEnumProviders.attachments(), + }), ], })); }); diff --git a/public/scripts/extensions/caption/index.js b/public/scripts/extensions/caption/index.js index f5cc0845d..8e8595698 100644 --- a/public/scripts/extensions/caption/index.js +++ b/public/scripts/extensions/caption/index.js @@ -8,6 +8,8 @@ import { textgen_types, textgenerationwebui_settings } from '../../textgen-setti import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; import { SlashCommand } from '../../slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; +import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js'; +import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; export { MODULE_NAME }; const MODULE_NAME = 'caption'; @@ -451,11 +453,14 @@ jQuery(async function () { returns: 'caption', namedArgumentList: [ new SlashCommandNamedArgument( - 'quiet', 'suppress sending a captioned message', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', ['true', 'false'], - ), - new SlashCommandNamedArgument( - 'id', 'get image from a message with this ID', [ARGUMENT_TYPE.NUMBER], false, false, + 'quiet', 'suppress sending a captioned message', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', ), + SlashCommandNamedArgument.fromProps({ + name: 'id', + description: 'get image from a message with this ID', + typeList: [ARGUMENT_TYPE.NUMBER], + enumProvider: commonEnumProviders.messages(), + }), ], unnamedArgumentList: [ new SlashCommandArgument( diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index d05659ac1..98615b6d8 100644 --- a/public/scripts/extensions/expressions/index.js +++ b/public/scripts/extensions/expressions/index.js @@ -10,7 +10,8 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; import { SlashCommand } from '../../slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument } from '../../slash-commands/SlashCommandArgument.js'; import { isFunctionCallingSupported } from '../../openai.js'; -import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js'; +import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js'; +import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; export { MODULE_NAME }; const MODULE_NAME = 'expressions'; @@ -2044,6 +2045,14 @@ function migrateSettings() { }); eventSource.on(event_types.MOVABLE_PANELS_RESET, updateVisualNovelModeDebounced); eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced); + + const localEnumProviders = { + expressions: () => getCachedExpressions().map(expression => { + const isCustom = extension_settings.expressions.custom?.includes(expression); + return new SlashCommandEnumValue(expression, null, isCustom ? enumTypes.name : enumTypes.enum, isCustom ? 'C' : 'D'); + }), + }; + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'sprite', aliases: ['emote'], @@ -2053,7 +2062,7 @@ function migrateSettings() { description: 'spriteId', typeList: [ARGUMENT_TYPE.STRING], isRequired: true, - enumProvider: () => getCachedExpressions().map((x) => new SlashCommandEnumValue(x)), + enumProvider: localEnumProviders.expressions, }), ], helpString: 'Force sets the sprite for the current character.', @@ -2075,9 +2084,12 @@ function migrateSettings() { callback: (_, value) => lastExpression[String(value).trim()] ?? '', returns: 'sprite', unnamedArgumentList: [ - new SlashCommandArgument( - 'charName', [ARGUMENT_TYPE.STRING], true, - ), + SlashCommandArgument.fromProps({ + description: 'character name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: commonEnumProviders.characters('character'), + }), ], helpString: 'Returns the last set sprite / expression for the named character.', })); diff --git a/public/scripts/extensions/gallery/index.js b/public/scripts/extensions/gallery/index.js index ca0572912..ce9a12b32 100644 --- a/public/scripts/extensions/gallery/index.js +++ b/public/scripts/extensions/gallery/index.js @@ -12,6 +12,7 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; import { SlashCommand } from '../../slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; import { DragAndDropHandler } from '../../dragdrop.js'; +import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; const extensionName = 'gallery'; const extensionFolderPath = `scripts/extensions/${extensionName}/`; @@ -401,7 +402,10 @@ function viewWithDragbox(items) { // Registers a simple command for opening the char gallery. SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'show-gallery', aliases: ['sg'], - callback: showGalleryCommand, + callback: () => { + showCharGallery(); + return ''; + }, helpString: 'Shows the gallery.', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'list-gallery', @@ -409,21 +413,22 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'list-gallery callback: listGalleryCommand, returns: 'list of images', namedArgumentList: [ - new SlashCommandNamedArgument( - 'char', 'character name', [ARGUMENT_TYPE.STRING], false, - ), - new SlashCommandNamedArgument( - 'group', 'group name', [ARGUMENT_TYPE.STRING], false, - ), + SlashCommandNamedArgument.fromProps({ + name: 'char', + description: 'character name', + typeList: [ARGUMENT_TYPE.STRING], + enumProvider: commonEnumProviders.characters('character'), + }), + SlashCommandNamedArgument.fromProps({ + name: 'group', + description: 'group name', + typeList: [ARGUMENT_TYPE.STRING], + enumProvider: commonEnumProviders.characters('group'), + }), ], helpString: 'List images in the gallery of the current char / group or a specified char / group.', })); - -function showGalleryCommand(args) { - showCharGallery(); -} - async function listGalleryCommand(args) { try { let url = args.char ?? (args.group ? groups.find(it=>it.name == args.group)?.id : null) ?? (selected_group || this_chid); diff --git a/public/scripts/extensions/memory/index.js b/public/scripts/extensions/memory/index.js index 1cc62ed02..5d61417e3 100644 --- a/public/scripts/extensions/memory/index.js +++ b/public/scripts/extensions/memory/index.js @@ -24,6 +24,7 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; import { SlashCommand } from '../../slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; import { resolveVariable } from '../../variables.js'; +import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; export { MODULE_NAME }; const MODULE_NAME = '1_memory'; @@ -919,7 +920,14 @@ jQuery(async function () { callback: summarizeCallback, namedArgumentList: [ new SlashCommandNamedArgument('source', 'API to use for summarization', [ARGUMENT_TYPE.STRING], false, false, '', ['main', 'extras']), - new SlashCommandNamedArgument('prompt', 'prompt to use for summarization', [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.VARIABLE_NAME], false, false, ''), + SlashCommandNamedArgument.fromProps({ + name: 'prompt', + description: 'prompt to use for summarization', + typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.VARIABLE_NAME], + defaultValue: '', + enumProvider: commonEnumProviders.variables('all'), + forceEnum: false, + }), ], unnamedArgumentList: [ new SlashCommandArgument('text to summarize', [ARGUMENT_TYPE.STRING], false, false, ''), diff --git a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js index f5877086d..a2c54e9ee 100644 --- a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js +++ b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js @@ -1,9 +1,13 @@ import { SlashCommand } from '../../../slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../../slash-commands/SlashCommandArgument.js'; +import { enumIcons } from '../../../slash-commands/SlashCommandCommonEnumsProvider.js'; +import { SlashCommandEnumValue, enumTypes } from '../../../slash-commands/SlashCommandEnumValue.js'; import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js'; import { isTrueBoolean } from '../../../utils.js'; // eslint-disable-next-line no-unused-vars import { QuickReplyApi } from '../api/QuickReplyApi.js'; +import { QuickReply } from './QuickReply.js'; +import { QuickReplySet } from './QuickReplySet.js'; export class SlashCommandHandler { /**@type {QuickReplyApi}*/ api; @@ -19,6 +23,50 @@ export class SlashCommandHandler { init() { + function getExecutionIcons(/**@type {QuickReply} */ qr) { + let icons = ''; + if (qr.preventAutoExecute) icons += '🚫'; + if (qr.isHidden) icons += '👁️'; + if (qr.executeOnStartup) icons += '🚀'; + if (qr.executeOnUser) icons += enumIcons.user; + if (qr.executeOnAi) icons += enumIcons.assistant; + if (qr.executeOnChatChange) icons += '💬'; + if (qr.executeOnGroupMemberDraft) icons += enumIcons.group; + return icons; + } + + const localEnumProviders = { + /** All quick reply sets, optionally filtering out sets that wer already used in the "set" named argument */ + qrSets: (executor) => QuickReplySet.list.filter(qrSet => qrSet.name != String(executor.namedArgumentList.find(x => x.name == 'set')?.value)) + .map(qrSet => new SlashCommandEnumValue(qrSet.name, null, enumTypes.enum, 'S')), + + /** All QRs inside a set, utilizing the "set" named argument */ + qrEntries: (executor) => QuickReplySet.get(String(executor.namedArgumentList.find(x => x.name == 'set')?.value))?.qrList.map(qr => { + const icons = getExecutionIcons(qr); + const message = `${qr.automationId ? `[${qr.automationId}]` : ''}${icons ? `[auto: ${icons}]` : ''} ${qr.title || qr.message}`.trim(); + return new SlashCommandEnumValue(qr.label, message, enumTypes.enum, enumIcons.qr); + }) ?? [], + + /** All QRs as a set.name string, to be able to execute, for example via the /run command */ + qrExecutables: () => { + const globalSetList = this.api.settings.config.setList; + const chatSetList = this.api.settings.chatConfig?.setList; + + const globalQrs = globalSetList.map(link => link.set.qrList.map(qr => ({ set: link.set, qr }))).flat(); + const chatQrs = chatSetList?.map(link => link.set.qrList.map(qr => ({ set: link.set, qr }))).flat() ?? []; + const otherQrs = QuickReplySet.list.filter(set => !globalSetList.some(link => link.set.name === set.name && !chatSetList?.some(link => link.set.name === set.name))) + .map(set => set.qrList.map(qr => ({ set, qr }))).flat(); + + return [ + ...globalQrs.map(x => new SlashCommandEnumValue(`${x.set.name}.${x.qr.label}`, `[global] ${x.qr.title || x.qr.message}`, enumTypes.name, enumIcons.qr)), + ...chatQrs.map(x => new SlashCommandEnumValue(`${x.set.name}.${x.qr.label}`, `[chat] ${x.qr.title || x.qr.message}`, enumTypes.enum, enumIcons.qr)), + ...otherQrs.map(x => new SlashCommandEnumValue(`${x.set.name}.${x.qr.label}`, `${x.qr.title || x.qr.message}`, enumTypes.qr, enumIcons.qr)), + ]; + }, + } + + window['qrEnumProviderExecutables'] = localEnumProviders.qrExecutables; + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr', callback: (_, value) => this.executeQuickReplyByIndex(Number(value)), unnamedArgumentList: [ @@ -29,110 +77,166 @@ export class SlashCommandHandler { helpString: 'Activates the specified Quick Reply', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qrset', - callback: () => toastr.warning('The command /qrset has been deprecated. Use /qr-set, /qr-set-on, and /qr-set-off instead.'), + callback: () => { + toastr.warning('The command /qrset has been deprecated. Use /qr-set, /qr-set-on, and /qr-set-off instead.'); + return ''; + }, helpString: 'DEPRECATED – The command /qrset has been deprecated. Use /qr-set, /qr-set-on, and /qr-set-off instead.', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set', - callback: (args, value) => this.toggleGlobalSet(value, args), + callback: (args, value) => { + this.toggleGlobalSet(value, args); + return ''; + }, namedArgumentList: [ new SlashCommandNamedArgument( 'visible', 'set visibility', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true', ), ], unnamedArgumentList: [ - new SlashCommandArgument( - 'QR set name', [ARGUMENT_TYPE.STRING], true, - ), + SlashCommandArgument.fromProps({ + description: 'QR set name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: localEnumProviders.qrSets, + }), ], helpString: 'Toggle global QR set', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-on', - callback: (args, value) => this.addGlobalSet(value, args), + callback: (args, value) => { + this.addGlobalSet(value, args); + return ''; + }, namedArgumentList: [ new SlashCommandNamedArgument( 'visible', 'set visibility', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true', ), ], unnamedArgumentList: [ - new SlashCommandArgument( - 'QR set name', [ARGUMENT_TYPE.STRING], true, - ), + SlashCommandArgument.fromProps({ + description: 'QR set name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: localEnumProviders.qrSets, + }), ], helpString: 'Activate global QR set', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-off', - callback: (_, value) => this.removeGlobalSet(value), + callback: (_, value) => { + this.removeGlobalSet(value); + return ''; + }, unnamedArgumentList: [ - new SlashCommandArgument( - 'QR set name', [ARGUMENT_TYPE.STRING], true, - ), + SlashCommandArgument.fromProps({ + description: 'QR set name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: localEnumProviders.qrSets, + }), ], helpString: 'Deactivate global QR set', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-chat-set', - callback: (args, value) => this.toggleChatSet(value, args), + callback: (args, value) => { + this.toggleChatSet(value, args); + return ''; + }, namedArgumentList: [ new SlashCommandNamedArgument( 'visible', 'set visibility', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true', ), ], unnamedArgumentList: [ - new SlashCommandArgument( - 'QR set name', [ARGUMENT_TYPE.STRING], true, - ), + SlashCommandArgument.fromProps({ + description: 'QR set name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: localEnumProviders.qrSets, + }), ], helpString: 'Toggle chat QR set', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-chat-set-on', - callback: (args, value) => this.addChatSet(value, args), + callback: (args, value) => { + this.addChatSet(value, args); + return ''; + }, namedArgumentList: [ new SlashCommandNamedArgument( - 'visible', 'whether the QR set should be visible', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true', ['true', 'false'], + 'visible', 'whether the QR set should be visible', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true', ), ], unnamedArgumentList: [ - new SlashCommandArgument( - 'QR set name', [ARGUMENT_TYPE.STRING], true, - ), + SlashCommandArgument.fromProps({ + description: 'QR set name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: localEnumProviders.qrSets, + }), ], helpString: 'Activate chat QR set', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-chat-set-off', - callback: (_, value) => this.removeChatSet(value), + callback: (_, value) => { + this.removeChatSet(value); + return ''; + }, unnamedArgumentList: [ - new SlashCommandArgument( - 'QR set name', [ARGUMENT_TYPE.STRING], true, - ), + SlashCommandArgument.fromProps({ + description: 'QR set name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: localEnumProviders.qrSets, + }), ], helpString: 'Deactivate chat QR set', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-list', - callback: (_, value) => this.listSets(value ?? 'all'), + callback: (_, value) => JSON.stringify(this.listSets(value ?? 'all')), returns: 'list of QR sets', namedArgumentList: [], unnamedArgumentList: [ new SlashCommandArgument( - 'set type', [ARGUMENT_TYPE.STRING], false, false, null, ['all', 'global', 'chat'], + 'set type', [ARGUMENT_TYPE.STRING], false, false, 'all', ['all', 'global', 'chat'], ), ], helpString: 'Gets a list of the names of all quick reply sets.', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-list', - callback: (_, value) => this.listQuickReplies(value), + callback: (_, value) => { + return JSON.stringify(this.listQuickReplies(value)); + }, returns: 'list of QRs', namedArgumentList: [], unnamedArgumentList: [ - new SlashCommandArgument( - 'set name', [ARGUMENT_TYPE.STRING], true, - ), + SlashCommandArgument.fromProps({ + description: 'QR set name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: localEnumProviders.qrSets, + }), ], helpString: 'Gets a list of the names of all quick replies in this quick reply set.', })); const qrArgs = [ - new SlashCommandNamedArgument('label', 'text on the button, e.g., label=MyButton', [ARGUMENT_TYPE.STRING]), - new SlashCommandNamedArgument('set', 'name of the QR set, e.g., set=PresetName1', [ARGUMENT_TYPE.STRING]), + SlashCommandNamedArgument.fromProps({ + name: 'set', + description: 'name of the QR set, e.g., set=PresetName1', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: localEnumProviders.qrSets, + }), + SlashCommandNamedArgument.fromProps({ + name: 'label', + description: 'text on the button, e.g., label=MyButton', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: localEnumProviders.qrLabels, + }), new SlashCommandNamedArgument('hidden', 'whether the button should be hidden, e.g., hidden=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'), new SlashCommandNamedArgument('startup', 'auto execute on app startup, e.g., startup=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'), new SlashCommandNamedArgument('user', 'auto execute on user message, e.g., user=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'), @@ -144,8 +248,12 @@ export class SlashCommandHandler { const qrUpdateArgs = [ new SlashCommandNamedArgument('newlabel', 'new text for the button', [ARGUMENT_TYPE.STRING], false), ]; + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-create', - callback: (args, message) => this.createQuickReply(args, message), + callback: (args, message) => { + this.createQuickReply(args, message); + return ''; + }, namedArgumentList: qrArgs, unnamedArgumentList: [ new SlashCommandArgument( @@ -165,9 +273,15 @@ export class SlashCommandHandler { `, })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-update', - callback: (args, message) => this.updateQuickReply(args, message), + callback: (args, message) => { + this.updateQuickReply(args, message); + return ''; + }, returns: 'updated quick reply', namedArgumentList: [...qrUpdateArgs, ...qrArgs], + unnamedArgumentList: [ + new SlashCommandArgument('command', [ARGUMENT_TYPE.STRING]), + ], helpString: `
Updates Quick Reply. @@ -183,34 +297,57 @@ export class SlashCommandHandler { `, })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-delete', - callback: (args, name) => this.deleteQuickReply(args, name), + callback: (args, name) => { + this.deleteQuickReply(args, name); + return ''; + }, namedArgumentList: [ - new SlashCommandNamedArgument( - 'set', 'Quick Reply set', [ARGUMENT_TYPE.STRING], true, - ), - new SlashCommandNamedArgument( - 'label', 'Quick Reply label', [ARGUMENT_TYPE.STRING], false, - ), + SlashCommandNamedArgument.fromProps({ + name: 'set', + description: 'QR set name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: localEnumProviders.qrSets, + }), + SlashCommandNamedArgument.fromProps({ + name: 'label', + description: 'Quick Reply label', + typeList: [ARGUMENT_TYPE.STRING], + enumProvider: localEnumProviders.qrEntries, + }), ], helpString: 'Deletes a Quick Reply from the specified set. If no label is provided, the entire set is deleted.', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-contextadd', - callback: (args, name) => this.createContextItem(args, name), + callback: (args, name) => { + this.createContextItem(args, name); + return ''; + }, namedArgumentList: [ - new SlashCommandNamedArgument( - 'set', 'string', [ARGUMENT_TYPE.STRING], true, - ), - new SlashCommandNamedArgument( - 'label', 'string', [ARGUMENT_TYPE.STRING], true, - ), + SlashCommandNamedArgument.fromProps({ + name: 'set', + description: 'QR set name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: localEnumProviders.qrSets, + }), + SlashCommandNamedArgument.fromProps({ + name: 'label', + description: 'Quick Reply label', + typeList: [ARGUMENT_TYPE.STRING], + enumProvider: localEnumProviders.qrEntries, + }), new SlashCommandNamedArgument( 'chain', 'boolean', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', ), ], unnamedArgumentList: [ - new SlashCommandArgument( - 'preset name', [ARGUMENT_TYPE.STRING], true, - ), + SlashCommandArgument.fromProps({ + description: 'QR set name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: localEnumProviders.qrSets, + }), ], helpString: `
@@ -227,19 +364,32 @@ export class SlashCommandHandler { `, })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-contextdel', - callback: (args, name) => this.deleteContextItem(args, name), + callback: (args, name) => { + this.deleteContextItem(args, name); + return ''; + }, namedArgumentList: [ - new SlashCommandNamedArgument( - 'set', 'string', [ARGUMENT_TYPE.STRING], true, - ), - new SlashCommandNamedArgument( - 'label', 'string', [ARGUMENT_TYPE.STRING], true, - ), + SlashCommandNamedArgument.fromProps({ + name: 'set', + description: 'QR set name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: localEnumProviders.qrSets, + }), + SlashCommandNamedArgument.fromProps({ + name: 'label', + description: 'Quick Reply label', + typeList: [ARGUMENT_TYPE.STRING], + enumProvider: localEnumProviders.qrEntries, + }), ], unnamedArgumentList: [ - new SlashCommandArgument( - 'preset name', [ARGUMENT_TYPE.STRING], true, - ), + SlashCommandArgument.fromProps({ + description: 'QR set name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: localEnumProviders.qrSets, + }), ], helpString: `
@@ -256,16 +406,25 @@ export class SlashCommandHandler { `, })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-contextclear', - callback: (args, label) => this.clearContextMenu(args, label), + callback: (args, label) => { + this.clearContextMenu(args, label); + return ''; + }, namedArgumentList: [ - new SlashCommandNamedArgument( - 'set', 'context menu preset name', [ARGUMENT_TYPE.STRING], true, - ), + SlashCommandNamedArgument.fromProps({ + name: 'set', + description: 'QR set name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: localEnumProviders.qrSets, + }), ], unnamedArgumentList: [ - new SlashCommandArgument( - 'label', [ARGUMENT_TYPE.STRING], true, - ), + SlashCommandArgument.fromProps({ + description: 'Quick Reply label', + typeList: [ARGUMENT_TYPE.STRING], + enumProvider: localEnumProviders.qrEntries, + }), ], helpString: `
@@ -288,13 +447,20 @@ export class SlashCommandHandler { new SlashCommandNamedArgument('inject', 'inject user input automatically (if disabled use {{input}})', [ARGUMENT_TYPE.BOOLEAN], false), ]; SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-create', - callback: (args, name) => this.createSet(name, args), + callback: (args, name) => { + this.createSet(name, args); + return ''; + }, aliases: ['qr-presetadd'], namedArgumentList: presetArgs, unnamedArgumentList: [ - new SlashCommandArgument( - 'name', [ARGUMENT_TYPE.STRING], true, - ), + SlashCommandArgument.fromProps({ + description: 'QR set name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: localEnumProviders.qrSets, + forceEnum: false, + }), ], helpString: `
@@ -312,11 +478,19 @@ export class SlashCommandHandler { })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-update', - callback: (args, name) => this.updateSet(name, args), + callback: (args, name) => { + this.updateSet(name, args); + return ''; + }, aliases: ['qr-presetupdate'], namedArgumentList: presetArgs, unnamedArgumentList: [ - new SlashCommandArgument('name', [ARGUMENT_TYPE.STRING], true), + SlashCommandArgument.fromProps({ + description: 'QR set name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: localEnumProviders.qrSets, + }), ], helpString: `
@@ -329,10 +503,18 @@ export class SlashCommandHandler { `, })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-delete', - callback: (args, name) => this.deleteSet(name), + callback: (_, name) => { + this.deleteSet(name); + return ''; + }, aliases: ['qr-presetdelete'], unnamedArgumentList: [ - new SlashCommandArgument('name', [ARGUMENT_TYPE.STRING], true), + SlashCommandArgument.fromProps({ + description: 'QR set name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: localEnumProviders.qrSets, + }), ], helpString: `
@@ -468,7 +650,7 @@ export class SlashCommandHandler { } deleteQuickReply(args, label) { try { - this.api.deleteQuickReply(args.set, label); + this.api.deleteQuickReply(args.set, args.label ?? label); } catch (ex) { toastr.error(ex.message); } diff --git a/public/scripts/extensions/regex/engine.js b/public/scripts/extensions/regex/engine.js index 24aa18d49..f55e8bb0b 100644 --- a/public/scripts/extensions/regex/engine.js +++ b/public/scripts/extensions/regex/engine.js @@ -92,7 +92,7 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown, /** * Runs the provided regex script on the given string - * @param {object} regexScript The regex script to run + * @param {import('./index.js').RegexScript} regexScript The regex script to run * @param {string} rawString The string to run the regex script on * @param {RegexScriptParams} params The parameters to use for the regex script * @returns {string} The new string diff --git a/public/scripts/extensions/regex/index.js b/public/scripts/extensions/regex/index.js index 374004e8b..7108b9315 100644 --- a/public/scripts/extensions/regex/index.js +++ b/public/scripts/extensions/regex/index.js @@ -3,11 +3,32 @@ import { extension_settings, renderExtensionTemplateAsync, writeExtensionField } import { selected_group } from '../../group-chats.js'; import { SlashCommand } from '../../slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; +import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; +import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js'; import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; import { download, getFileText, getSortableDelay, uuidv4 } from '../../utils.js'; import { resolveVariable } from '../../variables.js'; import { regex_placement, runRegexScript } from './engine.js'; +/** + * @typedef {object} RegexScript + * @property {string} scriptName - The name of the script + * @property {boolean} disabled - Whether the script is disabled + * @property {string} replaceString - The replace string + * @property {string[]} trimStrings - The trim strings + * @property {string?} findRegex - The find regex + * @property {string?} substituteRegex - The substitute regex + */ + +/** + * Retrieves the list of regex scripts by combining the scripts from the extension settings and the character data + * + * @return {RegexScript[]} An array of regex scripts, where each script is an object containing the necessary information. + */ +export function getRegexScripts() { + return [...(extension_settings.regex ?? []), ...(characters[this_chid]?.data?.extensions?.regex_scripts ?? [])]; +} + /** * Saves a regex script to the extension settings or character data. * @param {import('../../char-data.js').RegexScriptData} regexScript @@ -339,7 +360,7 @@ function runRegexCallback(args, value) { } const scriptName = String(resolveVariable(args.name)); - const scripts = [...(extension_settings.regex ?? []), ...(characters[this_chid]?.data?.extensions?.regex_scripts ?? [])]; + const scripts = getRegexScripts(); for (const script of scripts) { if (String(script.scriptName).toLowerCase() === String(scriptName).toLowerCase()) { @@ -551,14 +572,26 @@ jQuery(async () => { await loadRegexScripts(); $('#saved_regex_scripts').sortable('enable'); + const localEnumProviders = { + regexScripts: () => getRegexScripts().map(script => { + const isGlobal = extension_settings.regex?.some(x => x.scriptName === script.scriptName); + return new SlashCommandEnumValue(script.scriptName, `${enumIcons.getStateIcon(!script.disabled)} [${isGlobal ? 'global' : 'scoped'}] ${script.findRegex}`, + isGlobal ? enumTypes.enum : enumTypes.name, isGlobal ? 'G' : 'S'); + }), + }; + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'regex', callback: runRegexCallback, returns: 'replaced text', namedArgumentList: [ - new SlashCommandNamedArgument( - 'name', 'script name', [ARGUMENT_TYPE.STRING], true, - ), + SlashCommandNamedArgument.fromProps({ + name: 'name', + description: 'script name', + typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: true, + enumProvider: localEnumProviders.regexScripts, + }), ], unnamedArgumentList: [ new SlashCommandArgument( diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index de70e3c9c..2a759ac99 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -31,6 +31,8 @@ import { SlashCommand } from '../../slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; import { resolveVariable } from '../../variables.js'; import { debounce_timeout } from '../../constants.js'; +import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; +import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js'; export { MODULE_NAME }; const MODULE_NAME = 'sd'; @@ -3349,11 +3351,14 @@ jQuery(async () => { aliases: ['sd', 'img', 'image'], namedArgumentList: [ new SlashCommandNamedArgument( - 'quiet', 'whether to post the generated image to chat', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', ['false', 'true'], - ), - new SlashCommandNamedArgument( - 'negative', 'negative prompt prefix', [ARGUMENT_TYPE.STRING], false, false, '', + 'quiet', 'whether to post the generated image to chat', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', ), + SlashCommandNamedArgument.fromProps({ + name: 'negative', + description: 'negative prompt prefix', + typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.VARIABLE_NAME], + enumProvider: commonEnumProviders.variables('all'), + }), ], unnamedArgumentList: [ new SlashCommandArgument( @@ -3362,7 +3367,7 @@ jQuery(async () => { ], helpString: `
- Requests to generate an image and posts it to chat (unless quiet=true argument is specified). Supported arguments: ${Object.values(triggerWords).flat().join(', ')}. + Requests to generate an image and posts it to chat (unless quiet=true argument is specified)..
Anything else would trigger a "free mode" to make generate whatever you prompted. Example: /imagine apple tree would generate a picture of an apple tree. Returns a link to the generated image. @@ -3375,9 +3380,12 @@ jQuery(async () => { callback: changeComfyWorkflow, aliases: ['icw'], unnamedArgumentList: [ - new SlashCommandArgument( - 'workflowName', [ARGUMENT_TYPE.STRING], true, - ), + SlashCommandArgument.fromProps({ + description: 'workflow name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: () => Array.from(document.querySelectorAll('#sd_comfy_workflow > [value]')).map(x => x.getAttribute('value')).map(workflow => new SlashCommandEnumValue(workflow)), + }), ], helpString: '(workflowName) - change the workflow to be used for image generation with ComfyUI, e.g.
/imagine-comfy-workflow MyWorkflow
', })); diff --git a/public/scripts/extensions/token-counter/index.js b/public/scripts/extensions/token-counter/index.js index f93a20e9b..b7f0adeb2 100644 --- a/public/scripts/extensions/token-counter/index.js +++ b/public/scripts/extensions/token-counter/index.js @@ -123,6 +123,7 @@ async function doCount() { //toastr success with the token count of the chat const count = await getTokenCountAsync(allMessages); toastr.success(`Token count: ${count}`); + return count; } jQuery(() => { @@ -134,7 +135,7 @@ jQuery(() => { $('#extensionsMenu').prepend(buttonHtml); $('#token_counter').on('click', doTokenCounter); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'count', - callback: doCount, + callback: async () => String(await doCount()), returns: 'number of tokens', helpString: 'Counts the number of tokens in the current chat.', })); diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js index 8a0a4853d..da0d860b1 100644 --- a/public/scripts/extensions/tts/index.js +++ b/public/scripts/extensions/tts/index.js @@ -19,6 +19,8 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; import { SlashCommand } from '../../slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; import { debounce_timeout } from '../../constants.js'; +import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js'; +import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; export { talkingAnimation }; const UPDATE_INTERVAL = 1000; @@ -1204,12 +1206,19 @@ $(document).ready(function () { eventSource.makeLast(event_types.USER_MESSAGE_RENDERED, onMessageEvent); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'speak', - callback: onNarrateText, + callback: async (args, value) => { + await onNarrateText(args, value); + return ''; + }, aliases: ['narrate', 'tts'], namedArgumentList: [ - new SlashCommandNamedArgument( - 'voice', 'character voice name', [ARGUMENT_TYPE.STRING], false, - ), + SlashCommandNamedArgument.fromProps({ + name: 'voice', + description: 'character voice name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: false, + enumProvider: () => Object.keys(voiceMap).map(voiceName => new SlashCommandEnumValue(voiceName, null, enumTypes.enum, enumIcons.voice)), + }), ], unnamedArgumentList: [ new SlashCommandArgument( diff --git a/public/scripts/group-chats.js b/public/scripts/group-chats.js index ad6c38905..6cb40914a 100644 --- a/public/scripts/group-chats.js +++ b/public/scripts/group-chats.js @@ -233,9 +233,20 @@ export async function getGroupChat(groupId, reload = false) { if (freshChat) await eventSource.emit(event_types.GROUP_CHAT_CREATED); } +/** + * Retrieves the members of a group + * + * @param {string} [groupId=selected_group] - The ID of the group to retrieve members from. Defaults to the currently selected group. + * @returns {import('../script.js').Character[]} An array of character objects representing the members of the group. If the group is not found, an empty array is returned. + */ +export function getGroupMembers(groupId = selected_group) { + const group = groups.find((x) => x.id === groupId); + return group?.members.map(member => characters.find(x => x.avatar === member)) ?? []; +} + /** * Finds the character ID for a group member. - * @param {string} arg 1-based member index or character name + * @param {string} arg 0-based member index or character name * @returns {number} 0-based character ID */ export function findGroupMemberId(arg) { @@ -253,8 +264,7 @@ export function findGroupMemberId(arg) { return; } - // Index is 1-based - const index = parseInt(arg) - 1; + const index = parseInt(arg); const searchByName = isNaN(index); if (searchByName) { diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 815829c71..f8b61f89f 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -71,6 +71,7 @@ import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { SlashCommand } from './slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js'; import { renderTemplateAsync } from './templates.js'; +import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js'; export { openai_messages_count, @@ -199,16 +200,18 @@ const custom_prompt_post_processing_types = { CLAUDE: 'claude', }; -const prefixMap = selected_group ? { - assistant: '', - user: '', - system: 'OOC: ', +function getPrefixMap() { + return selected_group ? { + assistant: '', + user: '', + system: 'OOC: ', + } + : { + assistant: '{{char}}:', + user: '{{user}}:', + system: '', + }; } - : { - assistant: '{{char}}:', - user: '{{user}}:', - system: '', - }; const default_settings = { preset_settings_openai: 'Default', @@ -1709,7 +1712,7 @@ async function sendOpenAIRequest(type, messages, signal) { if (isAI21) { const joinedMsgs = messages.reduce((acc, obj) => { - const prefix = prefixMap[obj.role]; + const prefix = getPrefixMap()[obj.role]; return acc + (prefix ? (selected_group ? '\n' : prefix + ' ') : '') + obj.content + '\n'; }, ''); messages = substituteParams(joinedMsgs) + (isImpersonate ? `${name1}:` : `${name2}:`); @@ -4602,9 +4605,12 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ returns: 'current proxy', namedArgumentList: [], unnamedArgumentList: [ - new SlashCommandArgument( - 'name', [ARGUMENT_TYPE.STRING], true, - ), + SlashCommandArgument.fromProps({ + description: 'name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: () => proxies.map(preset => new SlashCommandEnumValue(preset.name, preset.url)), + }), ], helpString: 'Sets a proxy preset by name.', })); diff --git a/public/scripts/popup.js b/public/scripts/popup.js index 6e388839c..8e82994c5 100644 --- a/public/scripts/popup.js +++ b/public/scripts/popup.js @@ -3,33 +3,39 @@ import { removeFromArray, runAfterAnimation, uuidv4 } from './utils.js'; /** @readonly */ /** @enum {Number} */ export const POPUP_TYPE = { - 'TEXT': 1, - 'CONFIRM': 2, - 'INPUT': 3, + /** Main popup type. Containing any content displayed, with buttons below. Can also contain additional input controls. */ + TEXT: 1, + /** Popup mainly made to confirm something, answering with a simple Yes/No or similar. Focus on the button controls. */ + CONFIRM: 2, + /** Popup who's main focus is the input text field, which is displayed here. Can contain additional content above. Return value for this is the input string. */ + INPUT: 3, + /** Popup without any button controls. Used to simply display content, with a small X in the corner. */ + DISPLAY: 4, }; /** @readonly */ /** @enum {number?} */ export const POPUP_RESULT = { - 'AFFIRMATIVE': 1, - 'NEGATIVE': 0, - 'CANCELLED': null, + AFFIRMATIVE: 1, + NEGATIVE: 0, + CANCELLED: null, }; /** * @typedef {object} PopupOptions - * @property {string|boolean?} [okButton] - Custom text for the OK button, or `true` to use the default (If set, the button will always be displayed, no matter the type of popup) - * @property {string|boolean?} [cancelButton] - Custom text for the Cancel button, or `true` to use the default (If set, the button will always be displayed, no matter the type of popup) - * @property {number?} [rows] - The number of rows for the input field - * @property {boolean?} [wide] - Whether to display the popup in wide mode (wide screen, 1/1 aspect ratio) - * @property {boolean?} [wider] - Whether to display the popup in wider mode (just wider, no height scaling) - * @property {boolean?} [large] - Whether to display the popup in large mode (90% of screen) - * @property {boolean?} [allowHorizontalScrolling] - Whether to allow horizontal scrolling in the popup - * @property {boolean?} [allowVerticalScrolling] - Whether to allow vertical scrolling in the popup - * @property {POPUP_RESULT|number?} [defaultResult] - The default result of this popup when Enter is pressed. Can be changed from `POPUP_RESULT.AFFIRMATIVE`. - * @property {CustomPopupButton[]|string[]?} [customButtons] - Custom buttons to add to the popup. If only strings are provided, the buttons will be added with default options, and their result will be in order from `2` onward. - * @property {(popup: Popup) => boolean?} [onClosing] - Handler called before the popup closes, return `false` to cancel the close - * @property {(popup: Popup) => void?} [onClose] - Handler called after the popup closes, but before the DOM is cleaned up + * @property {string|boolean?} [okButton=null] - Custom text for the OK button, or `true` to use the default (If set, the button will always be displayed, no matter the type of popup) + * @property {string|boolean?} [cancelButton=null] - Custom text for the Cancel button, or `true` to use the default (If set, the button will always be displayed, no matter the type of popup) + * @property {number?} [rows=1] - The number of rows for the input field + * @property {boolean?} [wide=false] - Whether to display the popup in wide mode (wide screen, 1/1 aspect ratio) + * @property {boolean?} [wider=false] - Whether to display the popup in wider mode (just wider, no height scaling) + * @property {boolean?} [large=false] - Whether to display the popup in large mode (90% of screen) + * @property {boolean?} [transparent=false] - Whether to display the popup in transparent mode (no background, border, shadow or anything, only its content) + * @property {boolean?} [allowHorizontalScrolling=false] - Whether to allow horizontal scrolling in the popup + * @property {boolean?} [allowVerticalScrolling=false] - Whether to allow vertical scrolling in the popup + * @property {POPUP_RESULT|number?} [defaultResult=POPUP_RESULT.AFFIRMATIVE] - The default result of this popup when Enter is pressed. Can be changed from `POPUP_RESULT.AFFIRMATIVE`. + * @property {CustomPopupButton[]|string[]?} [customButtons=null] - Custom buttons to add to the popup. If only strings are provided, the buttons will be added with default options, and their result will be in order from `2` onward. + * @property {(popup: Popup) => boolean?} [onClosing=null] - Handler called before the popup closes, return `false` to cancel the close + * @property {(popup: Popup) => void?} [onClose=null] - Handler called after the popup closes, but before the DOM is cleaned up */ /** @@ -75,8 +81,9 @@ export class Popup { /** @type {HTMLElement} */ content; /** @type {HTMLTextAreaElement} */ input; /** @type {HTMLElement} */ controls; - /** @type {HTMLElement} */ ok; - /** @type {HTMLElement} */ cancel; + /** @type {HTMLElement} */ okButton; + /** @type {HTMLElement} */ cancelButton; + /** @type {HTMLElement} */ closeButton; /** @type {POPUP_RESULT|number?} */ defaultResult; /** @type {CustomPopupButton[]|string[]?} */ customButtons; @@ -99,7 +106,7 @@ export class Popup { * @param {string} [inputValue=''] - The initial value of the input field * @param {PopupOptions} [options={}] - Additional options for the popup */ - constructor(content, type, inputValue = '', { okButton = null, cancelButton = null, rows = 1, wide = false, wider = false, large = false, allowHorizontalScrolling = false, allowVerticalScrolling = false, defaultResult = POPUP_RESULT.AFFIRMATIVE, customButtons = null, onClosing = null, onClose = null } = {}) { + constructor(content, type, inputValue = '', { okButton = null, cancelButton = null, rows = 1, wide = false, wider = false, large = false, transparent = false, allowHorizontalScrolling = false, allowVerticalScrolling = false, defaultResult = POPUP_RESULT.AFFIRMATIVE, customButtons = null, onClosing = null, onClose = null } = {}) { Popup.util.popups.push(this); // Make this popup uniquely identifiable @@ -118,19 +125,21 @@ export class Popup { this.content = this.dlg.querySelector('.popup-content'); this.input = this.dlg.querySelector('.popup-input'); this.controls = this.dlg.querySelector('.popup-controls'); - this.ok = this.dlg.querySelector('.popup-button-ok'); - this.cancel = this.dlg.querySelector('.popup-button-cancel'); + this.okButton = this.dlg.querySelector('.popup-button-ok'); + this.cancelButton = this.dlg.querySelector('.popup-button-cancel'); + this.closeButton = this.dlg.querySelector('.popup-button-close'); this.dlg.setAttribute('data-id', this.id); if (wide) this.dlg.classList.add('wide_dialogue_popup'); if (wider) this.dlg.classList.add('wider_dialogue_popup'); if (large) this.dlg.classList.add('large_dialogue_popup'); + if (transparent) this.dlg.classList.add('transparent_dialogue_popup'); if (allowHorizontalScrolling) this.dlg.classList.add('horizontal_scrolling_dialogue_popup'); if (allowVerticalScrolling) this.dlg.classList.add('vertical_scrolling_dialogue_popup'); // If custom button captions are provided, we set them beforehand - this.ok.textContent = typeof okButton === 'string' ? okButton : 'OK'; - this.cancel.textContent = typeof cancelButton === 'string' ? cancelButton : template.getAttribute('popup-button-cancel'); + this.okButton.textContent = typeof okButton === 'string' ? okButton : 'OK'; + this.cancelButton.textContent = typeof cancelButton === 'string' ? cancelButton : template.getAttribute('popup-button-cancel'); this.defaultResult = defaultResult; this.customButtons = customButtons; @@ -141,17 +150,14 @@ export class Popup { const buttonElement = document.createElement('div'); buttonElement.classList.add('menu_button', 'popup-button-custom', 'result-control'); buttonElement.classList.add(...(button.classes ?? [])); - buttonElement.setAttribute('data-result', String(button.result ?? undefined)); + buttonElement.dataset.result = String(button.result ?? undefined); buttonElement.textContent = button.text; buttonElement.tabIndex = 0; - if (button.action) buttonElement.addEventListener('click', button.action); - if (button.result) buttonElement.addEventListener('click', () => this.complete(button.result)); - if (button.appendAtEnd) { this.controls.appendChild(buttonElement); } else { - this.controls.insertBefore(buttonElement, this.ok); + this.controls.insertBefore(buttonElement, this.okButton); } }); @@ -159,23 +165,30 @@ export class Popup { const defaultButton = this.controls.querySelector(`[data-result="${this.defaultResult}"]`); if (defaultButton) defaultButton.classList.add('menu_button_default'); + // Styling differences depending on the popup type + // General styling for all types first, that might be overriden for specific types below + this.input.style.display = 'none'; + this.closeButton.style.display = 'none'; + switch (type) { case POPUP_TYPE.TEXT: { - this.input.style.display = 'none'; - if (!cancelButton) this.cancel.style.display = 'none'; + if (!cancelButton) this.cancelButton.style.display = 'none'; break; } case POPUP_TYPE.CONFIRM: { - this.input.style.display = 'none'; - if (!okButton) this.ok.textContent = template.getAttribute('popup-button-yes'); - if (!cancelButton) this.cancel.textContent = template.getAttribute('popup-button-no'); + if (!okButton) this.okButton.textContent = template.getAttribute('popup-button-yes'); + if (!cancelButton) this.cancelButton.textContent = template.getAttribute('popup-button-no'); break; } case POPUP_TYPE.INPUT: { this.input.style.display = 'block'; - if (!okButton) this.ok.textContent = template.getAttribute('popup-button-save'); + if (!okButton) this.okButton.textContent = template.getAttribute('popup-button-save'); break; } + case POPUP_TYPE.DISPLAY: { + this.controls.style.display = 'none'; + this.closeButton.style.display = 'block'; + } default: { console.warn('Unknown popup type.', type); break; @@ -202,8 +215,14 @@ export class Popup { // Set focus event that remembers the focused element this.dlg.addEventListener('focusin', (evt) => { if (evt.target instanceof HTMLElement && evt.target != this.dlg) this.lastFocus = evt.target; }); - this.ok.addEventListener('click', () => this.complete(POPUP_RESULT.AFFIRMATIVE)); - this.cancel.addEventListener('click', () => this.complete(POPUP_RESULT.NEGATIVE)); + // Bind event listeners for all result controls to their defined event type + this.dlg.querySelectorAll(`[data-result]`).forEach(resultControl => { + if (!(resultControl instanceof HTMLElement)) return; + const result = Number(resultControl.dataset.result); + if (isNaN(result)) throw new Error('Invalid result control. Result must be a number. ' + resultControl.dataset.result); + const type = resultControl.dataset.resultEvent || 'click'; + resultControl.addEventListener(type, () => this.complete(result)); + }); // Bind dialog listeners manually, so we can be sure context is preserved const cancelListener = (evt) => { @@ -296,6 +315,9 @@ export class Popup { if (applyAutoFocus) { control.setAttribute('autofocus', ''); + // Manually enable tabindex too, as this might only be applied by the interactable functionality in the background, but too late for HTML autofocus + // interactable only gets applied when inserted into the DOM + control.tabIndex = 0; } else { control.focus(); } diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index bc43a8ca0..bfb2be120 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -46,7 +46,8 @@ import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandPa import { SlashCommand } from './slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js'; import { AUTOCOMPLETE_WIDTH } from './autocomplete/AutoComplete.js'; -import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js'; +import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js'; +import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; export { loadPowerUserSettings, @@ -2917,7 +2918,7 @@ function setAvgBG() { return `rgba(${rNew.toFixed(0)}, ${gNew.toFixed(0)}, ${bNew.toFixed(0)}, 1)`; } - + return ''; } async function setThemeCallback(_, text) { @@ -3922,9 +3923,11 @@ $(document).ready(() => { name: 'random', callback: doRandomChat, unnamedArgumentList: [ - new SlashCommandArgument( - 'optional tag name', [ARGUMENT_TYPE.STRING], false, - ), + SlashCommandArgument.fromProps({ + description: 'optional tag name', + typeList: [ARGUMENT_TYPE.STRING], + enumProvider: () => tags.filter(tag => Object.values(tag_map).some(x => x.includes(tag.id))).map(tag => new SlashCommandEnumValue(tag.name, null, enumTypes.enum, enumIcons.tag)), + }), ], helpString: 'Start a new chat with a random character. If an argument is provided, only considers characters that have the specified tag.', })); @@ -3944,9 +3947,13 @@ $(document).ready(() => { callback: doMesCut, returns: 'the text of cut messages separated by a newline', unnamedArgumentList: [ - new SlashCommandArgument( - 'number or range', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE], true, - ), + SlashCommandArgument.fromProps({ + description: 'number or range', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE], + isRequired: true, + acceptsMultiple: true, + enumProvider: commonEnumProviders.messages(), + }), ], helpString: `
diff --git a/public/scripts/preset-manager.js b/public/scripts/preset-manager.js index d751c5803..9043e8a91 100644 --- a/public/scripts/preset-manager.js +++ b/public/scripts/preset-manager.js @@ -22,6 +22,8 @@ import { kai_settings } from './kai-settings.js'; import { context_presets, getContextSettings, power_user } from './power-user.js'; import { SlashCommand } from './slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js'; +import { enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; +import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js'; import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { textgenerationwebui_preset_names, @@ -482,11 +484,12 @@ export async function initPresetManager() { SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'preset', callback: presetCommandCallback, returns: 'current preset', - namedArgumentList: [], unnamedArgumentList: [ - new SlashCommandArgument( - 'name', [ARGUMENT_TYPE.STRING], false, - ), + SlashCommandArgument.fromProps({ + description: 'name', + typeList: [ARGUMENT_TYPE.STRING], + enumProvider: () => getPresetManager().getAllPresets().map(preset => new SlashCommandEnumValue(preset, null, enumTypes.enum, enumIcons.preset)), + }), ], helpString: `
diff --git a/public/scripts/scrapers.js b/public/scripts/scrapers.js index aa34eb3a0..86d2ba567 100644 --- a/public/scripts/scrapers.js +++ b/public/scripts/scrapers.js @@ -433,6 +433,24 @@ class FandomScraper { } } +const iso6391Codes = [ + 'aa', 'ab', 'ae', 'af', 'ak', 'am', 'an', 'ar', 'as', 'av', 'ay', 'az', + 'ba', 'be', 'bg', 'bh', 'bi', 'bm', 'bn', 'bo', 'br', 'bs', 'ca', 'ce', + 'ch', 'co', 'cr', 'cs', 'cu', 'cv', 'cy', 'da', 'de', 'dv', 'dz', 'ee', + 'el', 'en', 'eo', 'es', 'et', 'eu', 'fa', 'ff', 'fi', 'fj', 'fo', 'fr', + 'fy', 'ga', 'gd', 'gl', 'gn', 'gu', 'gv', 'ha', 'he', 'hi', 'ho', 'hr', + 'ht', 'hu', 'hy', 'hz', 'ia', 'id', 'ie', 'ig', 'ii', 'ik', 'io', 'is', + 'it', 'iu', 'ja', 'jv', 'ka', 'kg', 'ki', 'kj', 'kk', 'kl', 'km', 'kn', + 'ko', 'kr', 'ks', 'ku', 'kv', 'kw', 'ky', 'la', 'lb', 'lg', 'li', 'ln', + 'lo', 'lt', 'lu', 'lv', 'mg', 'mh', 'mi', 'mk', 'ml', 'mn', 'mr', 'ms', + 'mt', 'my', 'na', 'nb', 'nd', 'ne', 'ng', 'nl', 'nn', 'no', 'nr', 'nv', + 'ny', 'oc', 'oj', 'om', 'or', 'os', 'pa', 'pi', 'pl', 'ps', 'pt', 'qu', + 'rm', 'rn', 'ro', 'ru', 'rw', 'sa', 'sc', 'sd', 'se', 'sg', 'si', 'sk', + 'sl', 'sm', 'sn', 'so', 'sq', 'sr', 'ss', 'st', 'su', 'sv', 'sw', 'ta', + 'te', 'tg', 'th', 'ti', 'tk', 'tl', 'tn', 'to', 'tr', 'ts', 'tt', 'tw', + 'ty', 'ug', 'uk', 'ur', 'uz', 've', 'vi', 'vo', 'wa', 'wo', 'xh', 'yi', + 'yo', 'za', 'zh', 'zu']; + /** * Scrape transcript from a YouTube video. * @implements {Scraper} @@ -464,7 +482,7 @@ class YouTubeScraper { helpString: 'Scrape a transcript from a YouTube video by ID or URL.', returns: ARGUMENT_TYPE.STRING, namedArgumentList: [ - new SlashCommandNamedArgument('lang', 'ISO 639-1 language code of the transcript, e.g. "en"', ARGUMENT_TYPE.STRING, false, false, ''), + new SlashCommandNamedArgument('lang', 'ISO 639-1 language code of the transcript, e.g. "en"', ARGUMENT_TYPE.STRING, false, false, '', iso6391Codes), ], unnamedArgumentList: [ new SlashCommandArgument('URL or ID of the YouTube video', ARGUMENT_TYPE.STRING, true, false), @@ -514,7 +532,7 @@ class YouTubeScraper { } const toast = toastr.info('Working, please wait...'); - const { transcript, id } = await this.getScript(videoUrl, lang); + const { transcript, id } = await this.getScript(String(videoUrl), lang); toastr.clear(toast); const file = new File([transcript], `YouTube - ${id} - ${Date.now()}.txt`, { type: 'text/plain' }); diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 3672a954d..a48805863 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -42,9 +42,9 @@ import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandPa import { SlashCommandParserError } from './slash-commands/SlashCommandParserError.js'; import { getMessageTimeStamp } from './RossAscends-mods.js'; import { hideChatMessageRange } from './chats.js'; -import { getContext, saveMetadataDebounced } from './extensions.js'; +import { extension_settings, getContext, saveMetadataDebounced } from './extensions.js'; import { getRegexedString, regex_placement } from './extensions/regex/engine.js'; -import { findGroupMemberId, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group } from './group-chats.js'; +import { findGroupMemberId, getGroupMembers, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group } from './group-chats.js'; import { chat_completion_sources, oai_settings, setupChatCompletionPromptManager } from './openai.js'; import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, user_avatar } from './personas.js'; import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js'; @@ -61,8 +61,9 @@ import { AutoComplete } from './autocomplete/AutoComplete.js'; import { SlashCommand } from './slash-commands/SlashCommand.js'; import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js'; import { SlashCommandNamedArgumentAssignment } from './slash-commands/SlashCommandNamedArgumentAssignment.js'; -import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js'; +import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js'; import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; +import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; export { executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand, }; @@ -78,9 +79,16 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: '?', callback: helpCommandCallback, aliases: ['help'], - unnamedArgumentList: [new SlashCommandArgument( - 'help topic', ARGUMENT_TYPE.STRING, false, false, null, ['slash', 'format', 'hotkeys', 'macros'], - )], + unnamedArgumentList: [SlashCommandArgument.fromProps({ + description: 'help topic', + typeList: [ARGUMENT_TYPE.STRING], + enumList: [ + new SlashCommandEnumValue('slash', 'slash commands (STscript)', enumTypes.command, '/'), + new SlashCommandEnumValue('macros', '{{macros}} (text replacement)', enumTypes.macro, enumIcons.macro), + new SlashCommandEnumValue('format', 'chat/text formatting', enumTypes.name, '★'), + new SlashCommandEnumValue('hotkeys', 'keyboard shortcuts', enumTypes.enum, '⏎'), + ], + })], helpString: 'Get help on macros, chat formatting and commands.', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ @@ -93,9 +101,12 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ ), ], unnamedArgumentList: [ - new SlashCommandArgument( - 'persona name', [ARGUMENT_TYPE.STRING], true, - ), + SlashCommandArgument.fromProps({ + description: 'persona name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: commonEnumProviders.personas, + }), ], helpString: 'Selects the given persona with its name and avatar (by name or avatar url). If no matching persona exists, applies a temporary name.', aliases: ['name'], @@ -122,9 +133,8 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ typeList: [ARGUMENT_TYPE.STRING], isRequired: true, enumProvider: () => [...document.querySelectorAll('.bg_example')] - .map((it) => new SlashCommandEnumValue(it.getAttribute('bgfile'))) - .filter(it => it.value?.length) - , + .map(it => new SlashCommandEnumValue(it.getAttribute('bgfile'))) + .filter(it => it.value?.length), }), ], helpString: ` @@ -145,16 +155,22 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'sendas', callback: sendMessageAs, namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'name', + description: 'Character name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: commonEnumProviders.characters('character'), + forceEnum: false, + }), new SlashCommandNamedArgument( - 'name', 'Character name', [ARGUMENT_TYPE.STRING], true, - ), - new SlashCommandNamedArgument( - 'compact', 'Use compact layout', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', ['true', 'false'], + 'compact', 'Use compact layout', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', ), SlashCommandNamedArgument.fromProps({ name: 'at', description: 'position to insert the message', typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], + enumProvider: commonEnumProviders.messages({ allowIdAfter: true, allowVars: true }), }), ], unnamedArgumentList: [ @@ -197,6 +213,7 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'at', description: 'position to insert the message', typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], + enumProvider: commonEnumProviders.messages({ allowIdAfter: true, allowVars: true }), }), ], unnamedArgumentList: [ @@ -250,6 +267,7 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'at', description: 'position to insert the message', typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], + enumProvider: commonEnumProviders.messages({ allowIdAfter: true, allowVars: true }), }), ], unnamedArgumentList: [ @@ -333,10 +351,7 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ description: 'name', typeList: [ARGUMENT_TYPE.STRING], isRequired: true, - enumProvider: () => [ - ...characters.map(it => new SlashCommandEnumValue(it.name, null, 'qr', 'C')), - ...groups.map(it => new SlashCommandEnumValue(it.name, null, 'variable', 'G')), - ], + enumProvider: commonEnumProviders.characters('all'), }), ], helpString: 'Opens up a chat with the character or group by its name', @@ -384,7 +399,7 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ description: 'character name', typeList: [ARGUMENT_TYPE.STRING], isRequired: true, - enumProvider: () => characters.map(it => new SlashCommandEnumValue(it.name, null, 'qr', 'C')), + enumProvider: commonEnumProviders.characters('character'), }), ], unnamedArgumentList: [ @@ -399,9 +414,12 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ callback: deleteMessagesByNameCallback, namedArgumentList: [], unnamedArgumentList: [ - new SlashCommandArgument( - 'name', [ARGUMENT_TYPE.STRING], true, - ), + SlashCommandArgument.fromProps({ + description: 'name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: commonEnumProviders.characters('character'), + }), ], aliases: ['cancel'], helpString: ` @@ -434,15 +452,18 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'at', description: 'position to insert the message', typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], + enumProvider: commonEnumProviders.messages({ allowIdAfter: true, allowVars: true }), + }), + SlashCommandNamedArgument.fromProps({ + name: 'name', + description: 'display name', + typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.VARIABLE_NAME], + defaultValue: '{{user}}', + enumProvider: () => [ + ...commonEnumProviders.characters('character')(), + ...commonEnumProviders.variables('all')().map(x => { x.description = 'Variable'; return x; }), + ], }), - new SlashCommandNamedArgument( - 'name', - 'display name', - [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.VARIABLE_NAME], - false, - false, - '{{user}}', - ), ], unnamedArgumentList: [ new SlashCommandArgument( @@ -487,6 +508,14 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ 'false', ), ], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'group member index (starts with 0) or name', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], + isRequired: false, + enumProvider: commonEnumProviders.groupMembers(), + }), + ], helpString: `
Triggers a message generation. If in group, can trigger a message for the specified group member index or name. @@ -500,9 +529,12 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'hide', callback: hideMessageCallback, unnamedArgumentList: [ - new SlashCommandArgument( - 'message index or range', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE], true, - ), + SlashCommandArgument.fromProps({ + description: 'message index (starts with 0) or range', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE], + isRequired: true, + enumProvider: commonEnumProviders.messages(), + }), ], helpString: 'Hides a chat message from the prompt.', })); @@ -510,9 +542,12 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'unhide', callback: unhideMessageCallback, unnamedArgumentList: [ - new SlashCommandArgument( - 'message index or range', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE], true, - ), + SlashCommandArgument.fromProps({ + description: 'message index (starts with 0) or range', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE], + isRequired: true, + enumProvider: commonEnumProviders.messages(), + }), ], helpString: 'Unhides a message from the prompt.', })); @@ -521,9 +556,12 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ callback: disableGroupMemberCallback, aliases: ['disable', 'disablemember', 'memberdisable'], unnamedArgumentList: [ - new SlashCommandArgument( - 'member index or name', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], true, - ), + SlashCommandArgument.fromProps({ + description: 'member index (starts with 0) or name', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: commonEnumProviders.groupMembers(), + }), ], helpString: 'Disables a group member from being drafted for replies.', })); @@ -532,9 +570,12 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ aliases: ['enable', 'enablemember', 'memberenable'], callback: enableGroupMemberCallback, unnamedArgumentList: [ - new SlashCommandArgument( - 'member index or name', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], true, - ), + SlashCommandArgument.fromProps({ + description: 'member index (starts with 0) or name', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: commonEnumProviders.groupMembers(), + }), ], helpString: 'Enables a group member to be drafted for replies.', })); @@ -543,9 +584,12 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ callback: addGroupMemberCallback, aliases: ['addmember', 'memberadd'], unnamedArgumentList: [ - new SlashCommandArgument( - 'character name', [ARGUMENT_TYPE.STRING], true, - ), + SlashCommandArgument.fromProps({ + description: 'character name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: () => selected_group ? commonEnumProviders.characters('character')() : [], + }), ], helpString: `
@@ -566,9 +610,12 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ callback: removeGroupMemberCallback, aliases: ['removemember', 'memberremove'], unnamedArgumentList: [ - new SlashCommandArgument( - 'member index or name', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], true, - ), + SlashCommandArgument.fromProps({ + description: 'member index (starts with 0) or name', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: commonEnumProviders.groupMembers(), + }), ], helpString: `
@@ -590,9 +637,12 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ callback: moveGroupMemberUpCallback, aliases: ['upmember', 'memberup'], unnamedArgumentList: [ - new SlashCommandArgument( - 'member index or name', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], true, - ), + SlashCommandArgument.fromProps({ + description: 'member index (starts with 0) or name', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: commonEnumProviders.groupMembers(), + }), ], helpString: 'Moves a group member up in the group chat list.', })); @@ -601,9 +651,12 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ callback: moveGroupMemberDownCallback, aliases: ['downmember', 'memberdown'], unnamedArgumentList: [ - new SlashCommandArgument( - 'member index or name', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], true, - ), + SlashCommandArgument.fromProps({ + description: 'member index (starts with 0) or name', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: commonEnumProviders.groupMembers(), + }), ], helpString: 'Moves a group member down in the group chat list.', })); @@ -611,9 +664,12 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'peek', callback: peekCallback, unnamedArgumentList: [ - new SlashCommandArgument( - 'message index or range', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE], false, true, - ), + SlashCommandArgument.fromProps({ + description: 'member index (starts with 0) or name', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: commonEnumProviders.groupMembers(), + }), ], helpString: `
@@ -623,12 +679,8 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ Examples:
  • -
    /peek 5
    - Shows the character card for the 5th message. -
  • -
  • -
    /peek 2-5
    - Shows the character cards for messages 2 through 5. +
    /peek Gloria
    + Shows the character card for the character named "Gloria".
@@ -639,9 +691,14 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ callback: deleteSwipeCallback, aliases: ['swipedel'], unnamedArgumentList: [ - new SlashCommandArgument( - '1-based swipe id', [ARGUMENT_TYPE.NUMBER], - ), + SlashCommandArgument.fromProps({ + description: '1-based swipe id', + typeList: [ARGUMENT_TYPE.NUMBER], + isRequired: true, + enumProvider: () => Array.isArray(chat[chat.length - 1]?.swipes) ? + chat[chat.length - 1].swipes.map((/** @type {string} */ swipe, /** @type {number} */ i) => new SlashCommandEnumValue(String(i + 1), swipe, enumTypes.enum, enumIcons.message)) + : [], + }), ], helpString: `
@@ -670,9 +727,18 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ new SlashCommandNamedArgument( 'title', 'title of the toast message', [ARGUMENT_TYPE.STRING], false, ), - new SlashCommandNamedArgument( - 'severity', 'severity level of the toast message', [ARGUMENT_TYPE.STRING], false, false, null, ['info', 'warning', 'error', 'success'], - ), + SlashCommandNamedArgument.fromProps({ + name: 'severity', + description: 'severity level of the toast message', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: 'info', + enumProvider: () => [ + new SlashCommandEnumValue('info', 'info', enumTypes.macro, 'ℹ️'), + new SlashCommandEnumValue('warning', 'warning', enumTypes.enum, '⚠️'), + new SlashCommandEnumValue('error', 'error', enumTypes.enum, '❗'), + new SlashCommandEnumValue('success', 'success', enumTypes.enum, '✅'), + ], + }), ], unnamedArgumentList: [ new SlashCommandArgument( @@ -699,17 +765,28 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ returns: 'generated text', namedArgumentList: [ new SlashCommandNamedArgument( - 'lock', 'lock user input during generation', [ARGUMENT_TYPE.BOOLEAN], false, false, null, ['on', 'off'], - ), - new SlashCommandNamedArgument( - 'name', 'in-prompt name for instruct mode', [ARGUMENT_TYPE.STRING], false, false, 'System', + 'lock', 'lock user input during generation', [ARGUMENT_TYPE.BOOLEAN], false, false, null, commonEnumProviders.boolean('onOff')(), ), + SlashCommandNamedArgument.fromProps({ + name: 'name', + description: 'in-prompt name for instruct mode', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: 'System', + enumProvider: () => [...commonEnumProviders.characters('character')(), new SlashCommandEnumValue('System', null, enumTypes.enum, enumIcons.assistant)], + forceEnum: false, + }), new SlashCommandNamedArgument( 'length', 'API response length in tokens', [ARGUMENT_TYPE.NUMBER], false, ), - new SlashCommandNamedArgument( - 'as', 'role of the output prompt', [ARGUMENT_TYPE.STRING], false, false, 'system', ['system', 'char'], - ), + SlashCommandNamedArgument.fromProps({ + name: 'as', + description: 'role of the output prompt', + typeList: [ARGUMENT_TYPE.STRING], + enumList: [ + new SlashCommandEnumValue('system', null, enumTypes.enum, enumIcons.assistant), + new SlashCommandEnumValue('char', null, enumTypes.enum, enumIcons.character), + ], + }), ], unnamedArgumentList: [ new SlashCommandArgument( @@ -731,17 +808,23 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ returns: 'generated text', namedArgumentList: [ new SlashCommandNamedArgument( - 'lock', 'lock user input during generation', [ARGUMENT_TYPE.BOOLEAN], false, false, null, ['on', 'off'], + 'lock', 'lock user input during generation', [ARGUMENT_TYPE.BOOLEAN], false, false, null, commonEnumProviders.boolean('onOff')(), ), new SlashCommandNamedArgument( - 'instruct', 'use instruct mode', [ARGUMENT_TYPE.BOOLEAN], false, false, 'on', ['on', 'off'], + 'instruct', 'use instruct mode', [ARGUMENT_TYPE.BOOLEAN], false, false, 'on', commonEnumProviders.boolean('onOff')(), ), new SlashCommandNamedArgument( 'stop', 'one-time custom stop strings', [ARGUMENT_TYPE.LIST], false, ), - new SlashCommandNamedArgument( - 'as', 'role of the output prompt', [ARGUMENT_TYPE.STRING], false, false, 'system', ['system', 'char'], - ), + SlashCommandNamedArgument.fromProps({ + name: 'as', + description: 'role of the output prompt', + typeList: [ARGUMENT_TYPE.STRING], + enumList: [ + new SlashCommandEnumValue('system', null, enumTypes.enum, enumIcons.assistant), + new SlashCommandEnumValue('char', null, enumTypes.enum, enumIcons.character), + ], + }), new SlashCommandNamedArgument( 'system', 'system prompt at the start', [ARGUMENT_TYPE.STRING], false, ), @@ -792,7 +875,6 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ description: 'Whether to suppress the toast message notifying about the /abort call.', typeList: [ARGUMENT_TYPE.BOOLEAN], defaultValue: 'true', - enumList: ['true', 'false'], }), ], unnamedArgumentList: [ @@ -845,7 +927,12 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'pass', - callback: (_, arg) => arg, + callback: (_, arg) => { + // We do not support arrays of closures. Arrays of strings will be send as JSON + if (Array.isArray(arg) && arg.some(x => x instanceof SlashCommandClosure)) throw new Error('Command /pass does not support multiple closures'); + if (Array.isArray(arg)) return JSON.stringify(arg); + return arg; + }, returns: 'the provided value', unnamedArgumentList: [ new SlashCommandArgument( @@ -898,10 +985,10 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ 'default', 'default value of the input field', [ARGUMENT_TYPE.STRING], false, false, '"string"', ), new SlashCommandNamedArgument( - 'large', 'show large input field', [ARGUMENT_TYPE.BOOLEAN], false, false, 'off', ['on', 'off'], + 'large', 'show large input field', [ARGUMENT_TYPE.BOOLEAN], false, false, 'off', commonEnumProviders.boolean('onOff')(), ), new SlashCommandNamedArgument( - 'wide', 'show wide input field', [ARGUMENT_TYPE.BOOLEAN], false, false, 'off', ['on', 'off'], + 'wide', 'show wide input field', [ARGUMENT_TYPE.BOOLEAN], false, false, 'off', commonEnumProviders.boolean('onOff')(), ), new SlashCommandNamedArgument( 'okButton', 'text for the ok button', [ARGUMENT_TYPE.STRING], false, @@ -933,9 +1020,15 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ ), ], unnamedArgumentList: [ - new SlashCommandArgument( - 'scoped variable or qr label', [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING], true, - ), + SlashCommandArgument.fromProps({ + description: 'scoped variable or qr label', + typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: () => [ + ...commonEnumProviders.variables('scope')(), + ...(typeof window['qrEnumProviderExecutables'] === 'function') ? window['qrEnumProviderExecutables']() : [], + ], + }), ], helpString: `
@@ -950,19 +1043,29 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ aliases: ['message'], namedArgumentList: [ new SlashCommandNamedArgument( - 'names', 'show message author names', [ARGUMENT_TYPE.BOOLEAN], false, false, 'off', ['off', 'on'], + 'names', 'show message author names', [ARGUMENT_TYPE.BOOLEAN], false, false, 'off', commonEnumProviders.boolean('onOff')(), ), new SlashCommandNamedArgument( - 'hidden', 'include hidden messages', [ARGUMENT_TYPE.BOOLEAN], false, false, 'on', ['off', 'on'], - ), - new SlashCommandNamedArgument( - 'role', 'filter messages by role', [ARGUMENT_TYPE.STRING], false, false, null, ['system', 'assistant', 'user'], + 'hidden', 'include hidden messages', [ARGUMENT_TYPE.BOOLEAN], false, false, 'on', commonEnumProviders.boolean('onOff')(), ), + SlashCommandNamedArgument.fromProps({ + name: 'role', + description: 'filter messages by role', + typeList: [ARGUMENT_TYPE.STRING], + enumList: [ + new SlashCommandEnumValue('system', null, enumTypes.enum, enumIcons.system), + new SlashCommandEnumValue('assistant', null, enumTypes.enum, enumIcons.assistant), + new SlashCommandEnumValue('user', null, enumTypes.enum, enumIcons.user), + ], + }), ], unnamedArgumentList: [ - new SlashCommandArgument( - 'message index or range', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE], false, true, - ), + SlashCommandArgument.fromProps({ + description: 'message index (starts with 0) or range', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE], + isRequired: true, + enumProvider: commonEnumProviders.messages(), + }), ], returns: 'the specified message or range of messages as a string', helpString: ` @@ -1018,10 +1121,10 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ returns: 'popup text', namedArgumentList: [ new SlashCommandNamedArgument( - 'large', 'show large popup', [ARGUMENT_TYPE.BOOLEAN], false, false, null, ['on', 'off'], + 'large', 'show large popup', [ARGUMENT_TYPE.BOOLEAN], false, false, null, commonEnumProviders.boolean('onOff')(), ), new SlashCommandNamedArgument( - 'wide', 'show wide popup', [ARGUMENT_TYPE.BOOLEAN], false, false, null, ['on', 'off'], + 'wide', 'show wide popup', [ARGUMENT_TYPE.BOOLEAN], false, false, null, commonEnumProviders.boolean('onOff')(), ), new SlashCommandNamedArgument( 'okButton', 'text for the OK button', [ARGUMENT_TYPE.STRING], false, @@ -1084,9 +1187,16 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ new SlashCommandNamedArgument( 'limit', 'number of tokens to keep', [ARGUMENT_TYPE.NUMBER], true, ), - new SlashCommandNamedArgument( - 'direction', 'trim direction', [ARGUMENT_TYPE.STRING], true, false, null, ['start', 'end'], - ), + SlashCommandNamedArgument.fromProps({ + name: 'direction', + description: 'trim direction', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumList: [ + new SlashCommandEnumValue('start', null, enumTypes.enum, '⏪'), + new SlashCommandEnumValue('end', null, enumTypes.enum, '⏩'), + ], + }), ], unnamedArgumentList: [ new SlashCommandArgument( @@ -1145,9 +1255,17 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'inject', callback: injectCallback, namedArgumentList: [ - new SlashCommandNamedArgument( - 'id', 'injection ID', [ARGUMENT_TYPE.STRING], true, - ), + SlashCommandNamedArgument.fromProps({ + name: 'id', + description: 'injection ID or variable name pointing to ID', + typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: true, + + enumProvider: () => [ + ...commonEnumProviders.injects(), + ...commonEnumProviders.variables('all')().map(x => { x.description = 'Variable'; return x; }), + ], + }), new SlashCommandNamedArgument( 'position', 'injection position', [ARGUMENT_TYPE.STRING], false, false, 'after', ['before', 'after', 'chat'], ), @@ -1157,11 +1275,19 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ new SlashCommandNamedArgument( 'scan', 'include injection content into World Info scans', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', ), + SlashCommandNamedArgument.fromProps({ + name: 'role', + description: 'role for in-chat injections', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: false, + enumList: [ + new SlashCommandEnumValue('system', null, enumTypes.enum, enumIcons.system), + new SlashCommandEnumValue('assistant', null, enumTypes.enum, enumIcons.assistant), + new SlashCommandEnumValue('user', null, enumTypes.enum, enumIcons.user), + ], + }), new SlashCommandNamedArgument( - 'role', 'role for in-chat injections', [ARGUMENT_TYPE.STRING], false, false, 'system', ['system', 'user', 'assistant'], - ), - new SlashCommandNamedArgument( - 'ephemeral', 'remove injection after generation', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', ['true', 'false'], + 'ephemeral', 'remove injection after generation', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', ), ], unnamedArgumentList: [ @@ -1180,16 +1306,25 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'flushinject', aliases: ['flushinjects'], unnamedArgumentList: [ - new SlashCommandArgument( - 'injection ID or a variable name pointing to ID', [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.VARIABLE_NAME], false, false, '', - ), + SlashCommandArgument.fromProps({ + description: 'injection ID or a variable name pointing to ID', + typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.VARIABLE_NAME], + defaultValue: '', + enumProvider: () => [ + ...commonEnumProviders.injects(), + ...commonEnumProviders.variables('all')().map(x => { x.description = 'Variable'; return x; }), + ], + }), ], callback: flushInjectsCallback, helpString: 'Removes a script injection for the current chat. If no ID is provided, removes all script injections.', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'tokens', - callback: (_, text) => getTokenCountAsync(text), + callback: (_, text) => { + if (text instanceof SlashCommandClosure || Array.isArray(text)) throw new Error('Unnamed argument cannot be a closure for command /tokens'); + return getTokenCountAsync(text).then(count => String(count)); + }, returns: 'number of tokens', unnamedArgumentList: [ new SlashCommandArgument( @@ -1203,9 +1338,11 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ callback: modelCallback, returns: 'current model', unnamedArgumentList: [ - new SlashCommandArgument( - 'model name', [ARGUMENT_TYPE.STRING], false, - ), + SlashCommandArgument.fromProps({ + description: 'model name', + typeList: [ARGUMENT_TYPE.STRING], + enumProvider: () => getModelOptions()?.options.map(option => new SlashCommandEnumValue(option.value, option.value !== option.text ? option.text : null)), + }), ], helpString: 'Sets the model for the current API. Gets the current model name if no argument is provided.', })); @@ -1214,12 +1351,28 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ aliases: ['setpromptentries'], callback: setPromptEntryCallback, namedArgumentList: [ - new SlashCommandNamedArgument( - 'identifier', 'Prompt entry identifier(s) to target', [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.LIST], false, true, - ), - new SlashCommandNamedArgument( - 'name', 'Prompt entry name(s) to target', [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.LIST], false, true, - ), + SlashCommandNamedArgument.fromProps({ + name: 'identifier', + description: 'Prompt entry identifier(s) to target', + typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.LIST], + acceptsMultiple: true, + enumProvider: () => { + const promptManager = setupChatCompletionPromptManager(oai_settings); + const prompts = promptManager.serviceSettings.prompts; + return prompts.map(prompt => new SlashCommandEnumValue(prompt.identifier, prompt.name, enumTypes.enum)); + }, + }), + SlashCommandNamedArgument.fromProps({ + name: 'name', + description: 'Prompt entry name(s) to target', + typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.LIST], + acceptsMultiple: true, + enumProvider: () => { + const promptManager = setupChatCompletionPromptManager(oai_settings); + const prompts = promptManager.serviceSettings.prompts; + return prompts.map(prompt => new SlashCommandEnumValue(prompt.name, prompt.identifier, enumTypes.enum)); + }, + }), ], unnamedArgumentList: [ SlashCommandArgument.fromProps({ @@ -1228,7 +1381,7 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ isRequired: true, acceptsMultiple: false, defaultValue: 'toggle', // unnamed arguments don't support default values yet - enumList: ['on', 'off', 'toggle'], + enumList: commonEnumProviders.boolean('onOffToggle')(), }), ], helpString: 'Sets the specified prompt manager entry/entries on or off.', @@ -1505,7 +1658,7 @@ async function popupCallback(args, value) { await delay(1); await callGenericPopup(safeValue, POPUP_TYPE.TEXT, '', popupOptions); await delay(1); - return value; + return String(value); } function getMessagesCallback(args, value) { @@ -1609,13 +1762,13 @@ async function runCallback(args, name) { /** * - * @param {object} param0 - * @param {SlashCommandAbortController} param0._abortController - * @param {string} [param0.quiet] + * @param {import('./slash-commands/SlashCommand.js').NamedArguments} param0 * @param {string} [reason] */ function abortCallback({ _abortController, quiet }, reason) { + if (quiet instanceof SlashCommandClosure) throw new Error('argument \'quiet\' cannot be a closure for command /abort'); _abortController.abort((reason ?? '').toString().length == 0 ? '/abort command executed' : reason, !isFalseBoolean(quiet ?? 'true')); + return ''; } async function delayCallback(_, amount) { @@ -1644,9 +1797,9 @@ async function inputCallback(args, prompt) { }; // Do not remove this delay, otherwise the prompt will not show up await delay(1); - const result = await callPopup(safeValue, 'input', defaultInput, popupOptions); + const result = await callGenericPopup(safeValue, POPUP_TYPE.INPUT, defaultInput, popupOptions); await delay(1); - return result || ''; + return String(result || ''); } /** @@ -2766,12 +2919,11 @@ function setBackgroundCallback(_, bg) { } /** - * Sets a model for the current API. - * @param {object} _ Unused - * @param {string} model New model name - * @returns {string} New or existing model name + * Retrieves the available model options based on the currently selected main API and its subtype + * + * @returns {{control: HTMLSelectElement, options: HTMLOptionElement[]}?} An array of objects representing the available model options, or null if not supported */ -function modelCallback(_, model) { +function getModelOptions() { const modelSelectMap = [ { id: 'model_togetherai_select', api: 'textgenerationwebui', type: textgen_types.TOGETHERAI }, { id: 'openrouter_model', api: 'textgenerationwebui', type: textgen_types.OPENROUTER }, @@ -2812,17 +2964,33 @@ function modelCallback(_, model) { if (!modelSelectItem) { toastr.info('Setting a model for your API is not supported or not implemented yet.'); - return ''; + return null; } const modelSelectControl = document.getElementById(modelSelectItem); if (!(modelSelectControl instanceof HTMLSelectElement)) { toastr.error(`Model select control not found: ${main_api}[${apiSubType}]`); - return ''; + return null; } const options = Array.from(modelSelectControl.options); + return { control: modelSelectControl, options }; +} + +/** + * Sets a model for the current API. + * @param {object} _ Unused + * @param {string} model New model name + * @returns {string} New or existing model name + */ +function modelCallback(_, model) { + const { control: modelSelectControl, options } = getModelOptions(); + + // If no model was found, the reason was already logged, we just return here + if (options === null) { + return ''; + } if (!options.length) { toastr.warning('No model options found. Check your API settings.'); diff --git a/public/scripts/slash-commands/SlashCommand.js b/public/scripts/slash-commands/SlashCommand.js index d04934924..d0821d4d9 100644 --- a/public/scripts/slash-commands/SlashCommand.js +++ b/public/scripts/slash-commands/SlashCommand.js @@ -175,6 +175,11 @@ export class SlashCommand { } li.append(specs); } + const stopgap = document.createElement('span'); { + stopgap.classList.add('stopgap'); + stopgap.textContent = ''; + li.append(stopgap); + } const help = document.createElement('span'); { help.classList.add('help'); const content = document.createElement('span'); { diff --git a/public/scripts/slash-commands/SlashCommandArgument.js b/public/scripts/slash-commands/SlashCommandArgument.js index 324d5b9d6..e27831a0c 100644 --- a/public/scripts/slash-commands/SlashCommandArgument.js +++ b/public/scripts/slash-commands/SlashCommandArgument.js @@ -1,9 +1,8 @@ import { SlashCommandClosure } from './SlashCommandClosure.js'; +import { commonEnumProviders } from './SlashCommandCommonEnumsProvider.js'; import { SlashCommandEnumValue } from './SlashCommandEnumValue.js'; import { SlashCommandExecutor } from './SlashCommandExecutor.js'; - - /**@readonly*/ /**@enum {string}*/ export const ARGUMENT_TYPE = { @@ -18,20 +17,18 @@ export const ARGUMENT_TYPE = { 'DICTIONARY': 'dictionary', }; - - export class SlashCommandArgument { /** * Creates an unnamed argument from a properties object. * @param {Object} props * @param {string} props.description description of the argument - * @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} props.typeList default: ARGUMENT_TYPE.STRING - list of accepted types (from ARGUMENT_TYPE) - * @param {boolean} [props.isRequired] default: false - whether the argument is required (false = optional argument) - * @param {boolean} [props.acceptsMultiple] default: false - whether argument accepts multiple values - * @param {string|SlashCommandClosure} [props.defaultValue] default value if no value is provided - * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList] list of accepted values - * @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} [props.enumProvider] function that returns auto complete options - * @param {boolean} [props.forceEnum] default: true - whether the input must match one of the enum values + * @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} [props.typeList=[ARGUMENT_TYPE.STRING]] default: ARGUMENT_TYPE.STRING - list of accepted types (from ARGUMENT_TYPE) + * @param {boolean} [props.isRequired=false] default: false - whether the argument is required (false = optional argument) + * @param {boolean} [props.acceptsMultiple=false] default: false - whether argument accepts multiple values + * @param {string|SlashCommandClosure} [props.defaultValue=null] default value if no value is provided + * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList=[]] list of accepted values + * @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} [props.enumProvider=null] function that returns auto complete options + * @param {boolean} [props.forceEnum=false] default: false - whether the input must match one of the enum values */ static fromProps(props) { return new SlashCommandArgument( @@ -42,13 +39,10 @@ export class SlashCommandArgument { props.defaultValue ?? null, props.enumList ?? [], props.enumProvider ?? null, - props.forceEnum ?? true, + props.forceEnum ?? false, ); } - - - /**@type {string}*/ description; /**@type {ARGUMENT_TYPE[]}*/ typeList = []; /**@type {boolean}*/ isRequired = false; @@ -56,8 +50,7 @@ export class SlashCommandArgument { /**@type {string|SlashCommandClosure}*/ defaultValue; /**@type {SlashCommandEnumValue[]}*/ enumList = []; /**@type {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]}*/ enumProvider = null; - /**@type {boolean}*/ forceEnum = true; - + /**@type {boolean}*/ forceEnum = false; /** * @param {string} description @@ -78,25 +71,26 @@ export class SlashCommandArgument { }); this.enumProvider = enumProvider; this.forceEnum = forceEnum; + + // If no enums were set explictly and the type is one where we know possible enum values, we set them here + if (!this.enumList.length && this.typeList.includes(ARGUMENT_TYPE.BOOLEAN)) this.enumList = commonEnumProviders.boolean()(); } } - - export class SlashCommandNamedArgument extends SlashCommandArgument { /** * Creates an unnamed argument from a properties object. * @param {Object} props * @param {string} props.name the argument's name - * @param {string[]} [props.aliasList] list of aliases * @param {string} props.description description of the argument - * @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} props.typeList default: ARGUMENT_TYPE.STRING - list of accepted types (from ARGUMENT_TYPE) - * @param {boolean} [props.isRequired] default: false - whether the argument is required (false = optional argument) - * @param {boolean} [props.acceptsMultiple] default: false - whether argument accepts multiple values - * @param {string|SlashCommandClosure} [props.defaultValue] default value if no value is provided - * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList] list of accepted values - * @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} [props.enumProvider] function that returns auto complete options - * @param {boolean} [props.forceEnum] default: true - whether the input must match one of the enum values + * @param {string[]} [props.aliasList=[]] list of aliases + * @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} [props.typeList=[ARGUMENT_TYPE.STRING]] default: ARGUMENT_TYPE.STRING - list of accepted types (from ARGUMENT_TYPE) + * @param {boolean} [props.isRequired=false] default: false - whether the argument is required (false = optional argument) + * @param {boolean} [props.acceptsMultiple=false] default: false - whether argument accepts multiple values + * @param {string|SlashCommandClosure} [props.defaultValue=null] default value if no value is provided + * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList=[]] list of accepted values + * @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} [props.enumProvider=null] function that returns auto complete options + * @param {boolean} [props.forceEnum=true] default: true - whether the input must match one of the enum values */ static fromProps(props) { return new SlashCommandNamedArgument( @@ -113,21 +107,20 @@ export class SlashCommandNamedArgument extends SlashCommandArgument { ); } - - - /**@type {string}*/ name; /**@type {string[]}*/ aliasList = []; - /** * @param {string} name * @param {string} description * @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} types - * @param {string|SlashCommandClosure} defaultValue - * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} enums - * @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} enumProvider function that returns auto complete options - * @param {boolean} forceEnum + * @param {boolean} [isRequired=false] + * @param {boolean} [acceptsMultiple=false] + * @param {string|SlashCommandClosure} [defaultValue=null] + * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [enums=[]] + * @param {string[]} [aliases=[]] + * @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} [enumProvider=null] function that returns auto complete options + * @param {boolean} [forceEnum=true] */ constructor(name, description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], aliases = [], enumProvider = null, forceEnum = true) { super(description, types, isRequired, acceptsMultiple, defaultValue, enums, enumProvider, forceEnum); diff --git a/public/scripts/slash-commands/SlashCommandClosure.js b/public/scripts/slash-commands/SlashCommandClosure.js index 89eda369a..6f62cc69b 100644 --- a/public/scripts/slash-commands/SlashCommandClosure.js +++ b/public/scripts/slash-commands/SlashCommandClosure.js @@ -93,7 +93,7 @@ export class SlashCommandClosure { /** * - * @returns Promise + * @returns {Promise} */ async execute() { const closure = this.getCopy(); diff --git a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js new file mode 100644 index 000000000..683f28ffa --- /dev/null +++ b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js @@ -0,0 +1,232 @@ +import { chat_metadata, characters, substituteParams, chat, extension_prompt_roles, extension_prompt_types } from "../../script.js"; +import { extension_settings } from "../extensions.js"; +import { getGroupMembers, groups, selected_group } from "../group-chats.js"; +import { power_user } from "../power-user.js"; +import { searchCharByName, getTagsList, tags } from "../tags.js"; +import { SlashCommandClosure } from "./SlashCommandClosure.js"; +import { SlashCommandEnumValue, enumTypes } from "./SlashCommandEnumValue.js"; +import { SlashCommandExecutor } from "./SlashCommandExecutor.js"; + +/** + * A collection of regularly used enum icons + */ +export const enumIcons = { + default: '◊', + + // Variables + variable: '𝑥', + localVariable: 'L', + globalVariable: 'G', + scopeVariable: 'S', + + // Common types + character: '👤', + group: '🧑‍🤝‍🧑', + persona: '🧙‍♂️', + qr: 'QR', + closure: '𝑓', + macro: '{{', + tag: '🏷️', + world: '🌐', + preset: '⚙️', + file: '📄', + message: '💬', + voice: '🎤', + + true: '✔️', + false: '❌', + + // Value types + boolean: '🔲', + string: '📝', + number: '1️⃣', + array: '[]', + enum: '📚', + dictionary: '{}', + + // Roles + system: '⚙️', + user: '👤', + assistant: '🤖', + + // WI Icons + constant: '🔵', + normal: '🟢', + disabled: '❌', + vectorized: '🔗', + + /** + * Returns the appropriate state icon based on a boolean + * + * @param {boolean} state - The state to determine the icon for + * @returns {string} The corresponding state icon + */ + getStateIcon: (state) => { + return state ? enumIcons.true : enumIcons.false; + }, + + /** + * Returns the appropriate WI icon based on the entry + * + * @param {Object} entry - WI entry + * @returns {string} The corresponding WI icon + */ + getWiStatusIcon: (entry) => { + if (entry.constant) return enumIcons.constant; + if (entry.disable) return enumIcons.disabled; + if (entry.vectorized) return enumIcons.vectorized; + return enumIcons.normal; + }, + + /** + * Returns the appropriate icon based on the role + * + * @param {extension_prompt_roles} role - The role to get the icon for + * @returns {string} The corresponding icon + */ + getRoleIcon: (role) => { + switch (role) { + case extension_prompt_roles.SYSTEM: return enumIcons.system; + case extension_prompt_roles.USER: return enumIcons.user; + case extension_prompt_roles.ASSISTANT: return enumIcons.assistant; + default: return enumIcons.default; + } + }, + + /** + * A function to get the data type icon + * + * @param {string} type - The type of the data + * @returns {string} The corresponding data type icon + */ + getDataTypeIcon: (type) => { + // Remove possible nullable types definition to match type icon + type = type.replace(/\?$/, ''); + return enumIcons[type] ?? enumIcons.default; + } +} + +/** + * A collection of common enum providers + * + * Can be used on `SlashCommandNamedArgument` and `SlashCommandArgument` and their `enumProvider` property. + */ +export const commonEnumProviders = { + /** + * Enum values for booleans. Either using true/false or on/off + * Optionally supports "toggle". + * + * @param {('onOff'|'onOffToggle'|'trueFalse')?} [mode='trueFalse'] - The mode to use. Default is 'trueFalse'. + * @returns {() => SlashCommandEnumValue[]} + */ + boolean: (mode = 'trueFalse') => () => { + switch (mode) { + case 'onOff': return [new SlashCommandEnumValue('on', null, 'macro', enumIcons.true), new SlashCommandEnumValue('off', null, 'macro', enumIcons.false)]; + case 'onOffToggle': return [new SlashCommandEnumValue('on', null, 'macro', enumIcons.true), new SlashCommandEnumValue('off', null, 'macro', enumIcons.false), new SlashCommandEnumValue('toggle', null, 'macro', enumIcons.boolean)]; + case 'trueFalse': return [new SlashCommandEnumValue('true', null, 'macro', enumIcons.true), new SlashCommandEnumValue('false', null, 'macro', enumIcons.false)]; + default: throw new Error(`Invalid boolean enum provider mode: ${mode}`); + } + }, + + /** + * All possible variable names + * + * Can be filtered by `type` to only show global or local variables + * + * @param {...('global'|'local'|'scope'|'all')} type - The type of variables to include in the array. Can be 'all', 'global', or 'local'. + * @returns {() => SlashCommandEnumValue[]} + */ + variables: (...type) => () => { + const types = type.flat(); + const isAll = types.includes('all'); + return [ + ...isAll || types.includes('global') ? Object.keys(extension_settings.variables.global ?? []).map(name => new SlashCommandEnumValue(name, null, enumTypes.macro, enumIcons.globalVariable)) : [], + ...isAll || types.includes('local') ? Object.keys(chat_metadata.variables ?? []).map(name => new SlashCommandEnumValue(name, null, enumTypes.name, enumIcons.localVariable)) : [], + ...isAll || types.includes('scope') ? [].map(name => new SlashCommandEnumValue(name, null, enumTypes.variable, enumIcons.scopeVariable)) : [], // TODO: Add scoped variables here, Lenny + ] + }, + + /** + * All possible char entities, like characters and groups. Can be filtered down to just one type. + * + * @param {('all' | 'character' | 'group')?} [mode='all'] - Which type to return + * @returns {() => SlashCommandEnumValue[]} + */ + characters: (mode = 'all') => () => { + return [ + ...['all', 'character'].includes(mode) ? characters.map(char => new SlashCommandEnumValue(char.name, null, enumTypes.name, enumIcons.character)) : [], + ...['all', 'group'].includes(mode) ? groups.map(group => new SlashCommandEnumValue(group.name, null, enumTypes.qr, enumIcons.group)) : [], + ]; + }, + + /** + * All group members of the given group, or default the current active one + * + * @param {string?} groupId - The id of the group - pass in `undefined` to use the current active group + * @returns {() =>SlashCommandEnumValue[]} + */ + groupMembers: (groupId = undefined) => () => getGroupMembers(groupId).map((character, index) => new SlashCommandEnumValue(String(index), character.name, enumTypes.enum, enumIcons.character)), + + /** + * All possible personas + * + * @returns {SlashCommandEnumValue[]} + */ + personas: () => Object.values(power_user.personas).map(persona => new SlashCommandEnumValue(persona, null, enumTypes.name, enumIcons.persona)), + + /** + * All possible tags for a given char/group entity + * + * @param {('all' | 'existing' | 'not-existing')?} [mode='all'] - Which types of tags to show + * @returns {() => SlashCommandEnumValue[]} + */ + tagsForChar: (mode = 'all') => (/** @type {SlashCommandExecutor} */ executor) => { + // Try to see if we can find the char during execution to filter down the tags list some more. Otherwise take all tags. + const charName = executor.namedArgumentList.find(it => it.name == 'name')?.value; + if (charName instanceof SlashCommandClosure) throw new Error('Argument \'name\' does not support closures'); + const key = searchCharByName(substituteParams(charName), { suppressLogging: true }); + const assigned = key ? getTagsList(key) : []; + return tags.filter(it => !key || mode === 'all' || mode === 'existing' && assigned.includes(it) || mode === 'not-existing' && !assigned.includes(it)) + .map(tag => new SlashCommandEnumValue(tag.name, null, enumTypes.command, enumIcons.tag)); + }, + + /** + * All messages in the current chat, returning the message id + * + * Optionally supports variable names, and/or a placeholder for the last/new message id + * + * @param {object} [options={}] - Optional arguments + * @param {boolean} [options.allowIdAfter=false] - Whether to add an enum option for the new message id after the last message + * @param {boolean} [options.allowVars=false] - Whether to add enum option for variable names + * @returns {() => SlashCommandEnumValue[]} + */ + messages: ({ allowIdAfter = false, allowVars = false } = {}) => () => { + return [ + ...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)), + ...allowIdAfter ? [new SlashCommandEnumValue(String(chat.length), '>> After Last Message >>', enumTypes.enum, '➕')] : [], + ...allowVars ? commonEnumProviders.variables('all')() : [], + ]; + }, + + /** + * All existing worlds / lorebooks + * + * @returns {SlashCommandEnumValue[]} + */ + worlds: () => $('#world_info').children().toArray().map(x => new SlashCommandEnumValue(x.textContent, null, enumTypes.name, enumIcons.world)), + + /** + * All existing injects for the current chat + * + * @returns {SlashCommandEnumValue[]} + */ + injects: () => { + if (!chat_metadata.script_injects || !Object.keys(chat_metadata.script_injects).length) return []; + return Object.entries(chat_metadata.script_injects) + .map(([id, inject]) => { + const positionName = (Object.entries(extension_prompt_types)).find(([_, value]) => value === inject.position)?.[0] ?? 'unknown'; + return new SlashCommandEnumValue(id, `${enumIcons.getRoleIcon(inject.role ?? extension_prompt_roles.SYSTEM)}[Inject](${positionName}, depth: ${inject.depth}, scan: ${inject.scan ?? false}) ${inject.value}`, + enumTypes.enum, '💉'); + }); + }, +}; diff --git a/public/scripts/slash-commands/SlashCommandEnumValue.js b/public/scripts/slash-commands/SlashCommandEnumValue.js index 1fa610c56..973d1be99 100644 --- a/public/scripts/slash-commands/SlashCommandEnumValue.js +++ b/public/scripts/slash-commands/SlashCommandEnumValue.js @@ -1,13 +1,63 @@ + +/** + * @typedef {'enum' | 'command' | 'namedArgument' | 'variable' | 'qr' | 'macro' | 'number' | 'name'} EnumType + */ + +/** + * Collection of the enum types that can be used with `SlashCommandEnumValue` + * + * Contains documentation on which color this will result to + */ +export const enumTypes = { + /** 'enum' - [string] - light orange @type {EnumType} */ + enum: 'enum', + /** 'command' - [cmd] - light yellow @type {EnumType} */ + command: 'command', + /** 'namedArgument' - [argName] - sky blue @type {EnumType} */ + namedArgument: 'namedArgument', + /** 'variable' - [punctuationL1] - pink @type {EnumType} */ + variable: 'variable', + /** 'qr' - [variable] - light blue @type {EnumType} */ + qr: 'qr', + /** 'macro' - [variableLanguage] - blue @type {EnumType} */ + macro: 'macro', + /** 'number' - [number] - light green @type {EnumType} */ + number: 'number', + /** 'name' - [type] - forest green @type {EnumType} */ + name: 'name', + + /** + * Gets the value of the enum type based on the provided index + * + * Can be used to get differing colors or even random colors, by providing the index of a unique set + * + * @param {number?} index - The index used to retrieve the enum type + * @return {EnumType} The enum type corresponding to the index + */ + getBasedOnIndex(index) { + const keys = Object.keys(this); + return this[keys[(index ?? 0) % keys.length]]; + } +} + export class SlashCommandEnumValue { /**@type {string}*/ value; /**@type {string}*/ description; - /**@type {string}*/ type = 'enum'; + /**@type {EnumType}*/ type = 'enum'; /**@type {string}*/ typeIcon = '◊'; + /** + * A constructor for creating a SlashCommandEnumValue instance. + * + * @param {string} value - The value + * @param {string?} description - Optional description, displayed in a second line + * @param {EnumType?} type - type of the enum (defining its color) + * @param {string} typeIcon - The icon to display (Can be pulled from `enumIcons` for common ones) + */ constructor(value, description = null, type = 'enum', typeIcon = '◊') { this.value = value; this.description = description; - this.type = type; + this.type = type ?? 'enum'; this.typeIcon = typeIcon; } diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index 8dffa666e..1b6f65761 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -17,6 +17,10 @@ import { SlashCommandAutoCompleteNameResult } from './SlashCommandAutoCompleteNa import { SlashCommandUnnamedArgumentAssignment } from './SlashCommandUnnamedArgumentAssignment.js'; import { SlashCommandEnumValue } from './SlashCommandEnumValue.js'; import { MacroAutoCompleteOption } from '../autocomplete/MacroAutoCompleteOption.js'; +import { commonEnumProviders } from './SlashCommandCommonEnumsProvider.js'; + +/** @typedef {import('./SlashCommand.js').NamedArgumentsCapture} NamedArgumentsCapture */ +/** @typedef {import('./SlashCommand.js').NamedArguments} NamedArguments */ /**@readonly*/ /**@enum {Number}*/ @@ -32,7 +36,7 @@ export class SlashCommandParser { /** * @deprecated Use SlashCommandParser.addCommandObject() instead. * @param {string} command Command name - * @param {(namedArguments:Object., unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|void|Promise} callback The function to execute when the command is called + * @param {(namedArguments:NamedArguments|NamedArgumentsCapture, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|Promise} callback callback The function to execute when the command is called * @param {string[]} aliases List of alternative command names * @param {string} helpString Help text shown in autocomplete and command browser */ @@ -131,7 +135,7 @@ export class SlashCommandParser { description: 'The state of the parser flag to set.', typeList: [ARGUMENT_TYPE.BOOLEAN], defaultValue: 'on', - enumList: ['on', 'off'], + enumList: commonEnumProviders.boolean('onOff')(), }), ], splitUnnamedArgument: true, diff --git a/public/scripts/tags.js b/public/scripts/tags.js index 24a57a9c0..eb32cc646 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -9,6 +9,8 @@ import { eventSource, event_types, DEFAULT_PRINT_TIMEOUT, + substituteParams, + printCharacters, } from '../script.js'; // eslint-disable-next-line no-unused-vars import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, FilterHelper } from './filters.js'; @@ -25,6 +27,7 @@ import { debounce_timeout } from './constants.js'; import { INTERACTABLE_CONTROL_CLASS } from './keyboard.js'; import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js'; import { SlashCommandExecutor } from './slash-commands/SlashCommandExecutor.js'; +import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js'; import { renderTemplateAsync } from './templates.js'; export { @@ -491,6 +494,27 @@ export function getTagKeyForEntityElement(element) { return undefined; } +/** + * Gets the key for char/group by searching based on the name or avatar. If none can be found, a toastr will be shown and null returned. + * This function is mostly used in slash commands. + * + * @param {string?} [charName] The optionally provided char name + * @param {object} [options] - Optional arguments + * @param {boolean} [options.suppressLogging=false] - Whether to suppress the toastr warning + * @returns {string?} - The char/group key, or null if none found + */ +export function searchCharByName(charName, { suppressLogging = false } = {}) { + const entity = charName + ? (characters.find(x => x.name === charName) || groups.find(x => x.name == charName)) + : (selected_group ? groups.find(x => x.id == selected_group) : characters[this_chid]); + const key = getTagKeyForEntity(entity); + if (!key) { + if (!suppressLogging) toastr.warning(`Character ${charName} not found.`); + return null; + } + return key; +} + /** * Adds one or more tags to a given entity * @@ -752,7 +776,7 @@ async function handleTagImport(character, { forceShow = false } = {}) { async function showTagImportPopup(character, existingTags, newTags, folderTags) { /** @type {{[key: string]: import('./popup.js').CustomPopupButton}} */ const importButtons = { - NONE: { result: 2, text: 'Import None', }, + NONE: { result: 2, text: 'Import None' }, ALL: { result: 3, text: 'Import All' }, EXISTING: { result: 4, text: 'Import Existing' }, }; @@ -1768,22 +1792,6 @@ function printViewTagList(tagContainer, empty = true) { } function registerTagsSlashCommands() { - /** - * Gets the key for char/group for a slash command. If none can be found, a toastr will be shown and null returned. - * @param {string?} [charName] The optionally provided char name - * @returns {string?} - The char/group key, or null if none found - */ - function paraGetCharKey(charName) { - const entity = charName - ? (characters.find(x => x.name === charName) || groups.find(x => x.name == charName)) - : (selected_group ? groups.find(x => x.id == selected_group) : characters[this_chid]); - const key = getTagKeyForEntity(entity); - if (!key) { - toastr.warning(`Character ${charName} not found.`); - return null; - } - return key; - } /** * Gets a tag by its name. Optionally can create the tag if it does not exist. * @param {string} tagName - The name of the tag @@ -1812,11 +1820,12 @@ function registerTagsSlashCommands() { returns: 'true/false - Whether the tag was added or was assigned already', /** @param {{name: string}} namedArgs @param {string} tagName @returns {string} */ callback: ({ name }, tagName) => { - const key = paraGetCharKey(name); + const key = searchCharByName(name); if (!key) return 'false'; const tag = paraGetTag(tagName, { allowCreate: true }); if (!tag) return 'false'; const result = addTagsToEntity(tag, key); + printCharacters(); return String(result); }, namedArgumentList: [ @@ -1824,22 +1833,14 @@ function registerTagsSlashCommands() { description: 'Character name', typeList: [ARGUMENT_TYPE.STRING], defaultValue: '{{char}}', - enumProvider: ()=>[ - ...characters.map(it=>new SlashCommandEnumValue(it.name, null, 'qr', 'C')), - ...groups.map(it=>new SlashCommandEnumValue(it.name, null, 'variable', 'G')), - ], + enumProvider: commonEnumProviders.characters(), }), ], unnamedArgumentList: [ SlashCommandArgument.fromProps({ description: 'tag name', typeList: [ARGUMENT_TYPE.STRING], isRequired: true, - enumProvider: (executor)=>{ - const key = paraGetCharKey(/**@type {string}*/(executor.namedArgumentList.find(it=>it.name == 'name')?.value)); - if (!key) return tags.map(it=>new SlashCommandEnumValue(it.name, it.title)); - const assigned = getTagsList(key); - return tags.filter(it=>!assigned.includes(it)).map(it=>new SlashCommandEnumValue(it.name, it.title)); - }, + enumProvider: commonEnumProviders.tagsForChar('not-existing'), forceEnum: false, }), ], @@ -1864,11 +1865,12 @@ function registerTagsSlashCommands() { returns: 'true/false - Whether the tag was removed or wasn\'t assigned already', /** @param {{name: string}} namedArgs @param {string} tagName @returns {string} */ callback: ({ name }, tagName) => { - const key = paraGetCharKey(name); + const key = searchCharByName(name); if (!key) return 'false'; const tag = paraGetTag(tagName); if (!tag) return 'false'; const result = removeTagFromEntity(tag, key); + printCharacters(); return String(result); }, namedArgumentList: [ @@ -1876,10 +1878,7 @@ function registerTagsSlashCommands() { description: 'Character name', typeList: [ARGUMENT_TYPE.STRING], defaultValue: '{{char}}', - enumProvider: ()=>[ - ...characters.map(it=>new SlashCommandEnumValue(it.name, null, 'qr', 'C')), - ...groups.map(it=>new SlashCommandEnumValue(it.name, null, 'variable', 'G')), - ], + enumProvider: commonEnumProviders.characters(), }), ], unnamedArgumentList: [ @@ -1887,11 +1886,7 @@ function registerTagsSlashCommands() { typeList: [ARGUMENT_TYPE.STRING], isRequired: true, /**@param {SlashCommandExecutor} executor */ - enumProvider: (executor)=>{ - const key = paraGetCharKey(/**@type {string}*/(executor.namedArgumentList.find(it=>it.name == 'name')?.value)); - if (!key) return tags.map(it=>new SlashCommandEnumValue(it.name, it.title)); - return getTagsList(key).map(it=>new SlashCommandEnumValue(it.name, it.title)); - }, + enumProvider: commonEnumProviders.tagsForChar('existing'), }), ], helpString: ` @@ -1914,17 +1909,29 @@ function registerTagsSlashCommands() { returns: 'true/false - Whether the given tag name is assigned to the character', /** @param {{name: string}} namedArgs @param {string} tagName @returns {string} */ callback: ({ name }, tagName) => { - const key = paraGetCharKey(name); + const key = searchCharByName(name); if (!key) return 'false'; const tag = paraGetTag(tagName); if (!tag) return 'false'; return String(tag_map[key].includes(tag.id)); }, namedArgumentList: [ - new SlashCommandNamedArgument('name', 'Character name', [ARGUMENT_TYPE.STRING], false, false, '{{char}}'), + SlashCommandNamedArgument.fromProps({ + name: 'name', + description: 'Character name', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: '{{char}}', + enumProvider: commonEnumProviders.characters(), + }), ], unnamedArgumentList: [ - new SlashCommandArgument('tag name', [ARGUMENT_TYPE.STRING], true), + SlashCommandArgument.fromProps({ + description: 'tag name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + /**@param {SlashCommandExecutor} executor */ + enumProvider: commonEnumProviders.tagsForChar('all'), + }), ], helpString: `
@@ -1946,13 +1953,19 @@ function registerTagsSlashCommands() { returns: 'Comma-separated list of all assigned tags', /** @param {{name: string}} namedArgs @returns {string} */ callback: ({ name }) => { - const key = paraGetCharKey(name); + const key = searchCharByName(name); if (!key) return ''; const tags = getTagsList(key); return tags.map(x => x.name).join(', '); }, namedArgumentList: [ - new SlashCommandNamedArgument('name', 'Character name', [ARGUMENT_TYPE.STRING], false, false, '{{char}}'), + SlashCommandNamedArgument.fromProps({ + name: 'name', + description: 'Character name', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: '{{char}}', + enumProvider: commonEnumProviders.characters(), + }), ], helpString: `
diff --git a/public/scripts/variables.js b/public/scripts/variables.js index de8ff390b..3117f07ec 100644 --- a/public/scripts/variables.js +++ b/public/scripts/variables.js @@ -1,16 +1,20 @@ import { chat_metadata, getCurrentChatId, saveSettingsDebounced, sendSystemMessage, system_message_types } from '../script.js'; import { extension_settings, saveMetadataDebounced } from './extensions.js'; -import { executeSlashCommands, executeSlashCommandsWithOptions } from './slash-commands.js'; +import { executeSlashCommandsWithOptions } from './slash-commands.js'; import { SlashCommand } from './slash-commands/SlashCommand.js'; import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js'; +import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js'; import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js'; import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { SlashCommandScope } from './slash-commands/SlashCommandScope.js'; import { isFalseBoolean } from './utils.js'; +/** @typedef {import('./slash-commands/SlashCommandParser.js').NamedArguments} NamedArguments */ +/** @typedef {import('./slash-commands/SlashCommand.js').UnnamedArguments} UnnamedArguments */ + const MAX_LOOPS = 100; function getLocalVariable(name, args = {}) { @@ -116,6 +120,7 @@ function setGlobalVariable(name, value, args = {}) { extension_settings.variables.global[name] = value; } saveSettingsDebounced(); + return value; } function addLocalVariable(name, value) { @@ -314,14 +319,16 @@ function listVariablesCallback() { const htmlMessage = DOMPurify.sanitize(converter.makeHtml(message)); sendSystemMessage(system_message_types.GENERIC, htmlMessage); + return ''; } /** * - * @param {import('./slash-commands/SlashCommand.js').NamedArguments} args + * @param {NamedArguments} args * @param {(string|SlashCommandClosure)[]} value */ async function whileCallback(args, value) { + if (args.guard instanceof SlashCommandClosure) throw new Error('argument \'guard\' cannot be a closure for command /while'); const isGuardOff = isFalseBoolean(args.guard); const iterations = isGuardOff ? Number.MAX_SAFE_INTEGER : MAX_LOOPS; /**@type {string|SlashCommandClosure} */ @@ -360,11 +367,12 @@ async function whileCallback(args, value) { /** * - * @param {import('./slash-commands/SlashCommand.js').NamedArguments} args - * @param {import('./slash-commands/SlashCommand.js').UnnamedArguments} value + * @param {NamedArguments} args + * @param {UnnamedArguments} value * @returns */ async function timesCallback(args, value) { + if (args.guard instanceof SlashCommandClosure) throw new Error('argument \'guard\' cannot be a closure for command /while'); let repeats; let command; if (Array.isArray(value)) { @@ -398,7 +406,7 @@ async function timesCallback(args, value) { /** * - * @param {import('./slash-commands/SlashCommand.js').NamedArguments} args + * @param {NamedArguments} args * @param {(string|SlashCommandClosure)[]} value */ async function ifCallback(args, value) { @@ -765,13 +773,13 @@ function randValuesCallback(from, to, args) { if (args.round == 'floor') { return Math.floor(value); } - return String(value); + return value; } /** * Declare a new variable in the current scope. - * @param {{_scope:SlashCommandScope, key?:string}} args Named arguments. - * @param {String|[String, SlashCommandClosure]} value Name and optional value for the variable. + * @param {NamedArguments} args Named arguments. + * @param {string|SlashCommandClosure|(string|SlashCommandClosure)[]} value Name and optional value for the variable. * @returns The variable's value */ function letCallback(args, value) { @@ -784,7 +792,9 @@ function letCallback(args, value) { const val = value; args._scope.letVariable(key, val); return val; - } else if (value.includes(' ')) { + } + if (value instanceof SlashCommandClosure) throw new Error('/let unnamed argument does not support closures if no key is provided'); + if (value.includes(' ')) { const key = value.split(' ')[0]; const val = value.split(' ').slice(1).join(' '); args._scope.letVariable(key, val); @@ -795,7 +805,7 @@ function letCallback(args, value) { /** * Set or retrieve a variable in the current scope or nearest ancestor scope. - * @param {{_hasUnnamedArgument:boolean, _scope:SlashCommandScope, key?:string, index?:string|number}} args Named arguments. + * @param {NamedArguments} args Named arguments. * @param {string|SlashCommandClosure|(string|SlashCommandClosure)[]} value Name and optional value for the variable. * @returns The variable's value */ @@ -822,7 +832,7 @@ function varCallback(args, value) { } /** - * @param {import('./slash-commands/SlashCommand.js').NamedArguments} args + * @param {NamedArguments} args * @param {SlashCommandClosure} value * @returns {string} */ @@ -834,8 +844,8 @@ function closureSerializeCallback(args, value) { } /** - * @param {import('./slash-commands/SlashCommand.js').NamedArguments} args - * @param {import('./slash-commands/SlashCommand.js').UnnamedArguments} value + * @param {NamedArguments} args + * @param {UnnamedArguments} value * @returns {SlashCommandClosure} */ function closureDeserializeCallback(args, value) { @@ -846,17 +856,24 @@ function closureDeserializeCallback(args, value) { } export function registerVariableCommands() { - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'listvar', + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'listvar', callback: listVariablesCallback, helpString: 'List registered chat variables.', })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'setvar', - callback: (args, value) => setLocalVariable(args.key || args.name, value, args), + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'setvar', + callback: (args, value) => String(setLocalVariable(args.key || args.name, value, args)), returns: 'the set variable value', namedArgumentList: [ - new SlashCommandNamedArgument( - 'key', 'variable name', [ARGUMENT_TYPE.VARIABLE_NAME], true, - ), + SlashCommandNamedArgument.fromProps({ + name: 'key', + description: 'variable name', + typeList: [ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: true, + enumProvider: commonEnumProviders.variables('local'), + forceEnum: false, + }), new SlashCommandNamedArgument( 'index', 'list index', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], false, ), @@ -880,21 +897,28 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'getvar', - callback: (args, value) => getLocalVariable(value, args), + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'getvar', + callback: (args, value) => String(getLocalVariable(value, args)), returns: 'the variable value', namedArgumentList: [ - new SlashCommandNamedArgument( - 'key', 'variable name', [ARGUMENT_TYPE.VARIABLE_NAME], false, - ), + SlashCommandNamedArgument.fromProps({ + name: 'key', + description: 'variable name', + typeList: [ARGUMENT_TYPE.VARIABLE_NAME], + enumProvider: commonEnumProviders.variables('local'), + }), new SlashCommandNamedArgument( 'index', 'list index', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], false, ), ], unnamedArgumentList: [ - new SlashCommandArgument( - 'key', [ARGUMENT_TYPE.VARIABLE_NAME], false, - ), + SlashCommandArgument.fromProps({ + description: 'key', + typeList: [ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: false, + enumProvider: commonEnumProviders.variables('local'), + }), ], helpString: `
@@ -916,13 +940,19 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'addvar', - callback: (args, value) => addLocalVariable(args.key || args.name, value), + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'addvar', + callback: (args, value) => String(addLocalVariable(args.key || args.name, value)), returns: 'the new variable value', namedArgumentList: [ - new SlashCommandNamedArgument( - 'key', 'variable name', [ARGUMENT_TYPE.VARIABLE_NAME], true, - ), + SlashCommandNamedArgument.fromProps({ + name: 'key', + description: 'variable name', + typeList: [ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: true, + enumProvider: commonEnumProviders.variables('local'), + forceEnum: false, + }), ], unnamedArgumentList: [ new SlashCommandArgument( @@ -943,13 +973,19 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'setglobalvar', - callback: (args, value) => setGlobalVariable(args.key || args.name, value, args), + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'setglobalvar', + callback: (args, value) => String(setGlobalVariable(args.key || args.name, value, args)), returns: 'the set global variable value', namedArgumentList: [ - new SlashCommandNamedArgument( - 'key', 'variable name', [ARGUMENT_TYPE.VARIABLE_NAME], true, - ), + SlashCommandNamedArgument.fromProps({ + name: 'key', + description: 'variable name', + typeList: [ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: true, + enumProvider: commonEnumProviders.variables('global'), + forceEnum: false, + }), new SlashCommandNamedArgument( 'index', 'list index', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], false, ), @@ -973,21 +1009,27 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'getglobalvar', - callback: (args, value) => getGlobalVariable(value, args), + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'getglobalvar', + callback: (args, value) => String(getGlobalVariable(value, args)), returns: 'global variable value', namedArgumentList: [ - new SlashCommandNamedArgument( - 'key', 'variable name', [ARGUMENT_TYPE.VARIABLE_NAME], false, - ), + SlashCommandNamedArgument.fromProps({ + name: 'key', + description: 'variable name', + typeList: [ARGUMENT_TYPE.VARIABLE_NAME], + enumProvider: commonEnumProviders.variables('global'), + }), new SlashCommandNamedArgument( 'index', 'list index', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], false, ), ], unnamedArgumentList: [ - new SlashCommandArgument( - 'key', [ARGUMENT_TYPE.VARIABLE_NAME], false, - ), + SlashCommandArgument.fromProps({ + description: 'key', + typeList: [ARGUMENT_TYPE.VARIABLE_NAME], + enumProvider: commonEnumProviders.variables('global'), + }), ], helpString: `
@@ -1009,13 +1051,19 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'addglobalvar', - callback: (args, value) => addGlobalVariable(args.key || args.name, value), + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'addglobalvar', + callback: (args, value) => String(addGlobalVariable(args.key || args.name, value)), returns: 'the new variable value', namedArgumentList: [ - new SlashCommandNamedArgument( - 'key', 'variable name', [ARGUMENT_TYPE.VARIABLE_NAME], true, - ), + SlashCommandNamedArgument.fromProps({ + name: 'key', + description: 'variable name', + typeList: [ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: true, + enumProvider: commonEnumProviders.variables('global'), + forceEnum: false, + }), ], unnamedArgumentList: [ new SlashCommandArgument( @@ -1036,13 +1084,19 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'incvar', - callback: (_, value) => incrementLocalVariable(value), + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'incvar', + callback: (_, value) => String(incrementLocalVariable(value)), returns: 'the new variable value', unnamedArgumentList: [ - new SlashCommandArgument( - 'key', [ARGUMENT_TYPE.VARIABLE_NAME], true, - ), + SlashCommandNamedArgument.fromProps({ + name: 'key', + description: 'variable name', + typeList: [ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: true, + enumProvider: commonEnumProviders.variables('local'), + forceEnum: false, + }), ], helpString: `
@@ -1058,13 +1112,19 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'decvar', - callback: (_, value) => decrementLocalVariable(value), + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'decvar', + callback: (_, value) => String(decrementLocalVariable(value)), returns: 'the new variable value', unnamedArgumentList: [ - new SlashCommandArgument( - 'key', [ARGUMENT_TYPE.VARIABLE_NAME], true, - ), + SlashCommandNamedArgument.fromProps({ + name: 'key', + description: 'variable name', + typeList: [ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: true, + enumProvider: commonEnumProviders.variables('local'), + forceEnum: false, + }), ], helpString: `
@@ -1080,13 +1140,19 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'incglobalvar', - callback: (_, value) => incrementGlobalVariable(value), + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'incglobalvar', + callback: (_, value) => String(incrementGlobalVariable(value)), returns: 'the new variable value', unnamedArgumentList: [ - new SlashCommandArgument( - 'key', [ARGUMENT_TYPE.VARIABLE_NAME], true, - ), + SlashCommandNamedArgument.fromProps({ + name: 'key', + description: 'variable name', + typeList: [ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: true, + enumProvider: commonEnumProviders.variables('global'), + forceEnum: false, + }), ], helpString: `
@@ -1102,13 +1168,19 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'decglobalvar', - callback: (_, value) => decrementGlobalVariable(value), + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'decglobalvar', + callback: (_, value) => String(decrementGlobalVariable(value)), returns: 'the new variable value', unnamedArgumentList: [ - new SlashCommandArgument( - 'key', [ARGUMENT_TYPE.VARIABLE_NAME], true, - ), + SlashCommandNamedArgument.fromProps({ + name: 'key', + description: 'variable name', + typeList: [ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: true, + enumProvider: commonEnumProviders.variables('global'), + forceEnum: false, + }), ], helpString: `
@@ -1124,26 +1196,37 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'if', + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'if', callback: ifCallback, returns: 'result of the executed command ("then" or "else")', namedArgumentList: [ - new SlashCommandNamedArgument( - 'left', 'left operand', [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER], true, - ), - new SlashCommandNamedArgument( - 'right', 'right operand', [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER], true, - ), + SlashCommandNamedArgument.fromProps({ + name: 'left', + description: 'left operand', + typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER], + isRequired: true, + enumProvider: commonEnumProviders.variables('all'), + forceEnum: false, + }), + SlashCommandNamedArgument.fromProps({ + name: 'right', + description: 'right operand', + typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER], + isRequired: true, + enumProvider: commonEnumProviders.variables('all'), + forceEnum: false, + }), new SlashCommandNamedArgument( 'rule', 'comparison rule', [ARGUMENT_TYPE.STRING], true, false, null, [ - new SlashCommandEnumValue('gt', 'a > b'), + new SlashCommandEnumValue('gt', 'a > b'), new SlashCommandEnumValue('gte', 'a >= b'), - new SlashCommandEnumValue('lt', 'a < b'), + new SlashCommandEnumValue('lt', 'a < b'), new SlashCommandEnumValue('lte', 'a <= b'), - new SlashCommandEnumValue('eq', 'a == b'), + new SlashCommandEnumValue('eq', 'a == b'), new SlashCommandEnumValue('neq', 'a !== b'), new SlashCommandEnumValue('not', '!a'), - new SlashCommandEnumValue('in', 'a includes b'), + new SlashCommandEnumValue('in', 'a includes b'), new SlashCommandEnumValue('nin', 'a not includes b'), ], ), @@ -1191,31 +1274,42 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'while', + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'while', callback: whileCallback, returns: 'result of the last executed command', namedArgumentList: [ - new SlashCommandNamedArgument( - 'left', 'left operand', [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER], true, - ), - new SlashCommandNamedArgument( - 'right', 'right operand', [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER], true, - ), + SlashCommandNamedArgument.fromProps({ + name: 'left', + description: 'left operand', + typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER], + isRequired: true, + enumProvider: commonEnumProviders.variables('all'), + forceEnum: false, + }), + SlashCommandNamedArgument.fromProps({ + name: 'right', + description: 'right operand', + typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER], + isRequired: true, + enumProvider: commonEnumProviders.variables('all'), + forceEnum: false, + }), new SlashCommandNamedArgument( 'rule', 'comparison rule', [ARGUMENT_TYPE.STRING], true, false, null, [ - new SlashCommandEnumValue('gt', 'a > b'), + new SlashCommandEnumValue('gt', 'a > b'), new SlashCommandEnumValue('gte', 'a >= b'), - new SlashCommandEnumValue('lt', 'a < b'), + new SlashCommandEnumValue('lt', 'a < b'), new SlashCommandEnumValue('lte', 'a <= b'), - new SlashCommandEnumValue('eq', 'a == b'), + new SlashCommandEnumValue('eq', 'a == b'), new SlashCommandEnumValue('neq', 'a !== b'), new SlashCommandEnumValue('not', '!a'), - new SlashCommandEnumValue('in', 'a includes b'), + new SlashCommandEnumValue('in', 'a includes b'), new SlashCommandEnumValue('nin', 'a not includes b'), ], ), new SlashCommandNamedArgument( - 'guard', 'disable loop iteration limit', [ARGUMENT_TYPE.STRING], false, false, null, ['off'], + 'guard', 'disable loop iteration limit', [ARGUMENT_TYPE.STRING], false, false, null, commonEnumProviders.boolean('onOff')(), ), ], unnamedArgumentList: [ @@ -1260,7 +1354,8 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'times', + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'times', callback: timesCallback, returns: 'result of the last executed command', namedArgumentList: [], @@ -1299,13 +1394,16 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'flushvar', - callback: (_, value) => deleteLocalVariable(value), - namedArgumentList: [], + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'flushvar', + callback: async (_, value) => deleteLocalVariable(value instanceof SlashCommandClosure ? (await value.execute())?.pipe : String(value)), unnamedArgumentList: [ - new SlashCommandArgument( - 'key', [ARGUMENT_TYPE.VARIABLE_NAME], true, - ), + SlashCommandNamedArgument.fromProps({ + name: 'key', + description: 'variable name or closure that returns a variable name', + typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.CLOSURE], + enumProvider: commonEnumProviders.variables('local'), + }), ], helpString: `
@@ -1321,13 +1419,17 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'flushglobalvar', - callback: (_, value) => deleteGlobalVariable(value), + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'flushglobalvar', + callback: async (_, value) => deleteGlobalVariable(value instanceof SlashCommandClosure ? (await value.execute())?.pipe : String(value)), namedArgumentList: [], unnamedArgumentList: [ - new SlashCommandArgument( - 'key', [ARGUMENT_TYPE.VARIABLE_NAME], true, - ), + SlashCommandNamedArgument.fromProps({ + name: 'key', + description: 'variable name or closure that returns a variable name', + typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.CLOSURE], + enumProvider: commonEnumProviders.variables('global'), + }), ], helpString: `
@@ -1344,13 +1446,19 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'add', + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'add', callback: addValuesCallback, returns: 'sum of the provided values', unnamedArgumentList: [ - new SlashCommandArgument( - 'values', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], true, true, - ), + SlashCommandArgument.fromProps({ + description: 'values to sum', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: true, + acceptsMultiple: true, + enumProvider: commonEnumProviders.variables('all'), + forceEnum: false, + }), ], helpString: `
@@ -1367,16 +1475,19 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'mul', + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'mul', callback: (args, value) => mulValuesCallback(args, value), - result: 'product of the provided values', + returns: 'product of the provided values', unnamedArgumentList: [ - new SlashCommandArgument( - 'values to multiply', - [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], - true, - true, - ), + SlashCommandArgument.fromProps({ + description: 'values to multiply', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: true, + acceptsMultiple: true, + enumProvider: commonEnumProviders.variables('all'), + forceEnum: false, + }), ], helpString: `
@@ -1392,16 +1503,19 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'max', + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'max', callback: maxValuesCallback, returns: 'maximum value of the set of values', unnamedArgumentList: [ - new SlashCommandArgument( - 'values', - [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], - true, - true, - ), + SlashCommandArgument.fromProps({ + description: 'values to find the max', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: true, + acceptsMultiple: true, + enumProvider: commonEnumProviders.variables('all'), + forceEnum: false, + }), ], helpString: `
@@ -1417,13 +1531,19 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'min', + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'min', callback: minValuesCallback, returns: 'minimum value of the set of values', unnamedArgumentList: [ - new SlashCommandArgument( - 'values', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], true, true, - ), + SlashCommandArgument.fromProps({ + description: 'values to find the min', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: true, + acceptsMultiple: true, + enumProvider: commonEnumProviders.variables('all'), + forceEnum: false, + }), ], helpString: `
@@ -1440,13 +1560,19 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'sub', + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'sub', callback: subValuesCallback, returns: 'difference of the provided values', unnamedArgumentList: [ - new SlashCommandArgument( - 'values', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], true, true, - ), + SlashCommandArgument.fromProps({ + description: 'values to find the difference', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: true, + acceptsMultiple: true, + enumProvider: commonEnumProviders.variables('all'), + forceEnum: false, + }), ], helpString: `
@@ -1463,16 +1589,25 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'div', + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'div', callback: divValuesCallback, returns: 'result of division', unnamedArgumentList: [ - new SlashCommandArgument( - 'dividend', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], true, - ), - new SlashCommandArgument( - 'divisor', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], true, - ), + SlashCommandArgument.fromProps({ + description: 'dividend', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: true, + enumProvider: commonEnumProviders.variables('all'), + forceEnum: false, + }), + SlashCommandArgument.fromProps({ + description: 'divisor', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: true, + enumProvider: commonEnumProviders.variables('all'), + forceEnum: false, + }), ], helpString: `
@@ -1489,16 +1624,25 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'mod', + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'mod', callback: modValuesCallback, returns: 'result of modulo operation', unnamedArgumentList: [ - new SlashCommandArgument( - 'dividend', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], true, - ), - new SlashCommandArgument( - 'divisor', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], true, - ), + SlashCommandArgument.fromProps({ + description: 'dividend', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: true, + enumProvider: commonEnumProviders.variables('all'), + forceEnum: false, + }), + SlashCommandArgument.fromProps({ + description: 'divisor', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: true, + enumProvider: commonEnumProviders.variables('all'), + forceEnum: false, + }), ], helpString: `
@@ -1515,16 +1659,25 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'pow', + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'pow', callback: powValuesCallback, returns: 'result of power operation', unnamedArgumentList: [ - new SlashCommandArgument( - 'base', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], true, - ), - new SlashCommandArgument( - 'exponent', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], true, - ), + SlashCommandArgument.fromProps({ + description: 'base', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: true, + enumProvider: commonEnumProviders.variables('all'), + forceEnum: false, + }), + SlashCommandArgument.fromProps({ + description: 'exponent', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: true, + enumProvider: commonEnumProviders.variables('all'), + forceEnum: false, + }), ], helpString: `
@@ -1541,13 +1694,18 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'sin', + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'sin', callback: sinValuesCallback, returns: 'sine of the provided value', unnamedArgumentList: [ - new SlashCommandArgument( - 'value', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], true, - ), + SlashCommandArgument.fromProps({ + description: 'value', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: true, + enumProvider: commonEnumProviders.variables('all'), + forceEnum: false, + }), ], helpString: `
@@ -1564,13 +1722,18 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'cos', + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'cos', callback: cosValuesCallback, returns: 'cosine of the provided value', unnamedArgumentList: [ - new SlashCommandArgument( - 'value', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], true, - ), + SlashCommandArgument.fromProps({ + description: 'value', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: true, + enumProvider: commonEnumProviders.variables('all'), + forceEnum: false, + }), ], helpString: `
@@ -1587,14 +1750,19 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'log', + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'log', callback: logValuesCallback, returns: 'log of the provided value', namedArgumentList: [], unnamedArgumentList: [ - new SlashCommandArgument( - 'value', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], true, - ), + SlashCommandArgument.fromProps({ + description: 'value', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: true, + enumProvider: commonEnumProviders.variables('all'), + forceEnum: false, + }), ], helpString: `
@@ -1611,13 +1779,18 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'abs', + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'abs', callback: absValuesCallback, returns: 'absolute value of the provided value', unnamedArgumentList: [ - new SlashCommandArgument( - 'value', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], true, - ), + SlashCommandArgument.fromProps({ + description: 'value', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: true, + enumProvider: commonEnumProviders.variables('all'), + forceEnum: false, + }), ], helpString: `
@@ -1634,13 +1807,18 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'sqrt', + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'sqrt', callback: sqrtValuesCallback, returns: 'square root of the provided value', unnamedArgumentList: [ - new SlashCommandArgument( - 'value', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], true, - ), + SlashCommandArgument.fromProps({ + description: 'value', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: true, + enumProvider: commonEnumProviders.variables('all'), + forceEnum: false, + }), ], helpString: `
@@ -1657,13 +1835,18 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'round', + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'round', callback: roundValuesCallback, returns: 'rounded value', unnamedArgumentList: [ - new SlashCommandArgument( - 'value', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], true, - ), + SlashCommandArgument.fromProps({ + description: 'value', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: true, + enumProvider: commonEnumProviders.variables('all'), + forceEnum: false, + }), ], helpString: `
@@ -1680,13 +1863,18 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'len', - callback: (_, value) => lenValuesCallback(value), + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'len', + callback: (_, value) => String(lenValuesCallback(value)), returns: 'length of the provided value', unnamedArgumentList: [ - new SlashCommandArgument( - 'value', [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.VARIABLE_NAME], true, - ), + SlashCommandArgument.fromProps({ + description: 'value', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: true, + enumProvider: commonEnumProviders.variables('all'), + forceEnum: false, + }), ], helpString: `
@@ -1702,8 +1890,9 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'rand', - callback: (args, value) => randValuesCallback(Number(args.from ?? 0), Number(args.to ?? (value.length ? value : 1)), args), + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'rand', + callback: (args, value) => String(randValuesCallback(Number(args.from ?? 0), Number(args.to ?? (value ? value : 1)), args)), returns: 'random number', namedArgumentList: [ new SlashCommandNamedArgument( @@ -1755,13 +1944,18 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'var', - callback: (args, value) => varCallback(args, value), + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'var', + callback: (/** @type {NamedArguments} */ args, value) => varCallback(args, value), returns: 'the variable value', namedArgumentList: [ - new SlashCommandNamedArgument( - 'key', 'variable name; forces setting the variable, even if no value is provided', [ARGUMENT_TYPE.VARIABLE_NAME], false, - ), + SlashCommandNamedArgument.fromProps({ + name: 'key', + description: 'variable name; forces setting the variable, even if no value is provided', + typeList: [ARGUMENT_TYPE.VARIABLE_NAME], + enumProvider: commonEnumProviders.variables('scope'), + forceEnum: false, + }), new SlashCommandNamedArgument( 'index', 'optional index for list or dictionary', @@ -1771,12 +1965,12 @@ export function registerVariableCommands() { ), ], unnamedArgumentList: [ - new SlashCommandArgument( - 'variable name', - [ARGUMENT_TYPE.VARIABLE_NAME], - false, // isRequired - false, // acceptsMultiple - ), + SlashCommandArgument.fromProps({ + description: 'variable name', + typeList: [ARGUMENT_TYPE.VARIABLE_NAME], + enumProvider: commonEnumProviders.variables('scope'), + forceEnum: false, + }), new SlashCommandArgument( 'variable value', [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.BOOLEAN, ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.DICTIONARY, ARGUMENT_TYPE.CLOSURE], @@ -1802,18 +1996,26 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'let', - callback: (args, value) => letCallback(args, value), + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'let', + callback: (/** @type {NamedArguments} */ args, value) => letCallback(args, value), returns: 'the variable value', namedArgumentList: [ - new SlashCommandNamedArgument( - 'key', 'variable name', [ARGUMENT_TYPE.VARIABLE_NAME], false, - ), + SlashCommandNamedArgument.fromProps({ + name: 'key', + description: 'variable name', + typeList: [ARGUMENT_TYPE.VARIABLE_NAME], + enumProvider: commonEnumProviders.variables('scope'), + forceEnum: false, + }), ], unnamedArgumentList: [ - new SlashCommandArgument( - 'variable name', [ARGUMENT_TYPE.VARIABLE_NAME], false, - ), + SlashCommandArgument.fromProps({ + description: 'variable name', + typeList: [ARGUMENT_TYPE.VARIABLE_NAME], + enumProvider: commonEnumProviders.variables('scope'), + forceEnum: false, + }), new SlashCommandArgument( 'variable value', [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.BOOLEAN, ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.DICTIONARY, ARGUMENT_TYPE.CLOSURE], ), @@ -1839,16 +2041,18 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'closure-serialize', + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'closure-serialize', /** * - * @param {import('./slash-commands/SlashCommand.js').NamedArguments} args + * @param {NamedArguments} args * @param {SlashCommandClosure} value * @returns {string} */ - callback: (args, value)=>closureSerializeCallback(args, value), + callback: (args, value) => closureSerializeCallback(args, value), unnamedArgumentList: [ - SlashCommandArgument.fromProps({ description: 'the closure to serialize', + SlashCommandArgument.fromProps({ + description: 'the closure to serialize', typeList: [ARGUMENT_TYPE.CLOSURE], isRequired: true, }), @@ -1868,15 +2072,17 @@ export function registerVariableCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'closure-deserialize', + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'closure-deserialize', /** - * @param {import('./slash-commands/SlashCommand.js').NamedArguments} args - * @param {import('./slash-commands/SlashCommand.js').UnnamedArguments} value + * @param {NamedArguments} args + * @param {UnnamedArguments} value * @returns {SlashCommandClosure} */ - callback: (args, value)=>closureDeserializeCallback(args, value), + callback: (args, value) => closureDeserializeCallback(args, value), unnamedArgumentList: [ - SlashCommandArgument.fromProps({ description: 'serialized closure', + SlashCommandArgument.fromProps({ + description: 'serialized closure', typeList: [ARGUMENT_TYPE.STRING], isRequired: true, }), diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index 9908208c9..491b90ff0 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -13,6 +13,10 @@ import { getRegexedString, regex_placement } from './extensions/regex/engine.js' import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { SlashCommand } from './slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; +import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js'; +import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; +import { SlashCommandExecutor } from './slash-commands/SlashCommandExecutor.js'; +import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; export { world_info, @@ -962,21 +966,56 @@ function registerWorldInfoSlashCommands() { return ''; } + /** A collection of local enum providers for this context of world info */ + const localEnumProviders = { + /** All possible fields that can be set in a WI entry */ + wiEntryFields: () => Object.entries(newEntryDefinition).map(([key, value]) => + new SlashCommandEnumValue(key, `[${value.type}] default: ${(typeof value.default === 'string' ? `'${value.default}'` : value.default)}`, + enumTypes.enum, enumIcons.getDataTypeIcon(value.type))), + + /** All existing UIDs based on the file argument as world name */ + wiUids: (/** @type {SlashCommandExecutor} */ executor) => { + const file = executor.namedArgumentList.find(it => it.name == 'file')?.value; + if (file instanceof SlashCommandClosure) throw new Error('Argument \'file\' does not support closures'); + // Try find world from cache + const world = worldInfoCache[file]; + if (!world) return []; + return Object.entries(world.entries).map(([uid, data]) => + new SlashCommandEnumValue(uid, `${data.comment ? `${data.comment}: ` : ''}${data.key.join(', ')}${data.keysecondary?.length ? ` [${Object.entries(world_info_logic).find(([_, value]) => value == data.selectiveLogic)[0]}] ${data.keysecondary.join(', ')}` : ''} [${getWiPositionString(data)}]`, + enumTypes.enum, enumIcons.getWiStatusIcon(data))); + }, + }; + + function getWiPositionString(entry) { + switch (entry.position) { + case world_info_position.before: return '↑Char'; + case world_info_position.after: return '↓Char'; + case world_info_position.EMTop: return '↑EM'; + case world_info_position.EMBottom: return '↓EM'; + case world_info_position.ANTop: return '↑AT'; + case world_info_position.ANBottom: return '↓AT'; + case world_info_position.atDepth: return `@D${enumIcons.getRoleIcon(entry.role)}`; + default: return ''; + } + } + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'world', callback: onWorldInfoChange, namedArgumentList: [ new SlashCommandNamedArgument( - 'state', 'set world state', [ARGUMENT_TYPE.STRING], false, false, null, ['off', 'toggle'], + 'state', 'set world state', [ARGUMENT_TYPE.STRING], false, false, null, commonEnumProviders.boolean('onOffToggle')(), ), new SlashCommandNamedArgument( 'silent', 'suppress toast messages', [ARGUMENT_TYPE.BOOLEAN], false, ), ], unnamedArgumentList: [ - new SlashCommandArgument( - 'name', [ARGUMENT_TYPE.STRING], false, - ), + SlashCommandArgument.fromProps({ + description: 'world name', + typeList: [ARGUMENT_TYPE.STRING], + enumProvider: commonEnumProviders.worlds, + }), ], helpString: `
@@ -992,18 +1031,27 @@ function registerWorldInfoSlashCommands() { helpString: 'Get a name of the chat-bound lorebook or create a new one if was unbound, and pass it down the pipe.', aliases: ['getchatlore', 'getchatwi'], })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'findentry', aliases: ['findlore', 'findwi'], returns: 'UID', callback: findBookEntryCallback, namedArgumentList: [ - new SlashCommandNamedArgument( - 'file', 'bookName', ARGUMENT_TYPE.STRING, true, - ), - new SlashCommandNamedArgument( - 'field', 'field value for fuzzy match (default: key)', ARGUMENT_TYPE.STRING, false, false, 'key', - ), + SlashCommandNamedArgument.fromProps({ + name: 'file', + description: 'book name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: commonEnumProviders.worlds, + }), + SlashCommandNamedArgument.fromProps({ + name: 'field', + description: 'field value for fuzzy match (default: key)', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: 'key', + enumList: localEnumProviders.wiEntryFields(), + }), ], unnamedArgumentList: [ new SlashCommandArgument( @@ -1030,17 +1078,28 @@ function registerWorldInfoSlashCommands() { callback: getEntryFieldCallback, returns: 'field value', namedArgumentList: [ - new SlashCommandNamedArgument( - 'file', 'bookName', ARGUMENT_TYPE.STRING, true, - ), - new SlashCommandNamedArgument( - 'field', 'field to retrieve (default: content)', ARGUMENT_TYPE.STRING, false, false, 'content', - ), + SlashCommandNamedArgument.fromProps({ + name: 'file', + description: 'book name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: commonEnumProviders.worlds, + }), + SlashCommandNamedArgument.fromProps({ + name: 'field', + description: 'field to retrieve (default: content)', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: 'content', + enumList: localEnumProviders.wiEntryFields(), + }), ], unnamedArgumentList: [ - new SlashCommandArgument( - 'UID', ARGUMENT_TYPE.STRING, true, - ), + SlashCommandArgument.fromProps({ + description: 'record UID', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: localEnumProviders.wiUids, + }), ], helpString: `
@@ -1062,9 +1121,13 @@ function registerWorldInfoSlashCommands() { aliases: ['createlore', 'createwi'], returns: 'UID of the new record', namedArgumentList: [ - new SlashCommandNamedArgument( - 'file', 'book name', [ARGUMENT_TYPE.STRING], true, - ), + SlashCommandNamedArgument.fromProps({ + name: 'file', + description: 'book name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: commonEnumProviders.worlds, + }), new SlashCommandNamedArgument( 'key', 'record key', [ARGUMENT_TYPE.STRING], false, ), @@ -1093,15 +1156,27 @@ function registerWorldInfoSlashCommands() { callback: setEntryFieldCallback, aliases: ['setlorefield', 'setwifield'], namedArgumentList: [ - new SlashCommandNamedArgument( - 'file', 'book name', [ARGUMENT_TYPE.STRING], true, - ), - new SlashCommandNamedArgument( - 'uid', 'record UID', [ARGUMENT_TYPE.STRING], true, - ), - new SlashCommandNamedArgument( - 'field', 'field name', [ARGUMENT_TYPE.STRING], true, false, 'content', - ), + SlashCommandNamedArgument.fromProps({ + name: 'file', + description: 'book name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: commonEnumProviders.worlds, + }), + SlashCommandNamedArgument.fromProps({ + name: 'uid', + description: 'record UID', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: localEnumProviders.wiUids, + }), + SlashCommandNamedArgument.fromProps({ + name: 'field', + description: 'field name (default: content)', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: 'content', + enumList: localEnumProviders.wiEntryFields(), + }), ], unnamedArgumentList: [ new SlashCommandArgument( @@ -2748,38 +2823,49 @@ function deleteWorldInfoEntry(data, uid) { delete data.entries[uid]; } -const newEntryTemplate = { - key: [], - keysecondary: [], - comment: '', - content: '', - constant: false, - vectorized: false, - selective: true, - selectiveLogic: world_info_logic.AND_ANY, - addMemo: false, - order: 100, - position: 0, - disable: false, - excludeRecursion: false, - preventRecursion: false, - delayUntilRecursion: false, - probability: 100, - useProbability: true, - depth: DEFAULT_DEPTH, - group: '', - groupOverride: false, - groupWeight: DEFAULT_WEIGHT, - scanDepth: null, - caseSensitive: null, - matchWholeWords: null, - useGroupScoring: null, - automationId: '', - role: 0, - sticky: null, - cooldown: null, +/** + * Definitions of types for new WI entries + * + * Use `newEntryTemplate` if you just need the template that contains default values + * + * @type {{[key: string]: { default: any, type: string }}} + */ +const newEntryDefinition = { + key: { default: [], type: 'array' }, + keysecondary: { default: [], type: 'array' }, + comment: { default: '', type: 'string' }, + content: { default: '', type: 'string' }, + constant: { default: false, type: 'boolean' }, + vectorized: { default: false, type: 'boolean' }, + selective: { default: true, type: 'boolean' }, + selectiveLogic: { default: world_info_logic.AND_ANY, type: 'enum' }, + addMemo: { default: false, type: 'boolean' }, + order: { default: 100, type: 'number' }, + position: { default: 0, type: 'number' }, + disable: { default: false, type: 'boolean' }, + excludeRecursion: { default: false, type: 'boolean' }, + preventRecursion: { default: false, type: 'boolean' }, + delayUntilRecursion: { default: false, type: 'boolean' }, + probability: { default: 100, type: 'number' }, + useProbability: { default: true, type: 'boolean' }, + depth: { default: DEFAULT_DEPTH, type: 'number' }, + group: { default: '', type: 'string' }, + groupOverride: { default: false, type: 'boolean' }, + groupWeight: { default: DEFAULT_WEIGHT, type: 'number' }, + scanDepth: { default: null, type: 'number?' }, + caseSensitive: { default: null, type: 'boolean?' }, + matchWholeWords: { default: null, type: 'boolean?' }, + useGroupScoring: { default: null, type: 'boolean?' }, + automationId: { default: '', type: 'string' }, + role: { default: 0, type: 'enum' }, + sticky: { default: null, type: 'number?' }, + cooldown: { default: null, type: 'number?' }, }; +const newEntryTemplate = Object.fromEntries( + Object.entries(newEntryDefinition).map(([key, value]) => [key, value.default]), +); + function createWorldInfoEntry(_name, data) { const newUid = getFreeWorldEntryUid(data); @@ -3829,6 +3915,7 @@ function onWorldInfoChange(args, text) { } break; } + case 'on': default: { selected_world_info.push(name); wiElement.prop('selected', true); diff --git a/public/style.css b/public/style.css index 96a1ef69e..6884f5929 100644 --- a/public/style.css +++ b/public/style.css @@ -366,16 +366,6 @@ input[type='checkbox']:focus-visible { font-weight: bold; } -.img_enlarged_container { - display: flex; - flex-direction: column; - justify-content: flex-end; - padding: 10px; - height: 100%; - width: 100%; -} - -.img_enlarged_container pre code, .mes_text pre code { position: relative; display: block; @@ -1521,6 +1511,14 @@ body[data-stscript-style] .autoComplete [data-option-type] { &[data-option-type="macro"] .type { color: var(--ac-color-variableLanguage); } + + &[data-option-type="number"] .type { + color: var(--ac-color-number); + } + + &[data-option-type="name"] .type { + color: var(--ac-color-type); + } } body[data-stscript-style] .hljs.language-stscript { @@ -1630,6 +1628,11 @@ body[data-stscript-style] .hljs.language-stscript { gap: 0.5em; display: contents; + >.stopgap { + opacity: 0.75; + display: none; + } + @container (max-width: 80em) { .specs { grid-column: 2 / 4; @@ -1641,6 +1644,10 @@ body[data-stscript-style] .hljs.language-stscript { opacity: 0.75; height: auto; } + + >.stopgap { + display: inline-block; + } } &.blank { @@ -3144,6 +3151,16 @@ grammarly-extension { min-width: 750px; } +.transparent_dialogue_popup { + background-color: transparent; + box-shadow: none; + border: none; +} + +.transparent_dialogue_popup:focus-visible { + outline: none; +} + #dialogue_popup .horizontal_scrolling_dialogue_popup { overflow-x: unset !important; } @@ -4457,38 +4474,106 @@ a { .mes_img_controls { position: absolute; - top: 0.5em; + top: 0.1em; left: 0; width: 100%; - display: none; + display: flex; + opacity: 0; flex-direction: row; justify-content: space-between; padding: 1em; } .mes_img_controls .right_menu_button { - padding: 0; filter: brightness(80%); + padding: 1px; + height: 1.25em; + width: 1.25em; +} + +.mes_img_controls .right_menu_button::before { + /* Fix weird alignment with this font-awesome icons on focus */ + position: relative; + top: 0.6125em; } .mes_img_controls .right_menu_button:hover { filter: brightness(150%); } -.mes_img_container:hover .mes_img_controls { - display: flex; +.mes_img_container:hover .mes_img_controls, +.mes_img_container:focus-within .mes_img_controls { + opacity: 1; } .mes .mes_img_container.img_extra { display: flex; } +.img_enlarged_holder { + /* Scaling via flex-grow and object-fit only works if we have some kind of base-height set */ + min-height: 120px; +} + +.img_enlarged_holder:has(.zoomed) { + overflow: auto; +} + .img_enlarged { - max-width: 100%; - max-height: 100%; - border-radius: 2px; - border: 1px solid transparent; object-fit: contain; + width: 100%; + height: 100%; + cursor: zoom-in +} + +.img_enlarged.zoomed { + object-fit: cover; + width: auto; + height: auto; + cursor: zoom-out; +} + +.img_enlarged_container { + display: flex; + flex-direction: column; + justify-content: center; + padding: 10px; + height: 100%; + width: 100%; +} + +.img_enlarged_holder::-webkit-scrollbar-corner { + background-color: transparent; +} + +.img_enlarged_container pre code { + position: relative; + display: block; + overflow-x: auto; + padding: 1em; +} + +.popup:has(.img_enlarged.zoomed).large_dialogue_popup { + height: 100vh !important; + height: 100svh !important; + max-height: 100vh !important; + max-height: 100svh !important; + max-width: 100vw !important; + max-width: 100svw !important; + padding: 0; +} + +.popup:has(.img_enlarged.zoomed).large_dialogue_popup .popup-content { + margin: 0; + padding: 0; +} + +.popup:has(.img_enlarged.zoomed).large_dialogue_popup .img_enlarged_container pre { + display: none; +} + +.popup:has(.img_enlarged.zoomed).large_dialogue_popup .popup-button-close { + display: none !important; } .cropper-container {