diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 891585bb9..a751c7d79 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -37,7 +37,7 @@ import { system_message_types, this_chid, } from '../script.js'; -import { SlashCommandParser as NewSlashCommandParser } from './slash-commands/SlashCommandParser.js'; +import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { SlashCommandParserError } from './slash-commands/SlashCommandParserError.js'; import { SlashCommandExecutor } from './slash-commands/SlashCommandExecutor.js'; import { getMessageTimeStamp } from './RossAscends-mods.js'; @@ -62,179 +62,7 @@ export { executeSlashCommands, getSlashCommandsHelp, registerSlashCommand, }; -class SlashCommandParser { - static COMMENT_KEYWORDS = ['#', '/']; - static RESERVED_KEYWORDS = [ - ...this.COMMENT_KEYWORDS, - ]; - - constructor() { - /** - * @type {Record} - Slash commands registered in the parser - */ - this.commands = {}; - /** - * @type {Record} - Help strings for each command - */ - this.helpStrings = {}; - } - - /** - * Adds a slash command to the parser. - * @param {string} command - The command name - * @param {function} callback - The callback function to execute - * @param {string[]} aliases - The command aliases - * @param {string} helpString - The help string for the command - * @param {boolean} [interruptsGeneration] - Whether the command interrupts message generation - * @param {boolean} [purgeFromMessage] - Whether the command should be purged from the message - * @returns {void} - */ - addCommand(command, callback, aliases, helpString = '', interruptsGeneration = false, purgeFromMessage = true) { - const fnObj = { callback, helpString, interruptsGeneration, purgeFromMessage }; - - if ([command, ...aliases].some(x => SlashCommandParser.RESERVED_KEYWORDS.includes(x))) { - console.error('ERROR: Reserved slash command keyword used!'); - return; - } - - if ([command, ...aliases].some(x => Object.hasOwn(this.commands, x))) { - console.trace('WARN: Duplicate slash command registered!'); - } - - this.commands[command] = fnObj; - - if (Array.isArray(aliases)) { - aliases.forEach((alias) => { - this.commands[alias] = fnObj; - }); - } - - let stringBuilder = `/${command} ${helpString} `; - if (Array.isArray(aliases) && aliases.length) { - let aliasesString = `(alias: ${aliases.map(x => `/${x}`).join(', ')})`; - stringBuilder += aliasesString; - } - this.helpStrings[command] = stringBuilder; - } - - /** - * Parses a slash command to extract the command name, the (named) arguments and the remaining text - * @param {string} text - Slash command text - * @returns {{command: SlashCommand, args: object, value: string, commandName: string}} - The parsed command, its arguments and the remaining text - */ - parse(text) { - // Parses a command even when spaces are present in arguments - // /buttons labels=["OK","I do not accept"] some text - // /fuzzy list=[ "red pink" , "yellow" ] threshold=" 0.6 " he yelled when the color was reddish and not pink | /echo - const excludedFromRegex = ['sendas']; - let command = ''; - const argObj = {}; - let unnamedArg = ''; - - // extract the command " /fuzzy " => "fuzzy" - text = text.trim(); - let remainingText = ''; - const commandArgPattern = /^\/([^\s]+)\s*(.*)$/s; - let match = commandArgPattern.exec(text); - if (match !== null && match[1].length > 0) { - command = match[1]; - remainingText = match[2]; - console.debug('command:' + command); - } - - if (SlashCommandParser.COMMENT_KEYWORDS.includes(command)) { - return { - commandName: command, - command: { - callback: () => {}, - helpString: '', - interruptsGeneration: false, - purgeFromMessage: true, - }, - args: {}, - value: '', - }; - } - - // parse the rest of the string to extract named arguments, the remainder is the "unnamedArg" which is usually text, like the prompt to send - while (remainingText.length > 0) { - // does the remaining text is like nameArg=[value] or nameArg=[value,value] or nameArg=[ value , value , value] - // where value can be a string like " this is some text " , note previously it was not possible to have have spaces - // where value can be a scalar like AScalar - // where value can be a number like +9 -1005.44 - // where value can be a macro like {{getvar::name}} - const namedArrayArgPattern = /^(\w+)=\[\s*(((?["'])[^"]*(\k)|{{[^}]*}}|[+-]?\d*\.?\d+|\w*)\s*,?\s*)+\]/s; - match = namedArrayArgPattern.exec(remainingText); - if (match !== null && match[0].length > 0) { - //console.log(`matching: ${match[0]}`); - const posFirstEqual = match[0].indexOf('='); - const key = match[0].substring(0, posFirstEqual).trim(); - const value = match[0].substring(posFirstEqual + 1).trim(); - - // Remove the quotes around the value, if any - argObj[key] = value.replace(/(^")|("$)/g, ''); - remainingText = remainingText.slice(match[0].length + 1).trim(); - continue; - } - - // does the remaining text is like nameArg=value - // where value can be a string like " this is some text " , note previously it was not possible to have have spaces - // where value can be a scalar like AScalar - // where value can be a number like +9 -1005.44 - // where value can be a macro like {{getvar::name}} - const namedScalarArgPattern = /^(\w+)=(((?["'])[^"]*(\k)|{{[^}]*}}|[+-]?\d*\.?\d+|\w*))/s; - match = namedScalarArgPattern.exec(remainingText); - if (match !== null && match[0].length > 0) { - //console.log(`matching: ${match[0]}`); - const posFirstEqual = match[0].indexOf('='); - const key = match[0].substring(0, posFirstEqual).trim(); - const value = match[0].substring(posFirstEqual + 1).trim(); - - // Remove the quotes around the value, if any - argObj[key] = value.replace(/(^")|("$)/g, ''); - remainingText = remainingText.slice(match[0].length + 1).trim(); - continue; - } - - // the remainder that matches no named argument is the "unamedArg" previously mentioned - unnamedArg = remainingText.trim(); - remainingText = ''; - } - - // Excluded commands format in their own function - if (!excludedFromRegex.includes(command)) { - console.debug(`parse: !excludedFromRegex.includes(${command}`); - console.debug(` parse: unnamedArg before: ${unnamedArg}`); - unnamedArg = getRegexedString( - unnamedArg, - regex_placement.SLASH_COMMAND, - ); - console.debug(` parse: unnamedArg after: ${unnamedArg}`); - } - - // your weird complex command is now transformed into a juicy tiny text or something useful :) - if (this.commands[command]) { - return { command: this.commands[command], args: argObj, value: unnamedArg, commandName: command }; - } - - return null; - } - - getHelpString() { - const listItems = Object - .entries(this.helpStrings) - .sort((a, b) => a[0].localeCompare(b[0])) - .map(x => x[1]) - .map(x => `
  • ${x}
  • `) - .join('\n'); - return `

    Slash commands:

      ${listItems}
    - Slash commands can be batched into a single input by adding a pipe character | at the end, and then writing a new slash command. -
    • Example:/cut 1 | /sys Hello, | /continue
    • -
    • This will remove the first message in chat, send a system message that starts with 'Hello,', and then ask the AI to continue the message.
    `; - } -} - -export const parser = new NewSlashCommandParser(); +export const parser = new SlashCommandParser(); const registerSlashCommand = parser.addCommand.bind(parser); const getSlashCommandsHelp = parser.getHelpString.bind(parser);