import { Generate, activateSendButtons, addOneMessage, callPopup, characters, chat, chat_metadata, comment_avatar, deactivateSendButtons, default_avatar, eventSource, event_types, extension_prompt_roles, extension_prompt_types, extractMessageBias, generateQuietPrompt, generateRaw, getThumbnailUrl, is_send_press, main_api, name1, reloadCurrentChat, removeMacros, retriggerFirstMessageOnEmptyChat, saveChatConditional, sendMessageAsUser, sendSystemMessage, setActiveCharacter, setActiveGroup, setCharacterId, setCharacterName, setExtensionPrompt, setUserName, substituteParams, system_avatar, system_message_types, this_chid, } from '../script.js'; import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { SlashCommandParserError } from './slash-commands/SlashCommandParserError.js'; import { getMessageTimeStamp } from './RossAscends-mods.js'; import { hideChatMessageRange } from './chats.js'; import { getContext, saveMetadataDebounced } from './extensions.js'; import { getRegexedString, regex_placement } from './extensions/regex/engine.js'; import { findGroupMemberId, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group } from './group-chats.js'; import { chat_completion_sources, oai_settings } from './openai.js'; import { autoSelectPersona } from './personas.js'; import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js'; import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js'; import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync } from './tokenizers.js'; import { delay, isFalseBoolean, isTrueBoolean, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js'; import { registerVariableCommands, resolveVariable } from './variables.js'; import { background_settings } from './backgrounds.js'; import { SlashCommandScope } from './slash-commands/SlashCommandScope.js'; import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js'; import { NAME_RESULT_TYPE, SlashCommandParserNameResult } from './slash-commands/SlashCommandParserNameResult.js'; import { SlashCommandAutoCompleteOption } from './slash-commands/SlashCommandAutoCompleteOption.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; import { SlashCommandAutoComplete } from './slash-commands/SlashCommandAutoComplete.js'; import { SlashCommand } from './slash-commands/SlashCommand.js'; export { executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand, }; export const parser = new SlashCommandParser(); const registerSlashCommand = parser.addCommand.bind(parser); const getSlashCommandsHelp = parser.getHelpString.bind(parser); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: '?', callback: helpCommandCallback, aliases: ['help'], unnamedArgumentList: [new SlashCommandArgument( 'help topic', ARGUMENT_TYPE.STRING, false, false, null, ['slash', 'format', 'hotkeys', 'macros'], )], helpString: 'Get help on macros, chat formatting and commands.', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'name', callback: setNameCallback, unnamedArgumentList: [ new SlashCommandArgument( 'persona', [ARGUMENT_TYPE.STRING], true, ), ], helpString: 'Sets user name and persona avatar (if set).', aliases: ['persona'], interruptsGeneration: true, purgeFromMessage: true, })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'sync', callback: syncCallback, helpString: 'Syncs the user persona in user-attributed messages in the current chat.', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'lock', callback: bindCallback, aliases: ['bind'], helpString: 'Locks/unlocks a persona (name and avatar) to the current chat', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'bg', callback: setBackgroundCallback, aliases: ['background'], returns: 'the current background', unnamedArgumentList: [ new SlashCommandArgument( 'filename', [ARGUMENT_TYPE.STRING], true, ), ], helpString: `
/bg beach.jpg
/sendas name="Chloe" Hello, guys!
will send "Hello, guys!" from "Chloe".
compact
is set to true
, the message is sent using a compact layout.
/sys The sun sets in the west.
/sys compact=true A brief note.
compact
is set to true
, the message is sent using a compact layout.
/comment This is a comment
/comment compact=true This is a compact comment
/continue
Continues the chat with no additional prompt.
/continue Let's explore this further...
Continues the chat with the provided prompt.
/delname John
compact
is set to true
, the message is sent using a compact layout.
/send Hello there!
/send compact=true Hi
await=true
named argument is passed, the command will await for the triggered generation before continuing.
/memberadd John Doe
/memberremove 2
/memberremove John Doe
/peek 5
Shows the character card for the 5th message.
/peek 2-5
Shows the character cards for messages 2 through 5.
/delswipe
Deletes the current swipe.
/delswipe 2
Deletes the second swipe from the last chat message.
/echo title="My Message" severity=info This is an info message
/genraw instruct=off Why is the sky blue?
/genraw stop=["\\n"] Say hi
list
against the text to search
.
If any item matches, then its name is returned. If no item matches the text, no value is returned.
threshold
(default is 0.4) allows control over the match strictness.
A low value (min 0.0) means the match is very strict.
At 1.0 (max) the match is very loose and will match anything.
/fuzzy list=["a","b","c"] threshold=0.4 abc
/pass (text) – passes the text to the next command through the pipe.
/pass Hello world
/delay 1000
default
argument is the default value of the input field, and the text argument is the text to display.
{{arg::key}}
.
/messages 10
Returns the 10th message.
/messages names=on 5-10
Returns messages 5 through 10 with author names.
/setinput Hello world
/popup large=on wide=on okButton="Submit" Enter some text:
/buttons labels=["Yes","No"] Do you want to continue?
/trimtokens limit=5 direction=start This is a long sentence with many words
/trimstart This is a sentence. And here is another sentence.
${inject.value}
(${positionName}, depth: ${inject.depth}, scan: ${inject.scan ?? false}, role: ${inject.role ?? extension_prompt_roles.SYSTEM})`;
})
.join('\n');
const converter = new showdown.Converter();
const messageText = `### Script injections:\n${injects}`;
const htmlMessage = DOMPurify.sanitize(converter.makeHtml(messageText));
sendSystemMessage(system_message_types.GENERIC, htmlMessage);
}
function flushInjectsCallback() {
if (!chat_metadata.script_injects) {
return '';
}
for (const [id, inject] of Object.entries(chat_metadata.script_injects)) {
const prefixedId = `${SCRIPT_PROMPT_KEY}${id}`;
setExtensionPrompt(prefixedId, '', inject.position, inject.depth, inject.scan, inject.role);
}
chat_metadata.script_injects = {};
saveMetadataDebounced();
return '';
}
export function processChatSlashCommands() {
const context = getContext();
if (!(context.chatMetadata.script_injects)) {
return;
}
for (const id of Object.keys(context.extensionPrompts)) {
if (!id.startsWith(SCRIPT_PROMPT_KEY)) {
continue;
}
console.log('Removing script injection', id);
delete context.extensionPrompts[id];
}
for (const [id, inject] of Object.entries(context.chatMetadata.script_injects)) {
const prefixedId = `${SCRIPT_PROMPT_KEY}${id}`;
console.log('Adding script injection', id);
setExtensionPrompt(prefixedId, inject.value, inject.position, inject.depth, inject.scan, inject.role);
}
}
function setInputCallback(_, value) {
$('#send_textarea').val(value || '')[0].dispatchEvent(new Event('input', { bubbles:true }));
return value;
}
function trimStartCallback(_, value) {
if (!value) {
return '';
}
return trimToStartSentence(value);
}
function trimEndCallback(_, value) {
if (!value) {
return '';
}
return trimToEndSentence(value);
}
async function trimTokensCallback(arg, value) {
if (!value) {
console.warn('WARN: No argument provided for /trimtokens command');
return '';
}
const limit = Number(resolveVariable(arg.limit));
if (isNaN(limit)) {
console.warn(`WARN: Invalid limit provided for /trimtokens command: ${limit}`);
return value;
}
if (limit <= 0) {
return '';
}
const direction = arg.direction || 'end';
const tokenCount = await getTokenCountAsync(value);
// Token count is less than the limit, do nothing
if (tokenCount <= limit) {
return value;
}
const { tokenizerName, tokenizerId } = getFriendlyTokenizerName(main_api);
console.debug('Requesting tokenization for /trimtokens command', tokenizerName);
try {
const textTokens = getTextTokens(tokenizerId, value);
if (!Array.isArray(textTokens) || !textTokens.length) {
console.warn('WARN: No tokens returned for /trimtokens command, falling back to estimation');
const percentage = limit / tokenCount;
const trimIndex = Math.floor(value.length * percentage);
const trimmedText = direction === 'start' ? value.substring(trimIndex) : value.substring(0, value.length - trimIndex);
return trimmedText;
}
const sliceTokens = direction === 'start' ? textTokens.slice(0, limit) : textTokens.slice(-limit);
const { text } = decodeTextTokens(tokenizerId, sliceTokens);
return text;
} catch (error) {
console.warn('WARN: Tokenization failed for /trimtokens command, returning original', error);
return value;
}
}
async function buttonsCallback(args, text) {
try {
const buttons = JSON.parse(resolveVariable(args?.labels));
if (!Array.isArray(buttons) || !buttons.length) {
console.warn('WARN: Invalid labels provided for /buttons command');
return '';
}
return new Promise(async (resolve) => {
const safeValue = DOMPurify.sanitize(text || '');
const buttonContainer = document.createElement('div');
buttonContainer.classList.add('flex-container', 'flexFlowColumn', 'wide100p', 'm-t-1');
for (const button of buttons) {
const buttonElement = document.createElement('div');
buttonElement.classList.add('menu_button', 'wide100p');
buttonElement.addEventListener('click', () => {
resolve(button);
$('#dialogue_popup_ok').trigger('click');
});
buttonElement.innerText = button;
buttonContainer.appendChild(buttonElement);
}
const popupContainer = document.createElement('div');
popupContainer.innerHTML = safeValue;
popupContainer.appendChild(buttonContainer);
callPopup(popupContainer, 'text', '', { okButton: 'Cancel' })
.then(() => resolve(''))
.catch(() => resolve(''));
});
} catch {
return '';
}
}
async function popupCallback(args, value) {
const safeValue = DOMPurify.sanitize(value || '');
const popupOptions = {
large: isTrueBoolean(args?.large),
wide: isTrueBoolean(args?.wide),
okButton: args?.okButton !== undefined && typeof args?.okButton === 'string' ? args.okButton : 'Ok',
};
await delay(1);
await callPopup(safeValue, 'text', '', popupOptions);
await delay(1);
return value;
}
function getMessagesCallback(args, value) {
const includeNames = !isFalseBoolean(args?.names);
const range = stringToRange(value, 0, chat.length - 1);
if (!range) {
console.warn(`WARN: Invalid range provided for /getmessages command: ${value}`);
return '';
}
const messages = [];
for (let messageId = range.start; messageId <= range.end; messageId++) {
const message = chat[messageId];
if (!message) {
console.warn(`WARN: No message found with ID ${messageId}`);
continue;
}
if (message.is_system) {
continue;
}
if (includeNames) {
messages.push(`${message.name}: ${message.mes}`);
} else {
messages.push(message.mes);
}
}
return messages.join('\n\n');
}
async function runCallback(args, name) {
if (!name) {
throw new Error('No name provided for /run command');
}
/**@type {SlashCommandScope} */
const scope = args._scope;
if (scope.existsVariable(name)) {
const closure = scope.getVariable(name);
if (!(closure instanceof SlashCommandClosure)) {
throw new Error(`"${name}" is not callable.`);
}
closure.scope.parent = scope;
Object.keys(closure.arguments).forEach(key=>{
if (Object.keys(args).includes(key)) {
closure.providedArguments[key] = args[key];
}
});
const result = await closure.execute();
return result.pipe;
}
if (typeof window['executeQuickReplyByName'] !== 'function') {
throw new Error('Quick Reply extension is not loaded');
}
try {
name = name.trim();
return await window['executeQuickReplyByName'](name, args);
} catch (error) {
throw new Error(`Error running Quick Reply "${name}": ${error.message}`, 'Error');
}
}
function abortCallback() {
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles:true }));
throw new Error('/abort command executed');
}
async function delayCallback(_, amount) {
if (!amount) {
console.warn('WARN: No amount provided for /delay command');
return;
}
amount = Number(amount);
if (isNaN(amount)) {
amount = 0;
}
await delay(amount);
}
async function inputCallback(args, prompt) {
const safeValue = DOMPurify.sanitize(prompt || '');
const defaultInput = args?.default !== undefined && typeof args?.default === 'string' ? args.default : '';
const popupOptions = {
large: isTrueBoolean(args?.large),
wide: isTrueBoolean(args?.wide),
okButton: args?.okButton !== undefined && typeof args?.okButton === 'string' ? args.okButton : 'Ok',
rows: args?.rows !== undefined && typeof args?.rows === 'string' ? isNaN(Number(args.rows)) ? 4 : Number(args.rows) : 4,
};
// Do not remove this delay, otherwise the prompt will not show up
await delay(1);
const result = await callPopup(safeValue, 'input', defaultInput, popupOptions);
await delay(1);
return result || '';
}
/**
* Each item in "args.list" is searched within "search_item" using fuzzy search. If any matches it returns the matched "item".
* @param {FuzzyCommandArgs} args - arguments containing "list" (JSON array) and optionaly "threshold" (float between 0.0 and 1.0)
* @param {string} searchInValue - the string where items of list are searched
* @returns {string} - the matched item from the list
* @typedef {{list: string, threshold: string}} FuzzyCommandArgs - arguments for /fuzzy command
* @example /fuzzy list=["down","left","up","right"] "he looks up" | /echo // should return "up"
* @link https://www.fusejs.io/
*/
function fuzzyCallback(args, searchInValue) {
if (!searchInValue) {
console.warn('WARN: No argument provided for /fuzzy command');
return '';
}
if (!args.list) {
console.warn('WARN: No list argument provided for /fuzzy command');
return '';
}
try {
const list = JSON.parse(resolveVariable(args.list));
if (!Array.isArray(list)) {
console.warn('WARN: Invalid list argument provided for /fuzzy command');
return '';
}
const params = {
includeScore: true,
findAllMatches: true,
ignoreLocation: true,
threshold: 0.4,
};
// threshold determines how strict is the match, low threshold value is very strict, at 1 (nearly?) everything matches
if ('threshold' in args) {
params.threshold = parseFloat(resolveVariable(args.threshold));
if (isNaN(params.threshold)) {
console.warn('WARN: \'threshold\' argument must be a float between 0.0 and 1.0 for /fuzzy command');
return '';
}
if (params.threshold < 0) {
params.threshold = 0;
}
if (params.threshold > 1) {
params.threshold = 1;
}
}
const fuse = new Fuse([searchInValue], params);
// each item in the "list" is searched within "search_item", if any matches it returns the matched "item"
for (const searchItem of list) {
const result = fuse.search(searchItem);
if (result.length > 0) {
console.info('fuzzyCallback Matched: ' + searchItem);
return searchItem;
}
}
return '';
} catch {
console.warn('WARN: Invalid list argument provided for /fuzzy command');
return '';
}
}
function setEphemeralStopStrings(value) {
if (typeof value === 'string' && value.length) {
try {
const stopStrings = JSON.parse(value);
if (Array.isArray(stopStrings)) {
stopStrings.forEach(stopString => addEphemeralStoppingString(stopString));
}
} catch {
// Do nothing
}
}
}
async function generateRawCallback(args, value) {
if (!value) {
console.warn('WARN: No argument provided for /genraw command');
return;
}
// Prevent generate recursion
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles:true }));
const lock = isTrueBoolean(args?.lock);
const as = args?.as || 'system';
const quietToLoud = as === 'char';
const systemPrompt = resolveVariable(args?.system) || '';
const length = Number(resolveVariable(args?.length) ?? 0) || 0;
try {
if (lock) {
deactivateSendButtons();
}
setEphemeralStopStrings(resolveVariable(args?.stop));
const result = await generateRaw(value, '', isFalseBoolean(args?.instruct), quietToLoud, systemPrompt, length);
return result;
} finally {
if (lock) {
activateSendButtons();
}
flushEphemeralStoppingStrings();
}
}
async function generateCallback(args, value) {
if (!value) {
console.warn('WARN: No argument provided for /gen command');
return;
}
// Prevent generate recursion
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles:true }));
const lock = isTrueBoolean(args?.lock);
const as = args?.as || 'system';
const quietToLoud = as === 'char';
const length = Number(resolveVariable(args?.length) ?? 0) || 0;
try {
if (lock) {
deactivateSendButtons();
}
setEphemeralStopStrings(resolveVariable(args?.stop));
const name = args?.name;
const result = await generateQuietPrompt(value, quietToLoud, false, '', name, length);
return result;
} finally {
if (lock) {
activateSendButtons();
}
flushEphemeralStoppingStrings();
}
}
async function echoCallback(args, value) {
const safeValue = DOMPurify.sanitize(String(value) || '');
if (safeValue === '') {
console.warn('WARN: No argument provided for /echo command');
return;
}
const title = args?.title !== undefined && typeof args?.title === 'string' ? args.title : undefined;
const severity = args?.severity !== undefined && typeof args?.severity === 'string' ? args.severity : 'info';
switch (severity) {
case 'error':
toastr.error(safeValue, title);
break;
case 'warning':
toastr.warning(safeValue, title);
break;
case 'success':
toastr.success(safeValue, title);
break;
case 'info':
default:
toastr.info(safeValue, title);
break;
}
return value;
}
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('')[0].dispatchEvent(new Event('input', { bubbles:true }));
// 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);
await 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;
}
await hideChatMessageRange(range.start, range.end, false);
}
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 '';
}
await hideChatMessageRange(range.start, range.end, true);
return '';
}
/**
* Copium for running group actions when the member is offscreen.
* @param {number} chid - character ID
* @param {string} action - one of 'enable', 'disable', 'up', 'down', 'view', 'remove'
* @returns {void}
*/
function performGroupMemberAction(chid, action) {
const memberSelector = `.group_member[chid="${chid}"]`;
// Do not optimize. Paginator gets recreated on every action
const paginationSelector = '#rm_group_members_pagination';
const pageSizeSelector = '#rm_group_members_pagination select';
let wasOffscreen = false;
let paginationValue = null;
let pageValue = null;
if ($(memberSelector).length === 0) {
wasOffscreen = true;
paginationValue = Number($(pageSizeSelector).val());
pageValue = $(paginationSelector).pagination('getCurrentPageNum');
$(pageSizeSelector).val($(pageSizeSelector).find('option').last().val()).trigger('change');
}
$(memberSelector).find(`[data-action="${action}"]`).trigger('click');
if (wasOffscreen) {
$(pageSizeSelector).val(paginationValue).trigger('change');
if ($(paginationSelector).length) {
$(paginationSelector).pagination('go', pageValue);
}
}
}
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 '';
}
performGroupMemberAction(chid, 'disable');
return '';
}
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 '';
}
performGroupMemberAction(chid, 'enable');
return '';
}
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 '';
}
performGroupMemberAction(chid, 'up');
return '';
}
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 '';
}
performGroupMemberAction(chid, 'down');
return '';
}
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 '';
}
performGroupMemberAction(chid, 'view');
return '';
}
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 '';
}
performGroupMemberAction(chid, 'remove');
return '';
}
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');
return character.name;
}
async function triggerGenerationCallback(args, value) {
const shouldAwait = isTrueBoolean(args?.await);
const outerPromise = new Promise((outerResolve) => setTimeout(async () => {
try {
await waitUntilCondition(() => !is_send_press && !is_group_generating, 10000, 100);
} catch {
console.warn('Timeout waiting for generation unlock');
toastr.warning('Cannot run /trigger command while the reply is being generated.');
return '';
}
// Prevent generate recursion
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles:true }));
let chid = undefined;
if (selected_group && value) {
chid = findGroupMemberId(value);
if (chid === undefined) {
console.warn(`WARN: No group member found for argument ${value}`);
}
}
outerResolve(new Promise(innerResolve => setTimeout(() => innerResolve(Generate('normal', { force_chid: chid })), 100)));
}, 1));
if (shouldAwait) {
const innerPromise = await outerPromise;
await innerPromise;
}
return '';
}
async function sendUserMessageCallback(args, text) {
if (!text) {
console.warn('WARN: No text provided for /send command');
return;
}
text = text.trim();
const compact = isTrueBoolean(args?.compact);
const bias = extractMessageBias(text);
const insertAt = Number(resolveVariable(args?.at));
await sendMessageAsUser(text, bias, insertAt, compact);
return '';
}
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}`);
return '';
}
function findCharacterIndex(name) {
const matchTypes = [
(a, b) => a === b,
(a, b) => a.startsWith(b),
(a, b) => a.includes(b),
];
const exactAvatarMatch = characters.findIndex(x => x.avatar === name);
if (exactAvatarMatch !== -1) {
return exactAvatarMatch;
}
for (const matchType of matchTypes) {
const index = characters.findIndex(x => matchType(x.name.toLowerCase(), name.toLowerCase()));
if (index !== -1) {
return index;
}
}
return -1;
}
async 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) {
await openChat(new String(characterIndex));
setActiveCharacter(characters[characterIndex]?.avatar);
setActiveGroup(null);
return characters[characterIndex]?.name;
} else {
const group = groups.find(it => it.name.toLowerCase() == name.toLowerCase());
if (group) {
await openGroupById(group.id);
setActiveCharacter(null);
setActiveGroup(group.id);
return group.name;
} else {
console.warn(`No matches found for name "${name}"`);
return '';
}
}
}
async function openChat(id) {
resetSelectedGroup();
setCharacterId(id);
await delay(1);
await reloadCurrentChat();
}
function continueChatCallback(_, prompt) {
setTimeout(async () => {
try {
await waitUntilCondition(() => !is_send_press && !is_group_generating, 10000, 100);
} catch {
console.warn('Timeout waiting for generation unlock');
toastr.warning('Cannot run /continue command while the reply is being generated.');
}
// Prevent infinite recursion
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles:true }));
$('#option_continue').trigger('click', { fromSlashCommand: true, additionalPrompt: prompt });
}, 1);
return '';
}
export async function generateSystemMessage(_, prompt) {
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles:true }));
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, false, false);
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);
retriggerFirstMessageOnEmptyChat();
return;
}
}
// Otherwise, set just the name
setUserName(name); //this prevented quickReply usage
retriggerFirstMessageOnEmptyChat();
}
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(args, text) {
if (!text) {
return;
}
let name;
let mesText;
if (args.name) {
name = args.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 = bias && !removeMacros(mesText).length;
const compact = isTrueBoolean(args?.compact);
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(),
isSmallSys: compact,
},
};
const insertAt = Number(resolveVariable(args.at));
if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) {
chat.splice(insertAt, 0, message);
await saveChatConditional();
await eventSource.emit(event_types.MESSAGE_RECEIVED, insertAt);
await reloadCurrentChat();
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, insertAt);
} else {
chat.push(message);
await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1));
addOneMessage(message);
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, (chat.length - 1));
await saveChatConditional();
}
}
export async function sendNarratorMessage(args, 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 = bias && !removeMacros(text).length;
const compact = isTrueBoolean(args?.compact);
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(),
isSmallSys: compact,
},
};
const insertAt = Number(resolveVariable(args.at));
if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) {
chat.splice(insertAt, 0, message);
await saveChatConditional();
await eventSource.emit(event_types.MESSAGE_SENT, insertAt);
await reloadCurrentChat();
await eventSource.emit(event_types.USER_MESSAGE_RENDERED, insertAt);
} else {
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') {
// We don't need to modify the 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, false);
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(args, text) {
if (!text) {
return;
}
const compact = isTrueBoolean(args?.compact);
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(),
isSmallSys: compact,
},
};
const insertAt = Number(resolveVariable(args.at));
if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) {
chat.splice(insertAt, 0, message);
await saveChatConditional();
await eventSource.emit(event_types.MESSAGE_SENT, insertAt);
await reloadCurrentChat();
await eventSource.emit(event_types.USER_MESSAGE_RENDERED, insertAt);
} else {
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) {
// allow reporting of the background name if called without args
// for use in ST Scripts via pipe
return background_settings.name;
}
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();
}
}
/**
* Sets a model for the current API.
* @param {object} _ Unused
* @param {string} model New model name
* @returns {string} New or existing model name
*/
function modelCallback(_, model) {
const modelSelectMap = [
{ id: 'model_togetherai_select', api: 'textgenerationwebui', type: textgen_types.TOGETHERAI },
{ id: 'openrouter_model', api: 'textgenerationwebui', type: textgen_types.OPENROUTER },
{ id: 'model_infermaticai_select', api: 'textgenerationwebui', type: textgen_types.INFERMATICAI },
{ id: 'model_dreamgen_select', api: 'textgenerationwebui', type: textgen_types.DREAMGEN },
{ id: 'mancer_model', api: 'textgenerationwebui', type: textgen_types.MANCER },
{ id: 'aphrodite_model', api: 'textgenerationwebui', type: textgen_types.APHRODITE },
{ id: 'ollama_model', api: 'textgenerationwebui', type: textgen_types.OLLAMA },
{ id: 'model_openai_select', api: 'openai', type: chat_completion_sources.OPENAI },
{ id: 'model_claude_select', api: 'openai', type: chat_completion_sources.CLAUDE },
{ id: 'model_windowai_select', api: 'openai', type: chat_completion_sources.WINDOWAI },
{ id: 'model_openrouter_select', api: 'openai', type: chat_completion_sources.OPENROUTER },
{ id: 'model_ai21_select', api: 'openai', type: chat_completion_sources.AI21 },
{ id: 'model_google_select', api: 'openai', type: chat_completion_sources.MAKERSUITE },
{ id: 'model_mistralai_select', api: 'openai', type: chat_completion_sources.MISTRALAI },
{ id: 'model_custom_select', api: 'openai', type: chat_completion_sources.CUSTOM },
{ id: 'model_cohere_select', api: 'openai', type: chat_completion_sources.COHERE },
{ id: 'model_perplexity_select', api: 'openai', type: chat_completion_sources.PERPLEXITY },
{ id: 'model_novel_select', api: 'novel', type: null },
{ id: 'horde_model', api: 'koboldhorde', type: null },
];
function getSubType() {
switch (main_api) {
case 'textgenerationwebui':
return textgenerationwebui_settings.type;
case 'openai':
return oai_settings.chat_completion_source;
default:
return null;
}
}
const apiSubType = getSubType();
const modelSelectItem = modelSelectMap.find(x => x.api == main_api && x.type == apiSubType)?.id;
if (!modelSelectItem) {
toastr.info('Setting a model for your API is not supported or not implemented yet.');
return '';
}
const modelSelectControl = document.getElementById(modelSelectItem);
if (!(modelSelectControl instanceof HTMLSelectElement)) {
toastr.error(`Model select control not found: ${main_api}[${apiSubType}]`);
return '';
}
const options = Array.from(modelSelectControl.options);
if (!options.length) {
toastr.warning('No model options found. Check your API settings.');
return '';
}
model = String(model || '').trim();
if (!model) {
return modelSelectControl.value;
}
console.log('Set model to ' + model);
let newSelectedOption = null;
const fuse = new Fuse(options, { keys: ['text', 'value'] });
const fuzzySearchResult = fuse.search(model);
const exactValueMatch = options.find(x => x.value.trim().toLowerCase() === model.trim().toLowerCase());
const exactTextMatch = options.find(x => x.text.trim().toLowerCase() === model.trim().toLowerCase());
if (exactValueMatch) {
newSelectedOption = exactValueMatch;
} else if (exactTextMatch) {
newSelectedOption = exactTextMatch;
} else if (fuzzySearchResult.length) {
newSelectedOption = fuzzySearchResult[0].item;
}
if (newSelectedOption) {
modelSelectControl.value = newSelectedOption.value;
$(modelSelectControl).trigger('change');
toastr.success(`Model set to "${newSelectedOption.text}"`);
return newSelectedOption.value;
} else {
toastr.warning(`No model found with name "${model}"`);
return '';
}
}
/**
*
* @param {string} text Slash command txt
* @param {object} [options]
* @param {boolean} [options.handleParserErrors]
* @param {SlashCommandScope} [options.scope]
* @param {boolean} [options.handleExecutionErrors]
* @param {PARSER_FLAG[]} [options.parserFlags]
* @param {AbortController} [options.abortController]
* @param {(done:number, total:number)=>void} [options.onProgress]
* @returns {Promise${ex.hint}`; const clickHint = '
Click to see details
'; toastr.error( `${toast}${clickHint}`, 'SlashCommandParserError', { escapeHtml:false, timeOut: 10000, onclick:()=>callPopup(toast, 'text') }, ); const result = new SlashCommandClosureResult(); result.interrupt = true; return result; } else { throw e; } } try { const result = await closure.execute(); if (result.isAborted) { toastr.warning(result.abortReason, 'Command execution aborted'); } } catch (e) { if (handleExecutionErrors) { toastr.error(e.message); const result = new SlashCommandClosureResult(); result.interrupt = true; return result; } else { throw e; } } } /** * * @param {HTMLTextAreaElement} textarea The textarea to receive autocomplete * @param {Boolean} isFloating Whether to show the auto complete as a floating window (e.g., large QR editor) */ export async function setSlashCommandAutoComplete(textarea, isFloating = false) { const parser = new SlashCommandParser(); const ac = new SlashCommandAutoComplete( textarea, () => ac.text[0] == '/', async(text, index) => await parser.getNameAt(text, index), isFloating, ); //TODO remove macro demo { const macroList = [ ['pipe', 'only for slash command batching. Replaced with the returned result of the previous command.'], ['newline', 'just inserts a newline.'], ['trim', 'trims newlines surrounding this macro.'], ['noop', 'no operation, just an empty string.'], ['original', 'global prompts defined in API settings. Only valid in Advanced Definitions prompt overrides.'], ['input', 'the user input'], ['charPrompt', 'the Character\'s Main Prompt override'], ['charJailbreak', 'the Character\'s Jailbreak Prompt override'], ['description', 'the Character\'s Description'], ['personality', 'the Character\'s Personality'], ['scenario', 'the Character\'s Scenario'], ['persona', 'your current Persona Description'], ['mesExamples', 'the Character\'s Dialogue Examples'], ['mesExamplesRaw', 'unformatted Dialogue Examples (only for Story String)'], ['user', 'your current Persona username'], ['char', 'the Character\'s name'], ['group', 'a comma-separated list of group member names or the character name in solo chats. Alias: {{charIfNotGroup}}'], ['model', 'a text generation model name for the currently selected API. Can be inaccurate!'], ['lastMessage', 'the text of the latest chat message.'], ['lastMessageId', 'index # of the latest chat message. Useful for slash command batching.'], ['firstIncludedMessageId', 'the ID of the first message included in the context. Requires generation to be ran at least once in the current session.'], ['currentSwipeId', 'the 1-based ID of the current swipe in the last chat message. Empty string if the last message is user or prompt-hidden.'], ['lastSwipeId', 'the number of swipes in the last chat message. Empty string if the last message is user or prompt-hidden.'], ['// (note)', 'you can leave a note here, and the macro will be replaced with blank content. Not visible for the AI.'], ['time', 'the current time'], ['date', 'the current date'], ['weekday', 'the current weekday'], ['isotime', 'the current ISO time (24-hour clock)'], ['isodate', 'the current ISO date (YYYY-MM-DD)'], ['datetimeformat …', 'the current date/time in the specified format, e. g. for German date/time: {{datetimeformat DD.MM.YYYY HH:mm}}'], ['datetimeformat DD.MM.YYYY HH', 'the current date/time in the specified format, e. g. for German date/time: '], ['time_UTC±#', 'the current time in the specified UTC time zone offset, e.g. UTC-4 or UTC+2'], ['idle_duration', 'the time since the last user message was sent'], ['bias "text here"', 'sets a behavioral bias for the AI until the next user input. Quotes around the text are important.'], ['roll', 'rolls a dice. (ex: >{{roll:1d6}} will roll a 6-sided dice and return a number between 1 and 6)'], ['roll', 'rolls a dice. (ex: will roll a 6-sided dice and return a number between 1 and 6)'], ['random', 'returns a random item from the list. (ex: {{random:1,2,3,4}} will return 1 of the 4 numbers at random. Works with text lists too.'], ['random', 'returns a random item from the list. (ex: will return 1 of the 4 numbers at random. Works with text lists too.'], ['random', 'alternative syntax for random that allows to use commas in the list items.'], ['pick', 'picks a random item from the list. Works the same as {{random}}, with the same possible syntax options, but the pick will stay consistent for this chat once picked and won\'t be re-rolled on consecutive messages and prompt processing.'], ['banned "text here"', 'dynamically add text in the quotes to banned words sequences, if Text Generation WebUI backend used. Do nothing for others backends. Can be used anywhere (Character description, WI, AN, etc.) Quotes around the text are important.'], ['maxPrompt', 'max allowed prompt length in tokens = (context size - response length)'], ['exampleSeparator', 'context template example dialogues separator'], ['chatStart', 'context template chat start line'], ['systemPrompt', 'main system prompt (either character prompt override if chosen, or instructSystemPrompt)'], ['instructSystemPrompt', 'instruct system prompt'], ['instructSystemPromptPrefix', 'instruct system prompt prefix sequence'], ['instructSystemPromptSuffix', 'instruct system prompt suffix sequence'], ['instructUserPrefix', 'instruct user prefix sequence'], ['instructUserSuffix', 'instruct user suffix sequence'], ['instructAssistantPrefix', 'instruct assistant prefix sequence'], ['instructAssistantSuffix', 'instruct assistant suffix sequence'], ['instructFirstAssistantPrefix', 'instruct assistant first output sequence'], ['instructLastAssistantPrefix', 'instruct assistant last output sequence'], ['instructSystemPrefix', 'instruct system message prefix sequence'], ['instructSystemSuffix', 'instruct system message suffix sequence'], ['instructSystemInstructionPrefix', 'instruct system instruction prefix'], ['instructUserFiller', 'instruct first user message filler'], ['instructStop', 'instruct stop sequence'], ['getvar', 'replaced with the value of the local variable "name"'], ['setvar', 'replaced with empty string, sets the local variable "name" to "value"'], ['addvar', 'replaced with empty strings, adds a numeric value of "increment" to the local variable "name"'], ['incvar', 'replaced with the result of the increment of value of the variable "name" by 1'], ['decvar', 'replaced with the result of the decrement of value of the variable "name" by 1'], ['getglobalvar', 'replaced with the value of the global variable "name"'], ['setglobalvar', 'replaced with empty string, sets the global variable "name" to "value"'], ['addglobalvar', 'replaced with empty string, adds a numeric value of "increment" to the global variable "name"'], ['incglobalvar', 'replaced with the result of the increment of value of the global variable "name" by 1'], ['decglobalvar', 'replaced with the result of the decrement of value of the global variable "name" by 1'], ['var', 'replaced with the value of the scoped variable "name"'], ['var', 'replaced with the value of item at index (for arrays / lists or objects / dictionaries) of the scoped variable "name"'], ]; class MacroOption extends SlashCommandAutoCompleteOption { item; constructor(item) { super(item[0], item[0]); this.item = item; } renderItem() { let li; li = this.makeItem(this.name, '{}', true, [], [], null, this.item[1]); li.setAttribute('data-name', this.name); return li; } renderDetails() { const frag = document.createDocumentFragment(); const specs = document.createElement('div'); { specs.classList.add('specs'); const name = document.createElement('div'); { name.classList.add('name'); name.classList.add('monospace'); name.textContent = this.value.toString(); specs.append(name); } frag.append(specs); } const help = document.createElement('span'); { help.classList.add('help'); help.textContent = this.item[1]; frag.append(help); } return frag; } } const options = macroList.map(m=>new MacroOption(m)); let text; const stack = []; let macroIndex = []; const mac = new SlashCommandAutoComplete( textarea, ()=> mac.text[0] != '/', async(newText, index) => { if (text != newText) { while (stack.pop()); while (macroIndex.pop()); text = newText; let remaining = text; let idx = remaining.search(/{{|}}/); while (idx > -1) { const symbol = remaining.slice(idx, idx + 2); remaining = remaining.slice(idx + 2); if (symbol == '{{') { const item = { name: /\w+/.exec(remaining)?.[0] ?? '', start: idx + 2, end: null, }; macroIndex.push(item); stack.push(item); } else { const item = stack.pop(); if (item) { item.end = idx; } } idx = remaining.search(/{{|}}/); } } const executor = macroIndex.filter(it=>it.start <= index && (it.end >= index || it.end == null)) .slice(-1)[0] ?? null ; if (executor) { const result = new SlashCommandParserNameResult( NAME_RESULT_TYPE.CLOSURE, executor.name, executor.start, options, false, ()=>`No matching macros for "{{${result.name}}}"`, ()=>'No macros found!', ); return result; } return null; }, isFloating, ); } } /**@type {HTMLTextAreaElement} */ const sendTextarea = document.querySelector('#send_textarea'); setSlashCommandAutoComplete(sendTextarea); sendTextarea.addEventListener('input', () => { if (sendTextarea.value[0] == '/') { sendTextarea.style.fontFamily = 'monospace'; } else { sendTextarea.style.fontFamily = null; } });