Spaces:
Build error
Build error
import { | |
addOneMessage, | |
autoSelectPersona, | |
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, | |
} from "../script.js"; | |
import { humanizedDateTime } from "./RossAscends-mods.js"; | |
import { resetSelectedGroup } from "./group-chats.js"; | |
import { getRegexedString, regex_placement } from "./extensions/regex/engine.js"; | |
import { chat_styles, power_user } from "./power-user.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 = `<span class="monospace">/${command}</span> ${helpString} `; | |
if (Array.isArray(aliases) && aliases.length) { | |
let aliasesString = `(aliases: ${aliases.map(x => `<span class="monospace">/${x}</span>`).join(', ')})`; | |
stringBuilder += aliasesString; | |
} | |
this.helpStrings.push(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) { | |
const argsArray = args.split(' '); | |
for (let arg of argsArray) { | |
const equalsIndex = arg.indexOf('='); | |
if (equalsIndex !== -1) { | |
const key = arg.substring(0, equalsIndex); | |
const value = arg.substring(equalsIndex + 1); | |
argObj[key] = value; | |
} | |
else { | |
break; | |
} | |
} | |
unnamedArg = argsArray.slice(Object.keys(argObj).length).join(' '); | |
// 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 = this.helpStrings.map(x => `<li>${x}</li>`).join('\n'); | |
return `<p>Slash commands:</p><ol>${listItems}</ol>`; | |
} | |
} | |
const parser = new SlashCommandParser(); | |
const registerSlashCommand = parser.addCommand.bind(parser); | |
const getSlashCommandsHelp = parser.getHelpString.bind(parser); | |
parser.addCommand('help', helpCommandCallback, ['?'], ' β displays this help message', true, true); | |
parser.addCommand('name', setNameCallback, ['persona'], '<span class="monospace">(name)</span> β 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'], '<span class="monospace">(filename)</span> β sets a background according to filename, partial names allowed, will set the first one alphabetically if multiple files begin with the provided argument string', false, true); | |
parser.addCommand('sendas', sendMessageAs, [], ` β sends message as a specific character.<br>Example:<br><pre><code>/sendas Chloe\nHello, guys!</code></pre>will send "Hello, guys!" from "Chloe".<br>Uses character avatar if it exists in the characters list.`, true, true); | |
parser.addCommand('sys', sendNarratorMessage, [], '<span class="monospace">(text)</span> β sends message as a system narrator', false, true); | |
parser.addCommand('sysname', setNarratorName, [], '<span class="monospace">(name)</span> β 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, [], '<span class="monospace">(text)</span> β 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'], '<span class="monospace">(name)</span> β opens up a chat with the character by its name', true, true); | |
parser.addCommand('sysgen', generateSystemMessage, [], '<span class="monospace">(prompt)</span> β generates a system message using a specified prompt', true, true); | |
parser.addCommand('delname', deleteMessagesByNameCallback, ['cancel'], '<span class="monospace">(name)</span> β deletes all messages attributed to a specified name', true, true); | |
parser.addCommand('send', sendUserMessageCallback, ['add'], '<span class="monospace">(text)</span> β adds a user message to the chat log without triggering a generation', true, true); | |
const NARRATOR_NAME_KEY = 'narrator_name'; | |
const NARRATOR_NAME_DEFAULT = 'System'; | |
const COMMENT_NAME_DEFAULT = 'Note'; | |
async function sendUserMessageCallback(_, text) { | |
if (!text) { | |
console.warn('WARN: No text provided for /send command'); | |
return; | |
} | |
text = text.trim(); | |
const bias = extractMessageBias(text); | |
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 }); | |
} | |
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 | |
} | |
function setNarratorName(_, text) { | |
const name = text || NARRATOR_NAME_DEFAULT; | |
chat_metadata[NARRATOR_NAME_KEY] = name; | |
toastr.info(`System narrator name set to ${name}`); | |
saveChatConditional(); | |
} | |
async function sendMessageAs(_, text) { | |
if (!text) { | |
return; | |
} | |
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; | |
} | |
const name = parts.shift().trim(); | |
let 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_name: true, | |
is_system: isSystem, | |
send_date: humanizedDateTime(), | |
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); | |
addOneMessage(message); | |
await eventSource.emit(event_types.MESSAGE_SENT, (chat.length - 1)); | |
saveChatConditional(); | |
} | |
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_name: false, | |
is_system: isSystem, | |
send_date: humanizedDateTime(), | |
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); | |
addOneMessage(message); | |
await eventSource.emit(event_types.MESSAGE_SENT, (chat.length - 1)); | |
saveChatConditional(); | |
} | |
async function sendCommentMessage(_, text) { | |
if (!text) { | |
return; | |
} | |
const message = { | |
name: COMMENT_NAME_DEFAULT, | |
is_user: false, | |
is_name: true, | |
is_system: true, | |
send_date: humanizedDateTime(), | |
mes: substituteParams(text.trim()), | |
force_avatar: comment_avatar, | |
extra: { | |
type: system_message_types.COMMENT, | |
gen_id: Date.now(), | |
}, | |
}; | |
chat.push(message); | |
addOneMessage(message); | |
await eventSource.emit(event_types.MESSAGE_SENT, (chat.length - 1)); | |
saveChatConditional(); | |
} | |
function helpCommandCallback(_, type) { | |
switch (type?.trim()) { | |
case 'slash': | |
case '1': | |
sendSystemMessage(system_message_types.SLASH_COMMANDS); | |
break; | |
case 'format': | |
case '2': | |
sendSystemMessage(system_message_types.FORMATTING); | |
break; | |
case 'hotkeys': | |
case '3': | |
sendSystemMessage(system_message_types.HOTKEYS); | |
break; | |
case 'macros': | |
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 bgElement = $(`.bg_example[bgfile^="${bg.trim()}"`); | |
if (bgElement.length) { | |
bgElement.get(0).click(); | |
} | |
} | |
function executeSlashCommands(text) { | |
if (!text) { | |
return false; | |
} | |
// Hack to allow multi-line slash commands | |
// All slash command messages should begin with a slash | |
const lines = text.split('|').map(line => line.trim()); | |
const linesToRemove = []; | |
let interrupt = false; | |
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; | |
} | |
console.debug('Slash command executing:', result); | |
result.command.callback(result.args, result.value); | |
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 }; | |
} | |