Merge pull request #2922 from SillyTavern/send-commands-return-value Update various sending commands to return a value
Signed| @@ -4785,7 +4785,7 @@ export function removeMacros(str) { | |||
| 4785 | * @param {boolean} [compact] Send as a compact display message. | 4785 | * @param {boolean} [compact] Send as a compact display message. |
| 4786 | * @param {string} [name] Name of the user sending the message. Defaults to name1. | 4786 | * @param {string} [name] Name of the user sending the message. Defaults to name1. |
| 4787 | * @param {string} [avatar] Avatar of the user sending the message. Defaults to user_avatar. | 4787 | * @param {string} [avatar] Avatar of the user sending the message. Defaults to user_avatar. |
| 4788 | * @returns {Promise<void>} A promise that resolves when the message is inserted. | 4788 | * @returns {Promise<any>} A promise that resolves to the message when it is inserted. |
| 4789 | */ | 4789 | */ |
| 4790 | export async function sendMessageAsUser(messageText, messageBias, insertAt = null, compact = false, name = name1, avatar = user_avatar) { | 4790 | export async function sendMessageAsUser(messageText, messageBias, insertAt = null, compact = false, name = name1, avatar = user_avatar) { |
| 4791 | messageText = getRegexedString(messageText, regex_placement.USER_INPUT); | 4791 | messageText = getRegexedString(messageText, regex_placement.USER_INPUT); |
| @@ -4832,6 +4832,8 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul | |||
| 4832 | await eventSource.emit(event_types.USER_MESSAGE_RENDERED, chat_id); | 4832 | await eventSource.emit(event_types.USER_MESSAGE_RENDERED, chat_id); |
| 4833 | await saveChatConditional(); | 4833 | await saveChatConditional(); |
| 4834 | } | 4834 | } |
| 4835 | |||
| 4836 | return message; | ||
| 4835 | } | 4837 | } |
| 4836 | 4838 | ||
| 4837 | /** | 4839 | /** |
| @@ -12,6 +12,8 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from ' | |||
| 12 | import { isFunctionCallingSupported } from '../../openai.js'; | 12 | import { isFunctionCallingSupported } from '../../openai.js'; |
| 13 | import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js'; | 13 | import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js'; |
| 14 | import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; | 14 | import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 15 | import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js'; | ||
| 16 | import { SlashCommandClosure } from '../../slash-commands/SlashCommandClosure.js'; | ||
| 15 | export { MODULE_NAME }; | 17 | export { MODULE_NAME }; |
| 16 | 18 | ||
| 17 | const MODULE_NAME = 'expressions'; | 19 | const MODULE_NAME = 'expressions'; |
| @@ -2134,18 +2136,42 @@ function migrateSettings() { | |||
| 2134 | name: 'classify-expressions', | 2136 | name: 'classify-expressions', |
| 2135 | aliases: ['expressions'], | 2137 | aliases: ['expressions'], |
| 2136 | callback: async (args) => { | 2138 | callback: async (args) => { |
| 2137 | const list = await getExpressionsList(); | 2139 | /** @type {import('../../slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */ |
| 2138 | switch (String(args.format).toLowerCase()) { | 2140 | // @ts-ignore |
| 2141 | let returnType = args.return; | ||
| 2142 | |||
| 2143 | // Old legacy return type handling | ||
| 2144 | if (args.format) { | ||
| 2145 | toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning'); | ||
| 2146 | const type = String(args?.format).toLowerCase().trim(); | ||
| 2147 | switch (type) { | ||
| 2139 | case 'json': | 2148 | case 'json': |
| 2140 | return JSON.stringify(list); | 2149 | returnType = 'object'; |
| 2150 | break; | ||
| 2141 | default: | 2151 | default: |
| 2142 | return list.join(', '); | 2152 | returnType = 'pipe'; |
| 2153 | break; | ||
| 2154 | } | ||
| 2143 | } | 2155 | } |
| 2156 | |||
| 2157 | // Now the actual new return type handling | ||
| 2158 | const list = await getExpressionsList(); | ||
| 2159 | |||
| 2160 | return await slashCommandReturnHelper.doReturn(returnType ?? 'pipe', list, { objectToStringFunc: list => list.join(', ') }); | ||
| 2144 | }, | 2161 | }, |
| 2145 | namedArgumentList: [ | 2162 | namedArgumentList: [ |
| 2146 | SlashCommandNamedArgument.fromProps({ | 2163 | SlashCommandNamedArgument.fromProps({ |
| 2164 | name: 'return', | ||
| 2165 | description: 'The way how you want the return value to be provided', | ||
| 2166 | typeList: [ARGUMENT_TYPE.STRING], | ||
| 2167 | defaultValue: 'pipe', | ||
| 2168 | enumList: slashCommandReturnHelper.enumList({ allowObject: true }), | ||
| 2169 | forceEnum: true, | ||
| 2170 | }), | ||
| 2171 | // TODO remove some day | ||
| 2172 | SlashCommandNamedArgument.fromProps({ | ||
| 2147 | name: 'format', | 2173 | name: 'format', |
| 2148 | description: 'The format to return the list in: comma-separated plain text or JSON array. Default is plain text.', | 2174 | description: '!!! DEPRECATED - use "return" instead !!! The format to return the list in: comma-separated plain text or JSON array. Default is plain text.', |
| 2149 | typeList: [ARGUMENT_TYPE.STRING], | 2175 | typeList: [ARGUMENT_TYPE.STRING], |
| 2150 | enumList: [ | 2176 | enumList: [ |
| 2151 | new SlashCommandEnumValue('plain', null, enumTypes.enum, ', '), | 2177 | new SlashCommandEnumValue('plain', null, enumTypes.enum, ', '), |
| @@ -70,6 +70,7 @@ import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; | |||
| 70 | import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; | 70 | import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 71 | import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js'; | 71 | import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js'; |
| 72 | import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js'; | 72 | import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js'; |
| 73 | import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js'; | ||
| 73 | export { | 74 | export { |
| 74 | executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand, | 75 | executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand, |
| 75 | }; | 76 | }; |
| @@ -242,6 +243,7 @@ export function initDefaultSlashCommands() { | |||
| 242 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 243 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 243 | name: 'sendas', | 244 | name: 'sendas', |
| 244 | callback: sendMessageAs, | 245 | callback: sendMessageAs, |
| 246 | returns: 'Optionally the text of the sent message, if specified in the "return" argument', | ||
| 245 | namedArgumentList: [ | 247 | namedArgumentList: [ |
| 246 | SlashCommandNamedArgument.fromProps({ | 248 | SlashCommandNamedArgument.fromProps({ |
| 247 | name: 'name', | 249 | name: 'name', |
| @@ -269,6 +271,14 @@ export function initDefaultSlashCommands() { | |||
| 269 | typeList: [ARGUMENT_TYPE.NUMBER], | 271 | typeList: [ARGUMENT_TYPE.NUMBER], |
| 270 | enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), | 272 | enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), |
| 271 | }), | 273 | }), |
| 274 | SlashCommandNamedArgument.fromProps({ | ||
| 275 | name: 'return', | ||
| 276 | description: 'The way how you want the return value to be provided', | ||
| 277 | typeList: [ARGUMENT_TYPE.STRING], | ||
| 278 | defaultValue: 'none', | ||
| 279 | enumList: slashCommandReturnHelper.enumList({ allowObject: true }), | ||
| 280 | forceEnum: true, | ||
| 281 | }), | ||
| 272 | ], | 282 | ], |
| 273 | unnamedArgumentList: [ | 283 | unnamedArgumentList: [ |
| 274 | new SlashCommandArgument( | 284 | new SlashCommandArgument( |
| @@ -301,6 +311,7 @@ export function initDefaultSlashCommands() { | |||
| 301 | name: 'sys', | 311 | name: 'sys', |
| 302 | callback: sendNarratorMessage, | 312 | callback: sendNarratorMessage, |
| 303 | aliases: ['nar'], | 313 | aliases: ['nar'], |
| 314 | returns: 'Optionally the text of the sent message, if specified in the "return" argument', | ||
| 304 | namedArgumentList: [ | 315 | namedArgumentList: [ |
| 305 | new SlashCommandNamedArgument( | 316 | new SlashCommandNamedArgument( |
| 306 | 'compact', | 317 | 'compact', |
| @@ -316,6 +327,14 @@ export function initDefaultSlashCommands() { | |||
| 316 | typeList: [ARGUMENT_TYPE.NUMBER], | 327 | typeList: [ARGUMENT_TYPE.NUMBER], |
| 317 | enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), | 328 | enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), |
| 318 | }), | 329 | }), |
| 330 | SlashCommandNamedArgument.fromProps({ | ||
| 331 | name: 'return', | ||
| 332 | description: 'The way how you want the return value to be provided', | ||
| 333 | typeList: [ARGUMENT_TYPE.STRING], | ||
| 334 | defaultValue: 'none', | ||
| 335 | enumList: slashCommandReturnHelper.enumList({ allowObject: true }), | ||
| 336 | forceEnum: true, | ||
| 337 | }), | ||
| 319 | ], | 338 | ], |
| 320 | unnamedArgumentList: [ | 339 | unnamedArgumentList: [ |
| 321 | new SlashCommandArgument( | 340 | new SlashCommandArgument( |
| @@ -355,6 +374,7 @@ export function initDefaultSlashCommands() { | |||
| 355 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 374 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 356 | name: 'comment', | 375 | name: 'comment', |
| 357 | callback: sendCommentMessage, | 376 | callback: sendCommentMessage, |
| 377 | returns: 'Optionally the text of the sent message, if specified in the "return" argument', | ||
| 358 | namedArgumentList: [ | 378 | namedArgumentList: [ |
| 359 | new SlashCommandNamedArgument( | 379 | new SlashCommandNamedArgument( |
| 360 | 'compact', | 380 | 'compact', |
| @@ -370,6 +390,14 @@ export function initDefaultSlashCommands() { | |||
| 370 | typeList: [ARGUMENT_TYPE.NUMBER], | 390 | typeList: [ARGUMENT_TYPE.NUMBER], |
| 371 | enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), | 391 | enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), |
| 372 | }), | 392 | }), |
| 393 | SlashCommandNamedArgument.fromProps({ | ||
| 394 | name: 'return', | ||
| 395 | description: 'The way how you want the return value to be provided', | ||
| 396 | typeList: [ARGUMENT_TYPE.STRING], | ||
| 397 | defaultValue: 'none', | ||
| 398 | enumList: slashCommandReturnHelper.enumList({ allowObject: true }), | ||
| 399 | forceEnum: true, | ||
| 400 | }), | ||
| 373 | ], | 401 | ], |
| 374 | unnamedArgumentList: [ | 402 | unnamedArgumentList: [ |
| 375 | new SlashCommandArgument( | 403 | new SlashCommandArgument( |
| @@ -509,7 +537,7 @@ export function initDefaultSlashCommands() { | |||
| 509 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 537 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 510 | name: 'ask', | 538 | name: 'ask', |
| 511 | callback: askCharacter, | 539 | callback: askCharacter, |
| 512 | returns: 'the generated text', | 540 | returns: 'Optionally the text of the sent message, if specified in the "return" argument', |
| 513 | namedArgumentList: [ | 541 | namedArgumentList: [ |
| 514 | SlashCommandNamedArgument.fromProps({ | 542 | SlashCommandNamedArgument.fromProps({ |
| 515 | name: 'name', | 543 | name: 'name', |
| @@ -518,6 +546,14 @@ export function initDefaultSlashCommands() { | |||
| 518 | isRequired: true, | 546 | isRequired: true, |
| 519 | enumProvider: commonEnumProviders.characters('character'), | 547 | enumProvider: commonEnumProviders.characters('character'), |
| 520 | }), | 548 | }), |
| 549 | SlashCommandNamedArgument.fromProps({ | ||
| 550 | name: 'return', | ||
| 551 | description: 'The way how you want the return value to be provided', | ||
| 552 | typeList: [ARGUMENT_TYPE.STRING], | ||
| 553 | defaultValue: 'pipe', | ||
| 554 | enumList: slashCommandReturnHelper.enumList({ allowObject: true }), | ||
| 555 | forceEnum: true, | ||
| 556 | }), | ||
| 521 | ], | 557 | ], |
| 522 | unnamedArgumentList: [ | 558 | unnamedArgumentList: [ |
| 523 | new SlashCommandArgument( | 559 | new SlashCommandArgument( |
| @@ -556,6 +592,7 @@ export function initDefaultSlashCommands() { | |||
| 556 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 592 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 557 | name: 'send', | 593 | name: 'send', |
| 558 | callback: sendUserMessageCallback, | 594 | callback: sendUserMessageCallback, |
| 595 | returns: 'Optionally the text of the sent message, if specified in the "return" argument', | ||
| 559 | namedArgumentList: [ | 596 | namedArgumentList: [ |
| 560 | new SlashCommandNamedArgument( | 597 | new SlashCommandNamedArgument( |
| 561 | 'compact', | 598 | 'compact', |
| @@ -578,6 +615,14 @@ export function initDefaultSlashCommands() { | |||
| 578 | defaultValue: '{{user}}', | 615 | defaultValue: '{{user}}', |
| 579 | enumProvider: commonEnumProviders.personas, | 616 | enumProvider: commonEnumProviders.personas, |
| 580 | }), | 617 | }), |
| 618 | SlashCommandNamedArgument.fromProps({ | ||
| 619 | name: 'return', | ||
| 620 | description: 'The way how you want the return value to be provided', | ||
| 621 | typeList: [ARGUMENT_TYPE.STRING], | ||
| 622 | defaultValue: 'none', | ||
| 623 | enumList: slashCommandReturnHelper.enumList({ allowObject: true }), | ||
| 624 | forceEnum: true, | ||
| 625 | }), | ||
| 581 | ], | 626 | ], |
| 582 | unnamedArgumentList: [ | 627 | unnamedArgumentList: [ |
| 583 | new SlashCommandArgument( | 628 | new SlashCommandArgument( |
| @@ -1568,12 +1613,21 @@ export function initDefaultSlashCommands() { | |||
| 1568 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 1613 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 1569 | name: 'listinjects', | 1614 | name: 'listinjects', |
| 1570 | callback: listInjectsCallback, | 1615 | callback: listInjectsCallback, |
| 1571 | helpString: 'Lists all script injections for the current chat. Displays injects in a popup by default. Use the <code>format</code> argument to change the output format.', | 1616 | helpString: 'Lists all script injections for the current chat. Displays injects in a popup by default. Use the <code>return</code> argument to change the return type.', |
| 1572 | returns: 'JSON object of script injections', | 1617 | returns: 'Optionalls the JSON object of script injections', |
| 1573 | namedArgumentList: [ | 1618 | namedArgumentList: [ |
| 1574 | SlashCommandNamedArgument.fromProps({ | 1619 | SlashCommandNamedArgument.fromProps({ |
| 1620 | name: 'return', | ||
| 1621 | description: 'The way how you want the return value to be provided', | ||
| 1622 | typeList: [ARGUMENT_TYPE.STRING], | ||
| 1623 | defaultValue: 'popup-html', | ||
| 1624 | enumList: slashCommandReturnHelper.enumList({ allowPipe: false, allowObject: true, allowChat: true, allowPopup: true, allowTextVersion: false }), | ||
| 1625 | forceEnum: true, | ||
| 1626 | }), | ||
| 1627 | // TODO remove some day | ||
| 1628 | SlashCommandNamedArgument.fromProps({ | ||
| 1575 | name: 'format', | 1629 | name: 'format', |
| 1576 | description: 'output format', | 1630 | description: '!!! DEPRECATED - use "return" instead !!! output format', |
| 1577 | typeList: [ARGUMENT_TYPE.STRING], | 1631 | typeList: [ARGUMENT_TYPE.STRING], |
| 1578 | isRequired: true, | 1632 | isRequired: true, |
| 1579 | forceEnum: true, | 1633 | forceEnum: true, |
| @@ -1842,37 +1896,43 @@ function injectCallback(args, value) { | |||
| 1842 | } | 1896 | } |
| 1843 | 1897 | ||
| 1844 | async function listInjectsCallback(args) { | 1898 | async function listInjectsCallback(args) { |
| 1899 | /** @type {import('./slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */ | ||
| 1900 | let returnType = args.return; | ||
| 1901 | |||
| 1902 | // Old legacy return type handling | ||
| 1903 | if (args.format) { | ||
| 1904 | toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning'); | ||
| 1845 | const type = String(args?.format).toLowerCase().trim(); | 1905 | const type = String(args?.format).toLowerCase().trim(); |
| 1846 | if (!chat_metadata.script_injects || !Object.keys(chat_metadata.script_injects).length) { | 1906 | if (!chat_metadata.script_injects || !Object.keys(chat_metadata.script_injects).length) { |
| 1847 | type !== 'none' && toastr.info('No script injections for the current chat'); | 1907 | type !== 'none' && toastr.info('No script injections for the current chat'); |
| 1848 | return JSON.stringify({}); | ||
| 1849 | } | 1908 | } |
| 1850 | |||
| 1851 | const injects = Object.entries(chat_metadata.script_injects) | ||
| 1852 | .map(([id, inject]) => { | ||
| 1853 | const position = Object.entries(extension_prompt_types); | ||
| 1854 | const positionName = position.find(([_, value]) => value === inject.position)?.[0] ?? 'unknown'; | ||
| 1855 | return `* **${id}**: <code>${inject.value}</code> (${positionName}, depth: ${inject.depth}, scan: ${inject.scan ?? false}, role: ${inject.role ?? extension_prompt_roles.SYSTEM})`; | ||
| 1856 | }) | ||
| 1857 | .join('\n'); | ||
| 1858 | |||
| 1859 | const converter = new showdown.Converter(); | ||
| 1860 | const messageText = `### Script injections:\n${injects}`; | ||
| 1861 | const htmlMessage = DOMPurify.sanitize(converter.makeHtml(messageText)); | ||
| 1862 | |||
| 1863 | switch (type) { | 1909 | switch (type) { |
| 1864 | case 'none': | 1910 | case 'none': |
| 1911 | returnType = 'none'; | ||
| 1865 | break; | 1912 | break; |
| 1866 | case 'chat': | 1913 | case 'chat': |
| 1867 | sendSystemMessage(system_message_types.GENERIC, htmlMessage); | 1914 | returnType = 'chat-html'; |
| 1868 | break; | 1915 | break; |
| 1869 | case 'popup': | 1916 | case 'popup': |
| 1870 | default: | 1917 | default: |
| 1871 | await callGenericPopup(htmlMessage, POPUP_TYPE.TEXT); | 1918 | returnType = 'popup-html'; |
| 1872 | break; | 1919 | break; |
| 1873 | } | 1920 | } |
| 1921 | } | ||
| 1874 | 1922 | ||
| 1875 | return JSON.stringify(chat_metadata.script_injects); | 1923 | // Now the actual new return type handling |
| 1924 | const buildTextValue = (injects) => { | ||
| 1925 | const injectsStr = Object.entries(injects) | ||
| 1926 | .map(([id, inject]) => { | ||
| 1927 | const position = Object.entries(extension_prompt_types); | ||
| 1928 | const positionName = position.find(([_, value]) => value === inject.position)?.[0] ?? 'unknown'; | ||
| 1929 | return `* **${id}**: <code>${inject.value}</code> (${positionName}, depth: ${inject.depth}, scan: ${inject.scan ?? false}, role: ${inject.role ?? extension_prompt_roles.SYSTEM})`; | ||
| 1930 | }) | ||
| 1931 | .join('\n'); | ||
| 1932 | return `### Script injections:\n${injectsStr || 'No script injections for the current chat'}`; | ||
| 1933 | }; | ||
| 1934 | |||
| 1935 | return await slashCommandReturnHelper.doReturn(returnType ?? 'popup-html', chat_metadata.script_injects ?? {}, { objectToStringFunc: buildTextValue }); | ||
| 1876 | } | 1936 | } |
| 1877 | 1937 | ||
| 1878 | /** | 1938 | /** |
| @@ -2559,7 +2619,7 @@ async function askCharacter(args, text) { | |||
| 2559 | // Not supported in group chats | 2619 | // Not supported in group chats |
| 2560 | // TODO: Maybe support group chats? | 2620 | // TODO: Maybe support group chats? |
| 2561 | if (selected_group) { | 2621 | if (selected_group) { |
| 2562 | toastr.error('Cannot run /ask command in a group chat!'); | 2622 | toastr.warning('Cannot run /ask command in a group chat!'); |
| 2563 | return ''; | 2623 | return ''; |
| 2564 | } | 2624 | } |
| 2565 | 2625 | ||
| @@ -2633,7 +2693,9 @@ async function askCharacter(args, text) { | |||
| 2633 | } | 2693 | } |
| 2634 | } | 2694 | } |
| 2635 | 2695 | ||
| 2636 | return askResult; | 2696 | const message = askResult ? chat[chat.length - 1] : null; |
| 2697 | |||
| 2698 | return await slashCommandReturnHelper.doReturn(args.return ?? 'pipe', message, { objectToStringFunc: x => x.mes }); | ||
| 2637 | } | 2699 | } |
| 2638 | 2700 | ||
| 2639 | async function hideMessageCallback(_, arg) { | 2701 | async function hideMessageCallback(_, arg) { |
| @@ -2908,7 +2970,7 @@ function findPersonaByName(name) { | |||
| 2908 | 2970 | ||
| 2909 | async function sendUserMessageCallback(args, text) { | 2971 | async function sendUserMessageCallback(args, text) { |
| 2910 | if (!text) { | 2972 | if (!text) { |
| 2911 | console.warn('WARN: No text provided for /send command'); | 2973 | toastr.warning('You must specify text to send'); |
| 2912 | return; | 2974 | return; |
| 2913 | } | 2975 | } |
| 2914 | 2976 | ||
| @@ -2924,16 +2986,17 @@ async function sendUserMessageCallback(args, text) { | |||
| 2924 | insertAt = chat.length + insertAt; | 2986 | insertAt = chat.length + insertAt; |
| 2925 | } | 2987 | } |
| 2926 | 2988 | ||
| 2989 | let message; | ||
| 2927 | if ('name' in args) { | 2990 | if ('name' in args) { |
| 2928 | const name = args.name || ''; | 2991 | const name = args.name || ''; |
| 2929 | const avatar = findPersonaByName(name) || user_avatar; | 2992 | const avatar = findPersonaByName(name) || user_avatar; |
| 2930 | await sendMessageAsUser(text, bias, insertAt, compact, name, avatar); | 2993 | message = await sendMessageAsUser(text, bias, insertAt, compact, name, avatar); |
| 2931 | } | 2994 | } |
| 2932 | else { | 2995 | else { |
| 2933 | await sendMessageAsUser(text, bias, insertAt, compact); | 2996 | message = await sendMessageAsUser(text, bias, insertAt, compact); |
| 2934 | } | 2997 | } |
| 2935 | 2998 | ||
| 2936 | return ''; | 2999 | return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes }); |
| 2937 | } | 3000 | } |
| 2938 | 3001 | ||
| 2939 | async function deleteMessagesByNameCallback(_, name) { | 3002 | async function deleteMessagesByNameCallback(_, name) { |
| @@ -3221,30 +3284,20 @@ export function getNameAndAvatarForMessage(character, name = null) { | |||
| 3221 | 3284 | ||
| 3222 | export async function sendMessageAs(args, text) { | 3285 | export async function sendMessageAs(args, text) { |
| 3223 | if (!text) { | 3286 | if (!text) { |
| 3287 | toastr.warning('You must specify text to send as'); | ||
| 3224 | return ''; | 3288 | return ''; |
| 3225 | } | 3289 | } |
| 3226 | 3290 | ||
| 3227 | let name; | 3291 | let name = args.name?.trim(); |
| 3228 | let mesText; | 3292 | let mesText; |
| 3229 | 3293 | ||
| 3230 | if (args.name) { | 3294 | if (!name) { |
| 3231 | name = args.name.trim(); | ||
| 3232 | |||
| 3233 | if (!name && !text) { | ||
| 3234 | toastr.warning('You must specify a name and text to send as'); | ||
| 3235 | return ''; | ||
| 3236 | } | ||
| 3237 | } else { | ||
| 3238 | const namelessWarningKey = 'sendAsNamelessWarningShown'; | 3295 | const namelessWarningKey = 'sendAsNamelessWarningShown'; |
| 3239 | if (localStorage.getItem(namelessWarningKey) !== 'true') { | 3296 | if (localStorage.getItem(namelessWarningKey) !== 'true') { |
| 3240 | toastr.warning('To avoid confusion, please use /sendas name="Character Name"', 'Name defaulted to {{char}}', { timeOut: 10000 }); | 3297 | toastr.warning('To avoid confusion, please use /sendas name="Character Name"', 'Name defaulted to {{char}}', { timeOut: 10000 }); |
| 3241 | localStorage.setItem(namelessWarningKey, 'true'); | 3298 | localStorage.setItem(namelessWarningKey, 'true'); |
| 3242 | } | 3299 | } |
| 3243 | name = name2; | 3300 | name = name2; |
| 3244 | if (!text) { | ||
| 3245 | toastr.warning('You must specify text to send as'); | ||
| 3246 | return ''; | ||
| 3247 | } | ||
| 3248 | } | 3301 | } |
| 3249 | 3302 | ||
| 3250 | mesText = text.trim(); | 3303 | mesText = text.trim(); |
| @@ -3321,11 +3374,12 @@ export async function sendMessageAs(args, text) { | |||
| 3321 | await saveChatConditional(); | 3374 | await saveChatConditional(); |
| 3322 | } | 3375 | } |
| 3323 | 3376 | ||
| 3324 | return ''; | 3377 | return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes }); |
| 3325 | } | 3378 | } |
| 3326 | 3379 | ||
| 3327 | export async function sendNarratorMessage(args, text) { | 3380 | export async function sendNarratorMessage(args, text) { |
| 3328 | if (!text) { | 3381 | if (!text) { |
| 3382 | toastr.warning('You must specify text to send'); | ||
| 3329 | return ''; | 3383 | return ''; |
| 3330 | } | 3384 | } |
| 3331 | 3385 | ||
| @@ -3374,7 +3428,7 @@ export async function sendNarratorMessage(args, text) { | |||
| 3374 | await saveChatConditional(); | 3428 | await saveChatConditional(); |
| 3375 | } | 3429 | } |
| 3376 | 3430 | ||
| 3377 | return ''; | 3431 | return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes }); |
| 3378 | } | 3432 | } |
| 3379 | 3433 | ||
| 3380 | export async function promptQuietForLoudResponse(who, text) { | 3434 | export async function promptQuietForLoudResponse(who, text) { |
| @@ -3420,6 +3474,7 @@ export async function promptQuietForLoudResponse(who, text) { | |||
| 3420 | 3474 | ||
| 3421 | async function sendCommentMessage(args, text) { | 3475 | async function sendCommentMessage(args, text) { |
| 3422 | if (!text) { | 3476 | if (!text) { |
| 3477 | toastr.warning('You must specify text to send'); | ||
| 3423 | return ''; | 3478 | return ''; |
| 3424 | } | 3479 | } |
| 3425 | 3480 | ||
| @@ -3462,7 +3517,7 @@ async function sendCommentMessage(args, text) { | |||
| 3462 | await saveChatConditional(); | 3517 | await saveChatConditional(); |
| 3463 | } | 3518 | } |
| 3464 | 3519 | ||
| 3465 | return ''; | 3520 | return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes }); |
| 3466 | } | 3521 | } |
| 3467 | 3522 | ||
| 3468 | /** | 3523 | /** |
| @@ -36,6 +36,7 @@ export const enumIcons = { | |||
| 36 | message: '💬', | 36 | message: '💬', |
| 37 | voice: '🎤', | 37 | voice: '🎤', |
| 38 | server: '🖥️', | 38 | server: '🖥️', |
| 39 | popup: '🗔', | ||
| 39 | 40 | ||
| 40 | true: '✔️', | 41 | true: '✔️', |
| 41 | false: '❌', | 42 | false: '❌', |
| @@ -0,0 +1,80 @@ | |||
| 1 | import { sendSystemMessage, system_message_types } from '../../script.js'; | ||
| 2 | import { callGenericPopup, POPUP_TYPE } from '../popup.js'; | ||
| 3 | import { escapeHtml } from '../utils.js'; | ||
| 4 | import { enumIcons } from './SlashCommandCommonEnumsProvider.js'; | ||
| 5 | import { enumTypes, SlashCommandEnumValue } from './SlashCommandEnumValue.js'; | ||
| 6 | |||
| 7 | /** @typedef {'pipe'|'object'|'chat-html'|'chat-text'|'popup-html'|'popup-text'|'toast-html'|'toast-text'|'console'|'none'} SlashCommandReturnType */ | ||
| 8 | |||
| 9 | export const slashCommandReturnHelper = { | ||
| 10 | // Without this, VSCode formatter fucks up JS docs. Don't ask me why. | ||
| 11 | _: false, | ||
| 12 | |||
| 13 | /** | ||
| 14 | * Gets/creates the enum list of types of return relevant for a slash command | ||
| 15 | * | ||
| 16 | * @param {object} [options={}] Options | ||
| 17 | * @param {boolean} [options.allowPipe=true] Allow option to pipe the return value | ||
| 18 | * @param {boolean} [options.allowObject=false] Allow option to return the value as an object | ||
| 19 | * @param {boolean} [options.allowChat=false] Allow option to return the value as a chat message | ||
| 20 | * @param {boolean} [options.allowPopup=false] Allow option to return the value as a popup | ||
| 21 | * @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 | ||
| 22 | * @returns {SlashCommandEnumValue[]} The enum list | ||
| 23 | */ | ||
| 24 | enumList: ({ allowPipe = true, allowObject = false, allowChat = false, allowPopup = false, allowTextVersion = true } = {}) => [ | ||
| 25 | allowPipe && new SlashCommandEnumValue('pipe', 'Return to the pipe for the next command', enumTypes.name, '|'), | ||
| 26 | allowObject && new SlashCommandEnumValue('object', 'Return as an object (or array) to the pipe for the next command', enumTypes.variable, enumIcons.dictionary), | ||
| 27 | allowChat && new SlashCommandEnumValue('chat-html', 'Sending a chat message with the return value - Can display HTML', enumTypes.command, enumIcons.message), | ||
| 28 | allowChat && allowTextVersion && new SlashCommandEnumValue('chat-text', 'Sending a chat message with the return value - Will only display as text', enumTypes.qr, enumIcons.message), | ||
| 29 | allowPopup && new SlashCommandEnumValue('popup-html', 'Showing as a popup with the return value - Can display HTML', enumTypes.command, enumIcons.popup), | ||
| 30 | allowPopup && allowTextVersion && new SlashCommandEnumValue('popup-text', 'Showing as a popup with the return value - Will only display as text', enumTypes.qr, enumIcons.popup), | ||
| 31 | new SlashCommandEnumValue('toast-html', 'Show the return value as a toast notification - Can display HTML', enumTypes.command, 'ℹ️'), | ||
| 32 | allowTextVersion && new SlashCommandEnumValue('toast-text', 'Show the return value as a toast notification - Will only display as text', enumTypes.qr, 'ℹ️'), | ||
| 33 | new SlashCommandEnumValue('console', 'Log the return value (object, if it can be one) to the console', enumTypes.enum, '>'), | ||
| 34 | new SlashCommandEnumValue('none', 'No return value'), | ||
| 35 | ].filter(x => !!x), | ||
| 36 | |||
| 37 | /** | ||
| 38 | * Handles the return value based on the specified type | ||
| 39 | * | ||
| 40 | * @param {SlashCommandReturnType} type The type of return | ||
| 41 | * @param {object|number|string} value The value to return | ||
| 42 | * @param {object} [options={}] Options | ||
| 43 | * @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 | ||
| 44 | * @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 | ||
| 45 | * @returns {Promise<*>} The processed return value | ||
| 46 | */ | ||
| 47 | async doReturn(type, value, { objectToStringFunc = o => o?.toString(), objectToHtmlFunc = null } = {}) { | ||
| 48 | const shouldHtml = type.endsWith('html'); | ||
| 49 | const actualConverterFunc = shouldHtml && objectToHtmlFunc ? objectToHtmlFunc : objectToStringFunc; | ||
| 50 | const stringValue = typeof value !== 'string' ? actualConverterFunc(value) : value; | ||
| 51 | |||
| 52 | switch (type) { | ||
| 53 | case 'popup-html': | ||
| 54 | case 'popup-text': | ||
| 55 | case 'chat-text': | ||
| 56 | case 'chat-html': | ||
| 57 | case 'toast-text': | ||
| 58 | case 'toast-html': { | ||
| 59 | const htmlOrNotHtml = shouldHtml ? DOMPurify.sanitize((new showdown.Converter()).makeHtml(stringValue)) : escapeHtml(stringValue); | ||
| 60 | |||
| 61 | if (type.startsWith('popup')) await callGenericPopup(htmlOrNotHtml, POPUP_TYPE.TEXT); | ||
| 62 | if (type.startsWith('chat')) sendSystemMessage(system_message_types.GENERIC, htmlOrNotHtml); | ||
| 63 | if (type.startsWith('toast')) toastr.info(htmlOrNotHtml, null, { escapeHtml: !shouldHtml }); | ||
| 64 | |||
| 65 | return ''; | ||
| 66 | } | ||
| 67 | case 'pipe': | ||
| 68 | return stringValue ?? ''; | ||
| 69 | case 'object': | ||
| 70 | return JSON.stringify(value); | ||
| 71 | case 'console': | ||
| 72 | console.info(value); | ||
| 73 | return ''; | ||
| 74 | case 'none': | ||
| 75 | return ''; | ||
| 76 | default: | ||
| 77 | throw new Error(`Unknown return type: ${type}`); | ||
| 78 | } | ||
| 79 | }, | ||
| 80 | }; | ||
| @@ -11,6 +11,7 @@ import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureR | |||
| 11 | import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; | 11 | import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 12 | import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js'; | 12 | import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js'; |
| 13 | import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js'; | 13 | import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 14 | import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js'; | ||
| 14 | import { SlashCommandScope } from './slash-commands/SlashCommandScope.js'; | 15 | import { SlashCommandScope } from './slash-commands/SlashCommandScope.js'; |
| 15 | import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js'; | 16 | import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js'; |
| 16 | 17 | ||
| @@ -305,7 +306,28 @@ export function replaceVariableMacros(input) { | |||
| 305 | } | 306 | } |
| 306 | 307 | ||
| 307 | async function listVariablesCallback(args) { | 308 | async function listVariablesCallback(args) { |
| 308 | const type = String(args?.format || '').toLowerCase().trim() || 'popup'; | 309 | /** @type {import('./slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */ |
| 310 | let returnType = args.return; | ||
| 311 | |||
| 312 | // Old legacy return type handling | ||
| 313 | if (args.format) { | ||
| 314 | toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning'); | ||
| 315 | const type = String(args?.format).toLowerCase().trim(); | ||
| 316 | switch (type) { | ||
| 317 | case 'none': | ||
| 318 | returnType = 'none'; | ||
| 319 | break; | ||
| 320 | case 'chat': | ||
| 321 | returnType = 'chat-html'; | ||
| 322 | break; | ||
| 323 | case 'popup': | ||
| 324 | default: | ||
| 325 | returnType = 'popup-html'; | ||
| 326 | break; | ||
| 327 | } | ||
| 328 | } | ||
| 329 | |||
| 330 | // Now the actual new return type handling | ||
| 309 | const scope = String(args?.scope || '').toLowerCase().trim() || 'all'; | 331 | const scope = String(args?.scope || '').toLowerCase().trim() || 'all'; |
| 310 | if (!chat_metadata.variables) { | 332 | if (!chat_metadata.variables) { |
| 311 | chat_metadata.variables = {}; | 333 | chat_metadata.variables = {}; |
| @@ -317,35 +339,24 @@ async function listVariablesCallback(args) { | |||
| 317 | const localVariables = includeLocalVariables ? Object.entries(chat_metadata.variables).map(([name, value]) => `${name}: ${value}`) : []; | 339 | const localVariables = includeLocalVariables ? Object.entries(chat_metadata.variables).map(([name, value]) => `${name}: ${value}`) : []; |
| 318 | const globalVariables = includeGlobalVariables ? Object.entries(extension_settings.variables.global).map(([name, value]) => `${name}: ${value}`) : []; | 340 | const globalVariables = includeGlobalVariables ? Object.entries(extension_settings.variables.global).map(([name, value]) => `${name}: ${value}`) : []; |
| 319 | 341 | ||
| 320 | const jsonVariables = [ | 342 | const buildTextValue = (_) => { |
| 321 | ...Object.entries(chat_metadata.variables).map(x => ({ key: x[0], value: x[1], scope: 'local' })), | ||
| 322 | ...Object.entries(extension_settings.variables.global).map(x => ({ key: x[0], value: x[1], scope: 'global' })), | ||
| 323 | ]; | ||
| 324 | |||
| 325 | const localVariablesString = localVariables.length > 0 ? localVariables.join('\n\n') : 'No local variables'; | 343 | const localVariablesString = localVariables.length > 0 ? localVariables.join('\n\n') : 'No local variables'; |
| 326 | const globalVariablesString = globalVariables.length > 0 ? globalVariables.join('\n\n') : 'No global variables'; | 344 | const globalVariablesString = globalVariables.length > 0 ? globalVariables.join('\n\n') : 'No global variables'; |
| 327 | const chatName = getCurrentChatId(); | 345 | const chatName = getCurrentChatId(); |
| 328 | 346 | ||
| 329 | const converter = new showdown.Converter(); | ||
| 330 | const message = [ | 347 | const message = [ |
| 331 | includeLocalVariables ? `### Local variables (${chatName}):\n${localVariablesString}` : '', | 348 | includeLocalVariables ? `### Local variables (${chatName}):\n${localVariablesString}` : '', |
| 332 | includeGlobalVariables ? `### Global variables:\n${globalVariablesString}` : '', | 349 | includeGlobalVariables ? `### Global variables:\n${globalVariablesString}` : '', |
| 333 | ].filter(x => x).join('\n\n'); | 350 | ].filter(x => x).join('\n\n'); |
| 334 | const htmlMessage = DOMPurify.sanitize(converter.makeHtml(message)); | 351 | return message; |
| 352 | }; | ||
| 335 | 353 | ||
| 336 | switch (type) { | 354 | const jsonVariables = [ |
| 337 | case 'none': | 355 | ...Object.entries(chat_metadata.variables).map(x => ({ key: x[0], value: x[1], scope: 'local' })), |
| 338 | break; | 356 | ...Object.entries(extension_settings.variables.global).map(x => ({ key: x[0], value: x[1], scope: 'global' })), |
| 339 | case 'chat': | 357 | ]; |
| 340 | sendSystemMessage(system_message_types.GENERIC, htmlMessage); | ||
| 341 | break; | ||
| 342 | case 'popup': | ||
| 343 | default: | ||
| 344 | await callGenericPopup(htmlMessage, POPUP_TYPE.TEXT); | ||
| 345 | break; | ||
| 346 | } | ||
| 347 | 358 | ||
| 348 | return JSON.stringify(jsonVariables); | 359 | return await slashCommandReturnHelper.doReturn(returnType ?? 'popup-html', jsonVariables, { objectToStringFunc: buildTextValue }); |
| 349 | } | 360 | } |
| 350 | 361 | ||
| 351 | /** | 362 | /** |
| @@ -916,7 +927,7 @@ export function registerVariableCommands() { | |||
| 916 | name: 'listvar', | 927 | name: 'listvar', |
| 917 | callback: listVariablesCallback, | 928 | callback: listVariablesCallback, |
| 918 | aliases: ['listchatvar'], | 929 | aliases: ['listchatvar'], |
| 919 | helpString: 'List registered chat variables. Displays variables in a popup by default. Use the <code>format</code> argument to change the output format.', | 930 | helpString: 'List registered chat variables. Displays variables in a popup by default. Use the <code>return</code> argument to change the return type.', |
| 920 | returns: 'JSON list of local variables', | 931 | returns: 'JSON list of local variables', |
| 921 | namedArgumentList: [ | 932 | namedArgumentList: [ |
| 922 | SlashCommandNamedArgument.fromProps({ | 933 | SlashCommandNamedArgument.fromProps({ |
| @@ -933,8 +944,17 @@ export function registerVariableCommands() { | |||
| 933 | ], | 944 | ], |
| 934 | }), | 945 | }), |
| 935 | SlashCommandNamedArgument.fromProps({ | 946 | SlashCommandNamedArgument.fromProps({ |
| 947 | name: 'return', | ||
| 948 | description: 'The way how you want the return value to be provided', | ||
| 949 | typeList: [ARGUMENT_TYPE.STRING], | ||
| 950 | defaultValue: 'popup-html', | ||
| 951 | enumList: slashCommandReturnHelper.enumList({ allowPipe: false, allowObject: true, allowChat: true, allowPopup: true, allowTextVersion: false }), | ||
| 952 | forceEnum: true, | ||
| 953 | }), | ||
| 954 | // TODO remove some day | ||
| 955 | SlashCommandNamedArgument.fromProps({ | ||
| 936 | name: 'format', | 956 | name: 'format', |
| 937 | description: 'output format', | 957 | description: '!!! DEPRECATED - use "return" instead !!! output format', |
| 938 | typeList: [ARGUMENT_TYPE.STRING], | 958 | typeList: [ARGUMENT_TYPE.STRING], |
| 939 | isRequired: true, | 959 | isRequired: true, |
| 940 | forceEnum: true, | 960 | forceEnum: true, |