import { addOneMessage, characters, chat, chat_metadata, default_avatar, eventSource, event_types, extractMessageBias, getThumbnailUrl, replaceBiasMarkup, saveChatConditional, sendSystemMessage, setUserName, substituteParams, comment_avatar, system_avatar, system_message_types, setCharacterId, generateQuietPrompt, reloadCurrentChat, sendMessageAsUser, name1, Generate, this_chid, setCharacterName, generateRaw, } from "../script.js"; import { getMessageTimeStamp } from "./RossAscends-mods.js"; import { findGroupMemberId, groups, is_group_generating, resetSelectedGroup, saveGroupChat, selected_group } from "./group-chats.js"; import { getRegexedString, regex_placement } from "./extensions/regex/engine.js"; import { addEphemeralStoppingString, chat_styles, power_user } from "./power-user.js"; import { autoSelectPersona } from "./personas.js"; import { getContext } from "./extensions.js"; import { hideChatMessage, unhideChatMessage } from "./chats.js"; import { stringToRange } from "./utils.js"; import { registerVariableCommands } from "./variables.js"; export { executeSlashCommands, registerSlashCommand, getSlashCommandsHelp, } class SlashCommandParser { constructor() { this.commands = {}; this.helpStrings = {}; } addCommand(command, callback, aliases, helpString = '', interruptsGeneration = false, purgeFromMessage = true) { const fnObj = { callback, helpString, interruptsGeneration, purgeFromMessage }; if ([command, ...aliases].some(x => this.commands.hasOwnProperty(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; } parse(text) { const excludedFromRegex = ["sendas"] const firstSpace = text.indexOf(' '); const command = firstSpace !== -1 ? text.substring(1, firstSpace) : text.substring(1); const args = firstSpace !== -1 ? text.substring(firstSpace + 1) : ''; const argObj = {}; let unnamedArg; if (args.length > 0) { // Match named arguments const namedArgPattern = /(\w+)=("(?:\\.|[^"\\])*"|\S+)/g; let match; while ((match = namedArgPattern.exec(args)) !== null) { const key = match[1]; const value = match[2]; // Remove the quotes around the value, if any argObj[key] = substituteParams(value.replace(/(^")|("$)/g, '')); } // Match unnamed argument const unnamedArgPattern = /(?:\w+=(?:"(?:\\.|[^"\\])*"|\S+)\s*)*(.*)/s; match = unnamedArgPattern.exec(args); if (match !== null) { unnamedArg = match[1].trim(); } // Excluded commands format in their own function if (!excludedFromRegex.includes(command)) { unnamedArg = getRegexedString( unnamedArg, regex_placement.SLASH_COMMAND ); } } if (this.commands[command]) { return { command: this.commands[command], args: argObj, value: unnamedArg }; } return false; } 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. `; } } const parser = new SlashCommandParser(); const registerSlashCommand = parser.addCommand.bind(parser); const getSlashCommandsHelp = parser.getHelpString.bind(parser); parser.addCommand('?', helpCommandCallback, ['help'], ' – get help on macros, chat formatting and commands', true, true); parser.addCommand('name', setNameCallback, ['persona'], '(name) – sets user name and persona avatar (if set)', true, true); parser.addCommand('sync', syncCallback, [], ' – syncs user name in user-attributed messages in the current chat', true, true); parser.addCommand('lock', bindCallback, ['bind'], ' – locks/unlocks a persona (name and avatar) to the current chat', true, true); parser.addCommand('bg', setBackgroundCallback, ['background'], '(filename) – sets a background according to filename, partial names allowed', false, true); parser.addCommand('sendas', sendMessageAs, [], ` – sends message as a specific character. Uses character avatar if it exists in the characters list. Example that will send "Hello, guys!" from "Chloe": /sendas name="Chloe" Hello, guys!`, true, true); parser.addCommand('sys', sendNarratorMessage, ['nar'], '(text) – sends message as a system narrator', false, true); parser.addCommand('sysname', setNarratorName, [], '(name) – sets a name for future system narrator messages in this chat (display only). Default: System. Leave empty to reset.', true, true); parser.addCommand('comment', sendCommentMessage, [], '(text) – adds a note/comment message not part of the chat', false, true); parser.addCommand('single', setStoryModeCallback, ['story'], ' – sets the message style to single document mode without names or avatars visible', true, true); parser.addCommand('bubble', setBubbleModeCallback, ['bubbles'], ' – sets the message style to bubble chat mode', true, true); parser.addCommand('flat', setFlatModeCallback, ['default'], ' – sets the message style to flat chat mode', true, true); parser.addCommand('continue', continueChatCallback, ['cont'], ' – continues the last message in the chat', true, true); parser.addCommand('go', goToCharacterCallback, ['char'], '(name) – opens up a chat with the character by its name', true, true); parser.addCommand('sysgen', generateSystemMessage, [], '(prompt) – generates a system message using a specified prompt', true, true); parser.addCommand('ask', askCharacter, [], '(prompt) – asks a specified character card a prompt', true, true); parser.addCommand('delname', deleteMessagesByNameCallback, ['cancel'], '(name) – deletes all messages attributed to a specified name', true, true); parser.addCommand('send', sendUserMessageCallback, ['add'], '(text) – adds a user message to the chat log without triggering a generation', true, true); parser.addCommand('trigger', triggerGroupMessageCallback, [], '(member index or name) – triggers a message generation for the specified group member', true, true); parser.addCommand('hide', hideMessageCallback, [], '(message index or range) – hides a chat message from the prompt', true, true); parser.addCommand('unhide', unhideMessageCallback, [], '(message index or range) – unhides a message from the prompt', true, true); parser.addCommand('disable', disableGroupMemberCallback, [], '(member index or name) – disables a group member from being drafted for replies', true, true); parser.addCommand('enable', enableGroupMemberCallback, [], '(member index or name) – enables a group member to be drafted for replies', true, true); parser.addCommand('memberadd', addGroupMemberCallback, ['addmember'], '(character name) – adds a new group member to the group chat', true, true); parser.addCommand('memberremove', removeGroupMemberCallback, ['removemember'], '(member index or name) – removes a group member from the group chat', true, true); parser.addCommand('memberup', moveGroupMemberUpCallback, ['upmember'], '(member index or name) – moves a group member up in the group chat list', true, true); parser.addCommand('memberdown', moveGroupMemberDownCallback, ['downmember'], '(member index or name) – moves a group member down in the group chat list', true, true); parser.addCommand('peek', peekCallback, [], '(message index or range) – shows a group member character card without switching chats', true, true); parser.addCommand('delswipe', deleteSwipeCallback, ['swipedel'], '(optional 1-based id) – deletes a swipe from the last chat message. If swipe id not provided - deletes the current swipe.', true, true); parser.addCommand('echo', echoCallback, [], '(text) – echoes the text to toast message. Useful for pipes debugging.', true, true); parser.addCommand('gen', generateCallback, [], '(prompt) – generates text using the provided prompt and passes it to the next command through the pipe.', true, true); parser.addCommand('genraw', generateRawCallback, [], '(prompt) – generates text using the provided prompt and passes it to the next command through the pipe. Does not include chat history or character card. Use instruct=off to skip instruct formatting, e.g. /genraw instruct=off Why is the sky blue?. Use stop=... with a JSON-serialized array to add one-time custom stop strings, e.g. /genraw stop=["\\n"] Say hi', true, true); parser.addCommand('addswipe', addSwipeCallback, ['swipeadd'], '(text) – adds a swipe to the last chat message.', true, true); parser.addCommand('abort', abortCallback, [], ' – aborts the slash command batch execution', true, true); registerVariableCommands(); const NARRATOR_NAME_KEY = 'narrator_name'; const NARRATOR_NAME_DEFAULT = 'System'; export const COMMENT_NAME_DEFAULT = 'Note'; function abortCallback() { $('#send_textarea').val(''); throw new Error('/abort command executed'); } async function generateRawCallback(args, value) { if (!value) { console.warn('WARN: No argument provided for /genraw command'); return; } // Prevent generate recursion $('#send_textarea').val(''); if (typeof args.stop === 'string' && args.stop.length) { try { const stopStrings = JSON.parse(args.stop); if (Array.isArray(stopStrings)) { for (const stopString of stopStrings) { addEphemeralStoppingString(stopString); } } } catch { // Do nothing } } const result = await generateRaw(value, '', args.instruct); return result; } async function generateCallback(_, arg) { if (!arg) { console.warn('WARN: No argument provided for /gen command'); return; } // Prevent generate recursion $('#send_textarea').val(''); const result = await generateQuietPrompt(arg, false, false, ''); return result; } async function echoCallback(_, arg) { if (!String(arg)) { console.warn('WARN: No argument provided for /echo command'); return; } toastr.info(String(arg)); return arg; } async function addSwipeCallback(_, arg) { const lastMessage = chat[chat.length - 1]; if (!lastMessage) { toastr.warning("No messages to add swipes to."); return; } if (!arg) { console.warn('WARN: No argument provided for /addswipe command'); return; } if (lastMessage.is_user) { toastr.warning("Can't add swipes to user messages."); return; } if (lastMessage.is_system) { toastr.warning("Can't add swipes to system messages."); return; } if (lastMessage.extra?.image) { toastr.warning("Can't add swipes to message containing an image."); return; } if (!Array.isArray(lastMessage.swipes)) { lastMessage.swipes = [lastMessage.mes]; lastMessage.swipe_info = [{}]; lastMessage.swipe_id = 0; } lastMessage.swipes.push(arg); lastMessage.swipe_info.push({ send_date: getMessageTimeStamp(), gen_started: null, gen_finished: null, extra: { bias: extractMessageBias(arg), gen_id: Date.now(), api: 'manual', model: 'slash command', } }); await saveChatConditional(); await reloadCurrentChat(); } async function deleteSwipeCallback(_, arg) { const lastMessage = chat[chat.length - 1]; if (!lastMessage || !Array.isArray(lastMessage.swipes) || !lastMessage.swipes.length) { toastr.warning("No messages to delete swipes from."); return; } if (lastMessage.swipes.length <= 1) { toastr.warning("Can't delete the last swipe."); return; } const swipeId = arg && !isNaN(Number(arg)) ? (Number(arg) - 1) : lastMessage.swipe_id; if (swipeId < 0 || swipeId >= lastMessage.swipes.length) { toastr.warning(`Invalid swipe ID: ${swipeId + 1}`); return; } lastMessage.swipes.splice(swipeId, 1); if (Array.isArray(lastMessage.swipe_info) && lastMessage.swipe_info.length) { lastMessage.swipe_info.splice(swipeId, 1); } const newSwipeId = Math.min(swipeId, lastMessage.swipes.length - 1); lastMessage.swipe_id = newSwipeId; lastMessage.mes = lastMessage.swipes[newSwipeId]; await saveChatConditional(); await reloadCurrentChat(); } async function askCharacter(_, text) { // Prevent generate recursion $('#send_textarea').val(''); // Not supported in group chats // TODO: Maybe support group chats? if (selected_group) { toastr.error("Cannot run this command in a group chat!"); return; } if (!text) { console.warn('WARN: No text provided for /ask command') } const parts = text.split('\n'); if (parts.length <= 1) { toastr.warning('Both character name and message are required. Separate them with a new line.'); return; } // Grabbing the message const name = parts.shift().trim(); let mesText = parts.join('\n').trim(); const prevChId = this_chid; // Find the character const chId = characters.findIndex((e) => e.name === name); if (!characters[chId] || chId === -1) { toastr.error("Character not found."); return; } // Override character and send a user message setCharacterId(chId); // TODO: Maybe look up by filename instead of name const character = characters[chId]; let force_avatar, original_avatar; if (character && character.avatar !== 'none') { force_avatar = getThumbnailUrl('avatar', character.avatar); original_avatar = character.avatar; } else { force_avatar = default_avatar; original_avatar = default_avatar; } setCharacterName(character.name); sendMessageAsUser(mesText) const restoreCharacter = () => { setCharacterId(prevChId); setCharacterName(characters[prevChId].name); // Only force the new avatar if the character name is the same // This skips if an error was fired const lastMessage = chat[chat.length - 1]; if (lastMessage && lastMessage?.name === character.name) { lastMessage.force_avatar = force_avatar; lastMessage.original_avatar = original_avatar; } // Kill this callback once the event fires eventSource.removeListener(event_types.CHARACTER_MESSAGE_RENDERED, restoreCharacter) } // Run generate and restore previous character on error try { toastr.info(`Asking ${character.name} something...`); await Generate('ask_command') } catch { restoreCharacter() } // Restore previous character once message renders // Hack for generate eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, restoreCharacter); } async function hideMessageCallback(_, arg) { if (!arg) { console.warn('WARN: No argument provided for /hide command'); return; } const range = stringToRange(arg, 0, chat.length - 1); if (!range) { console.warn(`WARN: Invalid range provided for /hide command: ${arg}`); return; } for (let messageId = range.start; messageId <= range.end; messageId++) { const messageBlock = $(`.mes[mesid="${messageId}"]`); if (!messageBlock.length) { console.warn(`WARN: No message found with ID ${messageId}`); return; } await hideChatMessage(messageId, messageBlock); } } async function unhideMessageCallback(_, arg) { if (!arg) { console.warn('WARN: No argument provided for /unhide command'); return; } const range = stringToRange(arg, 0, chat.length - 1); if (!range) { console.warn(`WARN: Invalid range provided for /unhide command: ${arg}`); return; } for (let messageId = range.start; messageId <= range.end; messageId++) { const messageBlock = $(`.mes[mesid="${messageId}"]`); if (!messageBlock.length) { console.warn(`WARN: No message found with ID ${messageId}`); return; } await unhideChatMessage(messageId, messageBlock); } } async function disableGroupMemberCallback(_, arg) { if (!selected_group) { toastr.warning("Cannot run /disable command outside of a group chat."); return; } const chid = findGroupMemberId(arg); if (chid === undefined) { console.warn(`WARN: No group member found for argument ${arg}`); return; } $(`.group_member[chid="${chid}"] [data-action="disable"]`).trigger('click'); } async function enableGroupMemberCallback(_, arg) { if (!selected_group) { toastr.warning("Cannot run /enable command outside of a group chat."); return; } const chid = findGroupMemberId(arg); if (chid === undefined) { console.warn(`WARN: No group member found for argument ${arg}`); return; } $(`.group_member[chid="${chid}"] [data-action="enable"]`).trigger('click'); } async function moveGroupMemberUpCallback(_, arg) { if (!selected_group) { toastr.warning("Cannot run /memberup command outside of a group chat."); return; } const chid = findGroupMemberId(arg); if (chid === undefined) { console.warn(`WARN: No group member found for argument ${arg}`); return; } $(`.group_member[chid="${chid}"] [data-action="up"]`).trigger('click'); } async function moveGroupMemberDownCallback(_, arg) { if (!selected_group) { toastr.warning("Cannot run /memberdown command outside of a group chat."); return; } const chid = findGroupMemberId(arg); if (chid === undefined) { console.warn(`WARN: No group member found for argument ${arg}`); return; } $(`.group_member[chid="${chid}"] [data-action="down"]`).trigger('click'); } async function peekCallback(_, arg) { if (!selected_group) { toastr.warning("Cannot run /peek command outside of a group chat."); return; } if (is_group_generating) { toastr.warning("Cannot run /peek command while the group reply is generating."); return; } const chid = findGroupMemberId(arg); if (chid === undefined) { console.warn(`WARN: No group member found for argument ${arg}`); return; } $(`.group_member[chid="${chid}"] [data-action="view"]`).trigger('click'); } async function removeGroupMemberCallback(_, arg) { if (!selected_group) { toastr.warning("Cannot run /memberremove command outside of a group chat."); return; } if (is_group_generating) { toastr.warning("Cannot run /memberremove command while the group reply is generating."); return; } const chid = findGroupMemberId(arg); if (chid === undefined) { console.warn(`WARN: No group member found for argument ${arg}`); return; } $(`.group_member[chid="${chid}"] [data-action="remove"]`).trigger('click'); } async function addGroupMemberCallback(_, arg) { if (!selected_group) { toastr.warning("Cannot run /memberadd command outside of a group chat."); return; } if (!arg) { console.warn('WARN: No argument provided for /memberadd command'); return; } arg = arg.trim(); const chid = findCharacterIndex(arg); if (chid === -1) { console.warn(`WARN: No character found for argument ${arg}`); return; } const character = characters[chid]; const group = groups.find(x => x.id === selected_group); if (!group || !Array.isArray(group.members)) { console.warn(`WARN: No group found for ID ${selected_group}`); return; } const avatar = character.avatar; if (group.members.includes(avatar)) { toastr.warning(`${character.name} is already a member of this group.`); return; } group.members.push(avatar); await saveGroupChat(selected_group, true); // Trigger to reload group UI $('#rm_button_selected_ch').trigger('click'); } async function triggerGroupMessageCallback(_, arg) { if (!selected_group) { toastr.warning("Cannot run /trigger command outside of a group chat."); return; } if (is_group_generating) { toastr.warning("Cannot run trigger command while the group reply is generating."); return; } // Prevent generate recursion $('#send_textarea').val(''); const chid = findGroupMemberId(arg); if (chid === undefined) { console.warn(`WARN: No group member found for argument ${arg}`); return; } Generate('normal', { force_chid: chid }); } async function sendUserMessageCallback(_, text) { if (!text) { console.warn('WARN: No text provided for /send command'); return; } text = text.trim(); const bias = extractMessageBias(text); await sendMessageAsUser(text, bias); } async function deleteMessagesByNameCallback(_, name) { if (!name) { console.warn('WARN: No name provided for /delname command'); return; } name = name.trim(); const messagesToDelete = []; chat.forEach((value) => { if (value.name === name) { messagesToDelete.push(value); } }); if (!messagesToDelete.length) { console.debug('/delname: Nothing to delete'); return; } for (const message of messagesToDelete) { const index = chat.indexOf(message); if (index !== -1) { console.debug(`/delname: Deleting message #${index}`, message); chat.splice(index, 1); } } await saveChatConditional(); await reloadCurrentChat(); toastr.info(`Deleted ${messagesToDelete.length} messages from ${name}`); } function findCharacterIndex(name) { const matchTypes = [ (a, b) => a === b, (a, b) => a.startsWith(b), (a, b) => a.includes(b), ]; for (const matchType of matchTypes) { const index = characters.findIndex(x => matchType(x.name.toLowerCase(), name.toLowerCase())); if (index !== -1) { return index; } } return -1; } function goToCharacterCallback(_, name) { if (!name) { console.warn('WARN: No character name provided for /go command'); return; } name = name.trim(); const characterIndex = findCharacterIndex(name); if (characterIndex !== -1) { openChat(new String(characterIndex)); } else { console.warn(`No matches found for name "${name}"`); } } function openChat(id) { resetSelectedGroup(); setCharacterId(id); setTimeout(() => { reloadCurrentChat(); }, 1); } function continueChatCallback() { // Prevent infinite recursion $('#send_textarea').val(''); $('#option_continue').trigger('click', { fromSlashCommand: true }); } export async function generateSystemMessage(_, prompt) { $('#send_textarea').val(''); if (!prompt) { console.warn('WARN: No prompt provided for /sysgen command'); toastr.warning('You must provide a prompt for the system message'); return; } // Generate and regex the output if applicable toastr.info('Please wait', 'Generating...'); let message = await generateQuietPrompt(prompt); message = getRegexedString(message, regex_placement.SLASH_COMMAND); sendNarratorMessage(_, message); } function syncCallback() { $('#sync_name_button').trigger('click'); } function bindCallback() { $('#lock_user_name').trigger('click'); } function setStoryModeCallback() { $('#chat_display').val(chat_styles.DOCUMENT).trigger('change'); } function setBubbleModeCallback() { $('#chat_display').val(chat_styles.BUBBLES).trigger('change'); } function setFlatModeCallback() { $('#chat_display').val(chat_styles.DEFAULT).trigger('change'); } function setNameCallback(_, name) { if (!name) { toastr.warning('you must specify a name to change to') return; } name = name.trim(); // If the name is a persona, auto-select it for (let persona of Object.values(power_user.personas)) { if (persona.toLowerCase() === name.toLowerCase()) { autoSelectPersona(name); return; } } // Otherwise, set just the name setUserName(name); //this prevented quickReply usage } async function setNarratorName(_, text) { const name = text || NARRATOR_NAME_DEFAULT; chat_metadata[NARRATOR_NAME_KEY] = name; toastr.info(`System narrator name set to ${name}`); await saveChatConditional(); } export async function sendMessageAs(namedArgs, text) { if (!text) { return; } let name; let mesText; if (namedArgs.name) { name = namedArgs.name.trim(); mesText = text.trim(); if (!name && !text) { toastr.warning('You must specify a name and text to send as'); return; } } else { const parts = text.split('\n'); if (parts.length <= 1) { toastr.warning('Both character name and message are required. Separate them with a new line.'); return; } name = parts.shift().trim(); mesText = parts.join('\n').trim(); } // Requires a regex check after the slash command is pushed to output mesText = getRegexedString(mesText, regex_placement.SLASH_COMMAND, { characterOverride: name }); // Messages that do nothing but set bias will be hidden from the context const bias = extractMessageBias(mesText); const isSystem = replaceBiasMarkup(mesText).trim().length === 0; const character = characters.find(x => x.name === name); let force_avatar, original_avatar; if (character && character.avatar !== 'none') { force_avatar = getThumbnailUrl('avatar', character.avatar); original_avatar = character.avatar; } else { force_avatar = default_avatar; original_avatar = default_avatar; } const message = { name: name, is_user: false, is_system: isSystem, send_date: getMessageTimeStamp(), mes: substituteParams(mesText), force_avatar: force_avatar, original_avatar: original_avatar, extra: { bias: bias.trim().length ? bias : null, gen_id: Date.now(), } }; chat.push(message); await eventSource.emit(event_types.MESSAGE_SENT, (chat.length - 1)); addOneMessage(message); await eventSource.emit(event_types.USER_MESSAGE_RENDERED, (chat.length - 1)); await saveChatConditional(); } export async function sendNarratorMessage(_, text) { if (!text) { return; } const name = chat_metadata[NARRATOR_NAME_KEY] || NARRATOR_NAME_DEFAULT; // Messages that do nothing but set bias will be hidden from the context const bias = extractMessageBias(text); const isSystem = replaceBiasMarkup(text).trim().length === 0; const message = { name: name, is_user: false, is_system: isSystem, send_date: getMessageTimeStamp(), mes: substituteParams(text.trim()), force_avatar: system_avatar, extra: { type: system_message_types.NARRATOR, bias: bias.trim().length ? bias : null, gen_id: Date.now(), }, }; chat.push(message); await eventSource.emit(event_types.MESSAGE_SENT, (chat.length - 1)); addOneMessage(message); await eventSource.emit(event_types.USER_MESSAGE_RENDERED, (chat.length - 1)); await saveChatConditional(); } export async function promptQuietForLoudResponse(who, text) { let character_id = getContext().characterId; if (who === 'sys') { text = "System: " + text; } else if (who === 'user') { text = name1 + ": " + text; } else if (who === 'char') { text = characters[character_id].name + ": " + text; } else if (who === 'raw') { text = text; } //text = `${text}${power_user.instruct.enabled ? '' : '\n'}${(power_user.always_force_name2 && who != 'raw') ? characters[character_id].name + ":" : ""}` let reply = await generateQuietPrompt(text, true); text = await getRegexedString(reply, regex_placement.SLASH_COMMAND); const message = { name: characters[character_id].name, is_user: false, is_name: true, is_system: false, send_date: getMessageTimeStamp(), mes: substituteParams(text.trim()), extra: { type: system_message_types.COMMENT, gen_id: Date.now(), }, }; chat.push(message); await eventSource.emit(event_types.MESSAGE_SENT, (chat.length - 1)); addOneMessage(message); await eventSource.emit(event_types.USER_MESSAGE_RENDERED, (chat.length - 1)); await saveChatConditional(); } async function sendCommentMessage(_, text) { if (!text) { return; } const message = { name: COMMENT_NAME_DEFAULT, is_user: false, is_system: true, send_date: getMessageTimeStamp(), mes: substituteParams(text.trim()), force_avatar: comment_avatar, extra: { type: system_message_types.COMMENT, gen_id: Date.now(), }, }; chat.push(message); await eventSource.emit(event_types.MESSAGE_SENT, (chat.length - 1)); addOneMessage(message); await eventSource.emit(event_types.USER_MESSAGE_RENDERED, (chat.length - 1)); await saveChatConditional(); } /** * Displays a help message from the slash command * @param {any} _ Unused * @param {string} type Type of help to display */ function helpCommandCallback(_, type) { switch (type?.trim()?.toLowerCase()) { case 'slash': case 'commands': case 'slashes': case 'slash commands': case '1': sendSystemMessage(system_message_types.SLASH_COMMANDS); break; case 'format': case 'formatting': case 'formats': case 'chat formatting': case '2': sendSystemMessage(system_message_types.FORMATTING); break; case 'hotkeys': case 'hotkey': case '3': sendSystemMessage(system_message_types.HOTKEYS); break; case 'macros': case 'macro': case '4': sendSystemMessage(system_message_types.MACROS); break; default: sendSystemMessage(system_message_types.HELP); break; } } $(document).on('click', '[data-displayHelp]', function (e) { e.preventDefault(); const page = String($(this).data('displayhelp')); helpCommandCallback(null, page); }); function setBackgroundCallback(_, bg) { if (!bg) { return; } console.log('Set background to ' + bg); const bgElements = Array.from(document.querySelectorAll(`.bg_example`)).map((x) => ({ element: x, bgfile: x.getAttribute('bgfile') })); const fuse = new Fuse(bgElements, { keys: ['bgfile'] }); const result = fuse.search(bg); if (!result.length) { toastr.error(`No background found with name "${bg}"`); return; } const bgElement = result[0].item.element; if (bgElement instanceof HTMLElement) { bgElement.click(); } } /** * Executes slash commands in the provided text * @param {string} text Slash command text * @param {boolean} unescape Whether to unescape the batch separator * @returns {Promise<{interrupt: boolean, newText: string, pipe: string} | boolean>} */ async function executeSlashCommands(text, unescape = false) { if (!text) { return false; } // Unescape the pipe character if (unescape) { text = text.replace(/\\\|/g, '|'); } // Hack to allow multi-line slash commands // All slash command messages should begin with a slash const placeholder = '\u200B'; // Use a zero-width space as a placeholder const chars = text.split(''); for (let i = 1; i < chars.length; i++) { if (chars[i] === '|' && chars[i - 1] !== '\\') { chars[i] = placeholder; } } const lines = chars.join('').split(placeholder).map(line => line.trim()); const linesToRemove = []; let interrupt = false; let pipeResult = ''; for (let index = 0; index < lines.length; index++) { const trimmedLine = lines[index].trim(); if (!trimmedLine.startsWith('/')) { continue; } const result = parser.parse(trimmedLine); if (!result) { continue; } if (result.value && typeof result.value === 'string') { result.value = substituteParams(result.value.trim()); } console.debug('Slash command executing:', result); let unnamedArg = result.value || pipeResult; if (pipeResult && typeof result.args === 'object') { for (const [key, value] of Object.entries(result.args)) { if (typeof value === 'string' && /{{pipe}}/i.test(value)) { result.args[key] = value.replace(/{{pipe}}/i, pipeResult); } } } if (pipeResult && typeof unnamedArg === 'string' && /{{pipe}}/i.test(unnamedArg)) { unnamedArg = unnamedArg.replace(/{{pipe}}/i, pipeResult); } pipeResult = await result.command.callback(result.args, unnamedArg); if (result.command.interruptsGeneration) { interrupt = true; } if (result.command.purgeFromMessage) { linesToRemove.push(lines[index]); } } const newText = lines.filter(x => linesToRemove.indexOf(x) === -1).join('\n'); return { interrupt, newText, pipe: pipeResult }; } function setSlashCommandAutocomplete(textarea) { textarea.autocomplete({ source: (input, output) => { // Only show for slash commands and if there's no space if (!input.term.startsWith('/') || input.term.includes(' ')) { output([]); return; } const slashCommand = input.term.toLowerCase().substring(1); // Remove the slash const result = Object .keys(parser.helpStrings) // Get all slash commands .filter(x => x.startsWith(slashCommand)) // Filter by the input .sort((a, b) => a.localeCompare(b)) // Sort alphabetically // .slice(0, 20) // Limit to 20 results .map(x => ({ label: parser.helpStrings[x], value: `/${x} ` })); // Map to the help string output(result); // Return the results }, select: (e, u) => { // unfocus the input $(e.target).val(u.item.value); }, minLength: 1, position: { my: "left bottom", at: "left top", collision: "none" }, }); textarea.autocomplete("instance")._renderItem = function (ul, item) { const width = $(textarea).innerWidth(); const content = $('
    ').html(item.label); return $("
  • ").width(width).append(content).appendTo(ul); }; } jQuery(function () { const textarea = $('#send_textarea'); setSlashCommandAutocomplete(textarea); })