diff --git a/public/script.js b/public/script.js index 8d085b6d2..b5a703f33 100644 --- a/public/script.js +++ b/public/script.js @@ -4785,7 +4785,7 @@ export function removeMacros(str) { * @param {boolean} [compact] Send as a compact display message. * @param {string} [name] Name of the user sending the message. Defaults to name1. * @param {string} [avatar] Avatar of the user sending the message. Defaults to user_avatar. - * @returns {Promise} A promise that resolves when the message is inserted. + * @returns {Promise} A promise that resolves to the message when it is inserted. */ export async function sendMessageAsUser(messageText, messageBias, insertAt = null, compact = false, name = name1, avatar = user_avatar) { messageText = getRegexedString(messageText, regex_placement.USER_INPUT); @@ -4832,6 +4832,8 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul await eventSource.emit(event_types.USER_MESSAGE_RENDERED, chat_id); await saveChatConditional(); } + + return message; } /** diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index edb19ff05..4a09e3aeb 100644 --- a/public/scripts/extensions/expressions/index.js +++ b/public/scripts/extensions/expressions/index.js @@ -12,6 +12,8 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from ' import { isFunctionCallingSupported } from '../../openai.js'; import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js'; import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; +import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js'; +import { SlashCommandClosure } from '../../slash-commands/SlashCommandClosure.js'; export { MODULE_NAME }; const MODULE_NAME = 'expressions'; @@ -2134,18 +2136,42 @@ function migrateSettings() { name: 'classify-expressions', aliases: ['expressions'], callback: async (args) => { - const list = await getExpressionsList(); - switch (String(args.format).toLowerCase()) { - case 'json': - return JSON.stringify(list); - default: - return list.join(', '); + /** @type {import('../../slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */ + // @ts-ignore + let returnType = args.return; + + // Old legacy return type handling + if (args.format) { + toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning'); + const type = String(args?.format).toLowerCase().trim(); + switch (type) { + case 'json': + returnType = 'object'; + break; + default: + returnType = 'pipe'; + break; + } } + + // Now the actual new return type handling + const list = await getExpressionsList(); + + return await slashCommandReturnHelper.doReturn(returnType ?? 'pipe', list, { objectToStringFunc: list => list.join(', ') }); }, namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'return', + description: 'The way how you want the return value to be provided', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: 'pipe', + enumList: slashCommandReturnHelper.enumList({ allowObject: true }), + forceEnum: true, + }), + // TODO remove some day SlashCommandNamedArgument.fromProps({ name: 'format', - description: 'The format to return the list in: comma-separated plain text or JSON array. Default is plain text.', + description: '!!! DEPRECATED - use "return" instead !!! The format to return the list in: comma-separated plain text or JSON array. Default is plain text.', typeList: [ARGUMENT_TYPE.STRING], enumList: [ new SlashCommandEnumValue('plain', null, enumTypes.enum, ', '), diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 0a30f24da..71fe8c2c7 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -70,6 +70,7 @@ import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js'; import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js'; +import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js'; export { executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand, }; @@ -242,6 +243,7 @@ export function initDefaultSlashCommands() { SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'sendas', callback: sendMessageAs, + returns: 'Optionally the text of the sent message, if specified in the "return" argument', namedArgumentList: [ SlashCommandNamedArgument.fromProps({ name: 'name', @@ -269,6 +271,14 @@ export function initDefaultSlashCommands() { typeList: [ARGUMENT_TYPE.NUMBER], enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), }), + SlashCommandNamedArgument.fromProps({ + name: 'return', + description: 'The way how you want the return value to be provided', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: 'none', + enumList: slashCommandReturnHelper.enumList({ allowObject: true }), + forceEnum: true, + }), ], unnamedArgumentList: [ new SlashCommandArgument( @@ -301,6 +311,7 @@ export function initDefaultSlashCommands() { name: 'sys', callback: sendNarratorMessage, aliases: ['nar'], + returns: 'Optionally the text of the sent message, if specified in the "return" argument', namedArgumentList: [ new SlashCommandNamedArgument( 'compact', @@ -316,6 +327,14 @@ export function initDefaultSlashCommands() { typeList: [ARGUMENT_TYPE.NUMBER], enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), }), + SlashCommandNamedArgument.fromProps({ + name: 'return', + description: 'The way how you want the return value to be provided', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: 'none', + enumList: slashCommandReturnHelper.enumList({ allowObject: true }), + forceEnum: true, + }), ], unnamedArgumentList: [ new SlashCommandArgument( @@ -355,6 +374,7 @@ export function initDefaultSlashCommands() { SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'comment', callback: sendCommentMessage, + returns: 'Optionally the text of the sent message, if specified in the "return" argument', namedArgumentList: [ new SlashCommandNamedArgument( 'compact', @@ -370,6 +390,14 @@ export function initDefaultSlashCommands() { typeList: [ARGUMENT_TYPE.NUMBER], enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), }), + SlashCommandNamedArgument.fromProps({ + name: 'return', + description: 'The way how you want the return value to be provided', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: 'none', + enumList: slashCommandReturnHelper.enumList({ allowObject: true }), + forceEnum: true, + }), ], unnamedArgumentList: [ new SlashCommandArgument( @@ -509,7 +537,7 @@ export function initDefaultSlashCommands() { SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'ask', callback: askCharacter, - returns: 'the generated text', + returns: 'Optionally the text of the sent message, if specified in the "return" argument', namedArgumentList: [ SlashCommandNamedArgument.fromProps({ name: 'name', @@ -518,6 +546,14 @@ export function initDefaultSlashCommands() { isRequired: true, enumProvider: commonEnumProviders.characters('character'), }), + SlashCommandNamedArgument.fromProps({ + name: 'return', + description: 'The way how you want the return value to be provided', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: 'pipe', + enumList: slashCommandReturnHelper.enumList({ allowObject: true }), + forceEnum: true, + }), ], unnamedArgumentList: [ new SlashCommandArgument( @@ -556,6 +592,7 @@ export function initDefaultSlashCommands() { SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'send', callback: sendUserMessageCallback, + returns: 'Optionally the text of the sent message, if specified in the "return" argument', namedArgumentList: [ new SlashCommandNamedArgument( 'compact', @@ -578,6 +615,14 @@ export function initDefaultSlashCommands() { defaultValue: '{{user}}', enumProvider: commonEnumProviders.personas, }), + SlashCommandNamedArgument.fromProps({ + name: 'return', + description: 'The way how you want the return value to be provided', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: 'none', + enumList: slashCommandReturnHelper.enumList({ allowObject: true }), + forceEnum: true, + }), ], unnamedArgumentList: [ new SlashCommandArgument( @@ -1568,12 +1613,21 @@ export function initDefaultSlashCommands() { SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'listinjects', callback: listInjectsCallback, - helpString: 'Lists all script injections for the current chat. Displays injects in a popup by default. Use the format argument to change the output format.', - returns: 'JSON object of script injections', + helpString: 'Lists all script injections for the current chat. Displays injects in a popup by default. Use the return argument to change the return type.', + returns: 'Optionalls the JSON object of script injections', namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'return', + description: 'The way how you want the return value to be provided', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: 'popup-html', + enumList: slashCommandReturnHelper.enumList({ allowPipe: false, allowObject: true, allowChat: true, allowPopup: true, allowTextVersion: false }), + forceEnum: true, + }), + // TODO remove some day SlashCommandNamedArgument.fromProps({ name: 'format', - description: 'output format', + description: '!!! DEPRECATED - use "return" instead !!! output format', typeList: [ARGUMENT_TYPE.STRING], isRequired: true, forceEnum: true, @@ -1842,37 +1896,43 @@ function injectCallback(args, value) { } async function listInjectsCallback(args) { - const type = String(args?.format).toLowerCase().trim(); - if (!chat_metadata.script_injects || !Object.keys(chat_metadata.script_injects).length) { - type !== 'none' && toastr.info('No script injections for the current chat'); - return JSON.stringify({}); + /** @type {import('./slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */ + let returnType = args.return; + + // Old legacy return type handling + if (args.format) { + toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning'); + const type = String(args?.format).toLowerCase().trim(); + if (!chat_metadata.script_injects || !Object.keys(chat_metadata.script_injects).length) { + type !== 'none' && toastr.info('No script injections for the current chat'); + } + switch (type) { + case 'none': + returnType = 'none'; + break; + case 'chat': + returnType = 'chat-html'; + break; + case 'popup': + default: + returnType = 'popup-html'; + break; + } } - const injects = Object.entries(chat_metadata.script_injects) - .map(([id, inject]) => { - const position = Object.entries(extension_prompt_types); - const positionName = position.find(([_, value]) => value === inject.position)?.[0] ?? 'unknown'; - return `* **${id}**: ${inject.value} (${positionName}, depth: ${inject.depth}, scan: ${inject.scan ?? false}, role: ${inject.role ?? extension_prompt_roles.SYSTEM})`; - }) - .join('\n'); + // Now the actual new return type handling + const buildTextValue = (injects) => { + const injectsStr = Object.entries(injects) + .map(([id, inject]) => { + const position = Object.entries(extension_prompt_types); + const positionName = position.find(([_, value]) => value === inject.position)?.[0] ?? 'unknown'; + return `* **${id}**: ${inject.value} (${positionName}, depth: ${inject.depth}, scan: ${inject.scan ?? false}, role: ${inject.role ?? extension_prompt_roles.SYSTEM})`; + }) + .join('\n'); + return `### Script injections:\n${injectsStr || 'No script injections for the current chat'}`; + }; - const converter = new showdown.Converter(); - const messageText = `### Script injections:\n${injects}`; - const htmlMessage = DOMPurify.sanitize(converter.makeHtml(messageText)); - - switch (type) { - case 'none': - break; - case 'chat': - sendSystemMessage(system_message_types.GENERIC, htmlMessage); - break; - case 'popup': - default: - await callGenericPopup(htmlMessage, POPUP_TYPE.TEXT); - break; - } - - return JSON.stringify(chat_metadata.script_injects); + return await slashCommandReturnHelper.doReturn(returnType ?? 'popup-html', chat_metadata.script_injects ?? {}, { objectToStringFunc: buildTextValue }); } /** @@ -2559,7 +2619,7 @@ async function askCharacter(args, text) { // Not supported in group chats // TODO: Maybe support group chats? if (selected_group) { - toastr.error('Cannot run /ask command in a group chat!'); + toastr.warning('Cannot run /ask command in a group chat!'); return ''; } @@ -2633,7 +2693,9 @@ async function askCharacter(args, text) { } } - return askResult; + const message = askResult ? chat[chat.length - 1] : null; + + return await slashCommandReturnHelper.doReturn(args.return ?? 'pipe', message, { objectToStringFunc: x => x.mes }); } async function hideMessageCallback(_, arg) { @@ -2908,7 +2970,7 @@ function findPersonaByName(name) { async function sendUserMessageCallback(args, text) { if (!text) { - console.warn('WARN: No text provided for /send command'); + toastr.warning('You must specify text to send'); return; } @@ -2924,16 +2986,17 @@ async function sendUserMessageCallback(args, text) { insertAt = chat.length + insertAt; } + let message; if ('name' in args) { const name = args.name || ''; const avatar = findPersonaByName(name) || user_avatar; - await sendMessageAsUser(text, bias, insertAt, compact, name, avatar); + message = await sendMessageAsUser(text, bias, insertAt, compact, name, avatar); } else { - await sendMessageAsUser(text, bias, insertAt, compact); + message = await sendMessageAsUser(text, bias, insertAt, compact); } - return ''; + return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes }); } async function deleteMessagesByNameCallback(_, name) { @@ -3221,30 +3284,20 @@ export function getNameAndAvatarForMessage(character, name = null) { export async function sendMessageAs(args, text) { if (!text) { + toastr.warning('You must specify text to send as'); return ''; } - let name; + let name = args.name?.trim(); let mesText; - if (args.name) { - name = args.name.trim(); - - if (!name && !text) { - toastr.warning('You must specify a name and text to send as'); - return ''; - } - } else { + if (!name) { const namelessWarningKey = 'sendAsNamelessWarningShown'; if (localStorage.getItem(namelessWarningKey) !== 'true') { toastr.warning('To avoid confusion, please use /sendas name="Character Name"', 'Name defaulted to {{char}}', { timeOut: 10000 }); localStorage.setItem(namelessWarningKey, 'true'); } name = name2; - if (!text) { - toastr.warning('You must specify text to send as'); - return ''; - } } mesText = text.trim(); @@ -3321,11 +3374,12 @@ export async function sendMessageAs(args, text) { await saveChatConditional(); } - return ''; + return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes }); } export async function sendNarratorMessage(args, text) { if (!text) { + toastr.warning('You must specify text to send'); return ''; } @@ -3374,7 +3428,7 @@ export async function sendNarratorMessage(args, text) { await saveChatConditional(); } - return ''; + return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes }); } export async function promptQuietForLoudResponse(who, text) { @@ -3420,6 +3474,7 @@ export async function promptQuietForLoudResponse(who, text) { async function sendCommentMessage(args, text) { if (!text) { + toastr.warning('You must specify text to send'); return ''; } @@ -3462,7 +3517,7 @@ async function sendCommentMessage(args, text) { await saveChatConditional(); } - return ''; + return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes }); } /** diff --git a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js index 5a40965c1..b96b83a53 100644 --- a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js +++ b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js @@ -36,6 +36,7 @@ export const enumIcons = { message: 'đŸ’Ŧ', voice: '🎤', server: 'đŸ–Ĩī¸', + popup: '🗔', true: '✔ī¸', false: '❌', diff --git a/public/scripts/slash-commands/SlashCommandReturnHelper.js b/public/scripts/slash-commands/SlashCommandReturnHelper.js new file mode 100644 index 000000000..3601c30cd --- /dev/null +++ b/public/scripts/slash-commands/SlashCommandReturnHelper.js @@ -0,0 +1,80 @@ +import { sendSystemMessage, system_message_types } from '../../script.js'; +import { callGenericPopup, POPUP_TYPE } from '../popup.js'; +import { escapeHtml } from '../utils.js'; +import { enumIcons } from './SlashCommandCommonEnumsProvider.js'; +import { enumTypes, SlashCommandEnumValue } from './SlashCommandEnumValue.js'; + +/** @typedef {'pipe'|'object'|'chat-html'|'chat-text'|'popup-html'|'popup-text'|'toast-html'|'toast-text'|'console'|'none'} SlashCommandReturnType */ + +export const slashCommandReturnHelper = { + // Without this, VSCode formatter fucks up JS docs. Don't ask me why. + _: false, + + /** + * Gets/creates the enum list of types of return relevant for a slash command + * + * @param {object} [options={}] Options + * @param {boolean} [options.allowPipe=true] Allow option to pipe the return value + * @param {boolean} [options.allowObject=false] Allow option to return the value as an object + * @param {boolean} [options.allowChat=false] Allow option to return the value as a chat message + * @param {boolean} [options.allowPopup=false] Allow option to return the value as a popup + * @param {boolean}[options.allowTextVersion=true] Used in combination with chat/popup/toast, some of them do not make sense for text versions, e.g.if you are building a HTML string anyway + * @returns {SlashCommandEnumValue[]} The enum list + */ + enumList: ({ allowPipe = true, allowObject = false, allowChat = false, allowPopup = false, allowTextVersion = true } = {}) => [ + allowPipe && new SlashCommandEnumValue('pipe', 'Return to the pipe for the next command', enumTypes.name, '|'), + allowObject && new SlashCommandEnumValue('object', 'Return as an object (or array) to the pipe for the next command', enumTypes.variable, enumIcons.dictionary), + allowChat && new SlashCommandEnumValue('chat-html', 'Sending a chat message with the return value - Can display HTML', enumTypes.command, enumIcons.message), + allowChat && allowTextVersion && new SlashCommandEnumValue('chat-text', 'Sending a chat message with the return value - Will only display as text', enumTypes.qr, enumIcons.message), + allowPopup && new SlashCommandEnumValue('popup-html', 'Showing as a popup with the return value - Can display HTML', enumTypes.command, enumIcons.popup), + allowPopup && allowTextVersion && new SlashCommandEnumValue('popup-text', 'Showing as a popup with the return value - Will only display as text', enumTypes.qr, enumIcons.popup), + new SlashCommandEnumValue('toast-html', 'Show the return value as a toast notification - Can display HTML', enumTypes.command, 'ℹī¸'), + allowTextVersion && new SlashCommandEnumValue('toast-text', 'Show the return value as a toast notification - Will only display as text', enumTypes.qr, 'ℹī¸'), + new SlashCommandEnumValue('console', 'Log the return value (object, if it can be one) to the console', enumTypes.enum, '>'), + new SlashCommandEnumValue('none', 'No return value'), + ].filter(x => !!x), + + /** + * Handles the return value based on the specified type + * + * @param {SlashCommandReturnType} type The type of return + * @param {object|number|string} value The value to return + * @param {object} [options={}] Options + * @param {(o: object) => string} [options.objectToStringFunc=null] Function to convert the object to a string, if object was provided and 'object' was not the chosen return type + * @param {(o: object) => string} [options.objectToHtmlFunc=null] Analog to 'objectToStringFunc', which will be used here if not provided - but can do a different string layout if HTML is requested + * @returns {Promise<*>} The processed return value + */ + async doReturn(type, value, { objectToStringFunc = o => o?.toString(), objectToHtmlFunc = null } = {}) { + const shouldHtml = type.endsWith('html'); + const actualConverterFunc = shouldHtml && objectToHtmlFunc ? objectToHtmlFunc : objectToStringFunc; + const stringValue = typeof value !== 'string' ? actualConverterFunc(value) : value; + + switch (type) { + case 'popup-html': + case 'popup-text': + case 'chat-text': + case 'chat-html': + case 'toast-text': + case 'toast-html': { + const htmlOrNotHtml = shouldHtml ? DOMPurify.sanitize((new showdown.Converter()).makeHtml(stringValue)) : escapeHtml(stringValue); + + if (type.startsWith('popup')) await callGenericPopup(htmlOrNotHtml, POPUP_TYPE.TEXT); + if (type.startsWith('chat')) sendSystemMessage(system_message_types.GENERIC, htmlOrNotHtml); + if (type.startsWith('toast')) toastr.info(htmlOrNotHtml, null, { escapeHtml: !shouldHtml }); + + return ''; + } + case 'pipe': + return stringValue ?? ''; + case 'object': + return JSON.stringify(value); + case 'console': + console.info(value); + return ''; + case 'none': + return ''; + default: + throw new Error(`Unknown return type: ${type}`); + } + }, +}; diff --git a/public/scripts/variables.js b/public/scripts/variables.js index 7c9830a36..c768c4f5e 100644 --- a/public/scripts/variables.js +++ b/public/scripts/variables.js @@ -11,6 +11,7 @@ import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureR import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js'; import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js'; +import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js'; import { SlashCommandScope } from './slash-commands/SlashCommandScope.js'; import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js'; @@ -305,7 +306,28 @@ export function replaceVariableMacros(input) { } async function listVariablesCallback(args) { - const type = String(args?.format || '').toLowerCase().trim() || 'popup'; + /** @type {import('./slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */ + let returnType = args.return; + + // Old legacy return type handling + if (args.format) { + toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning'); + const type = String(args?.format).toLowerCase().trim(); + switch (type) { + case 'none': + returnType = 'none'; + break; + case 'chat': + returnType = 'chat-html'; + break; + case 'popup': + default: + returnType = 'popup-html'; + break; + } + } + + // Now the actual new return type handling const scope = String(args?.scope || '').toLowerCase().trim() || 'all'; if (!chat_metadata.variables) { chat_metadata.variables = {}; @@ -317,35 +339,24 @@ async function listVariablesCallback(args) { const localVariables = includeLocalVariables ? Object.entries(chat_metadata.variables).map(([name, value]) => `${name}: ${value}`) : []; const globalVariables = includeGlobalVariables ? Object.entries(extension_settings.variables.global).map(([name, value]) => `${name}: ${value}`) : []; + const buildTextValue = (_) => { + const localVariablesString = localVariables.length > 0 ? localVariables.join('\n\n') : 'No local variables'; + const globalVariablesString = globalVariables.length > 0 ? globalVariables.join('\n\n') : 'No global variables'; + const chatName = getCurrentChatId(); + + const message = [ + includeLocalVariables ? `### Local variables (${chatName}):\n${localVariablesString}` : '', + includeGlobalVariables ? `### Global variables:\n${globalVariablesString}` : '', + ].filter(x => x).join('\n\n'); + return message; + }; + const jsonVariables = [ ...Object.entries(chat_metadata.variables).map(x => ({ key: x[0], value: x[1], scope: 'local' })), ...Object.entries(extension_settings.variables.global).map(x => ({ key: x[0], value: x[1], scope: 'global' })), ]; - const localVariablesString = localVariables.length > 0 ? localVariables.join('\n\n') : 'No local variables'; - const globalVariablesString = globalVariables.length > 0 ? globalVariables.join('\n\n') : 'No global variables'; - const chatName = getCurrentChatId(); - - const converter = new showdown.Converter(); - const message = [ - includeLocalVariables ? `### Local variables (${chatName}):\n${localVariablesString}` : '', - includeGlobalVariables ? `### Global variables:\n${globalVariablesString}` : '', - ].filter(x => x).join('\n\n'); - const htmlMessage = DOMPurify.sanitize(converter.makeHtml(message)); - - switch (type) { - case 'none': - break; - case 'chat': - sendSystemMessage(system_message_types.GENERIC, htmlMessage); - break; - case 'popup': - default: - await callGenericPopup(htmlMessage, POPUP_TYPE.TEXT); - break; - } - - return JSON.stringify(jsonVariables); + return await slashCommandReturnHelper.doReturn(returnType ?? 'popup-html', jsonVariables, { objectToStringFunc: buildTextValue }); } /** @@ -916,7 +927,7 @@ export function registerVariableCommands() { name: 'listvar', callback: listVariablesCallback, aliases: ['listchatvar'], - helpString: 'List registered chat variables. Displays variables in a popup by default. Use the format argument to change the output format.', + helpString: 'List registered chat variables. Displays variables in a popup by default. Use the return argument to change the return type.', returns: 'JSON list of local variables', namedArgumentList: [ SlashCommandNamedArgument.fromProps({ @@ -932,9 +943,18 @@ export function registerVariableCommands() { new SlashCommandEnumValue('global', 'Global variables', enumTypes.enum, enumIcons.globalVariable), ], }), + SlashCommandNamedArgument.fromProps({ + name: 'return', + description: 'The way how you want the return value to be provided', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: 'popup-html', + enumList: slashCommandReturnHelper.enumList({ allowPipe: false, allowObject: true, allowChat: true, allowPopup: true, allowTextVersion: false }), + forceEnum: true, + }), + // TODO remove some day SlashCommandNamedArgument.fromProps({ name: 'format', - description: 'output format', + description: '!!! DEPRECATED - use "return" instead !!! output format', typeList: [ARGUMENT_TYPE.STRING], isRequired: true, forceEnum: true,