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, name2, reloadCurrentChat, removeMacros, renameCharacter, saveChatConditional, sendMessageAsUser, sendSystemMessage, setActiveCharacter, setActiveGroup, setCharacterId, setCharacterName, setExtensionPrompt, setUserName, substituteParams, system_avatar, system_message_types, this_chid, } from '../script.js'; import { 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, setupChatCompletionPromptManager } from './openai.js'; import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockState, togglePersonaLock, user_avatar } 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 { debounce, delay, isFalseBoolean, isTrueBoolean, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js'; import { registerVariableCommands, resolveVariable } from './variables.js'; import { background_settings } from './backgrounds.js'; import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; import { AutoComplete } from './autocomplete/AutoComplete.js'; import { SlashCommand } from './slash-commands/SlashCommand.js'; import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js'; import { SlashCommandNamedArgumentAssignment } from './slash-commands/SlashCommandNamedArgumentAssignment.js'; import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js'; import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; export { executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand, }; export const parser = new SlashCommandParser(); /** * @deprecated Use SlashCommandParser.addCommandObject() instead */ const registerSlashCommand = SlashCommandParser.addCommand.bind(SlashCommandParser); const getSlashCommandsHelp = parser.getHelpString.bind(parser); export function initDefaultSlashCommands() { SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: '?', callback: helpCommandCallback, aliases: ['help'], unnamedArgumentList: [SlashCommandArgument.fromProps({ description: 'help topic', typeList: [ARGUMENT_TYPE.STRING], enumList: [ new SlashCommandEnumValue('slash', 'slash commands (STscript)', enumTypes.command, '/'), new SlashCommandEnumValue('macros', '{{macros}} (text replacement)', enumTypes.macro, enumIcons.macro), new SlashCommandEnumValue('format', 'chat/text formatting', enumTypes.name, '★'), new SlashCommandEnumValue('hotkeys', 'keyboard shortcuts', enumTypes.enum, '⏎'), ], })], helpString: 'Get help on macros, chat formatting and commands.', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'persona', callback: setNameCallback, namedArgumentList: [ new SlashCommandNamedArgument( 'mode', 'The mode for persona selection. ("lookup" = search for existing persona, "temp" = create a temporary name, set a temporary name, "all" = allow both in the same command)', [ARGUMENT_TYPE.STRING], false, false, 'all', ['lookup', 'temp', 'all'], ), ], unnamedArgumentList: [ SlashCommandArgument.fromProps({ description: 'persona name', typeList: [ARGUMENT_TYPE.STRING], isRequired: true, enumProvider: commonEnumProviders.personas, }), ], helpString: 'Selects the given persona with its name and avatar (by name or avatar url). If no matching persona exists, applies a temporary name.', aliases: ['name'], })); 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: lockPersonaCallback, aliases: ['bind'], helpString: 'Locks/unlocks a persona (name and avatar) to the current chat', unnamedArgumentList: [ SlashCommandArgument.fromProps({ description: 'state', typeList: [ARGUMENT_TYPE.STRING], isRequired: true, defaultValue: 'toggle', enumProvider: commonEnumProviders.boolean('onOffToggle'), }), ], })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'bg', callback: setBackgroundCallback, aliases: ['background'], returns: 'the current background', unnamedArgumentList: [ SlashCommandArgument.fromProps({ description: 'filename', typeList: [ARGUMENT_TYPE.STRING], isRequired: true, enumProvider: () => [...document.querySelectorAll('.bg_example')] .map(it => new SlashCommandEnumValue(it.getAttribute('bgfile'))) .filter(it => it.value?.length), }), ], 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.
name
is set, it will be displayed as the message sender. Can be an empty for no name.
/send Hello there!
/send compact=true Hi
await=true
named argument is passed, the command will await for the triggered generation before continuing.
/member-add John Doe
/member-remove 2
/member-remove John Doe
/peek Gloria
Shows the character card for the character named "Gloria".
/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}}
.
hidden=off
argument to exclude hidden messages.
role
argument to filter messages by role. Possible values are: system, assistant, user.
/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);
}
/**
* Flushes script injections for the current chat.
* @param {import('./slash-commands/SlashCommand.js').NamedArguments} _ Named arguments
* @param {string} value Unnamed argument
* @returns {string} Empty string
*/
function flushInjectsCallback(_, value) {
if (!chat_metadata.script_injects) {
return '';
}
const idArgument = value;
for (const [id, inject] of Object.entries(chat_metadata.script_injects)) {
if (idArgument && id !== idArgument) {
continue;
}
const prefixedId = `${SCRIPT_PROMPT_KEY}${id}`;
setExtensionPrompt(prefixedId, '', inject.position, inject.depth, inject.scan, inject.role);
delete chat_metadata.script_injects[id];
}
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 {
/** @type {string[]} */
const buttons = JSON.parse(resolveVariable(args?.labels));
if (!Array.isArray(buttons) || !buttons.length) {
console.warn('WARN: Invalid labels provided for /buttons command');
return '';
}
// Map custom buttons to results. Start at 2 because 1 and 0 are reserved for ok and cancel
const resultToButtonMap = new Map(buttons.map((button, index) => [index + 2, button]));
return new Promise(async (resolve) => {
const safeValue = DOMPurify.sanitize(text || '');
/** @type {Popup} */
let popup;
const buttonContainer = document.createElement('div');
buttonContainer.classList.add('flex-container', 'flexFlowColumn', 'wide100p', 'm-t-1');
for (const [result, button] of resultToButtonMap) {
const buttonElement = document.createElement('div');
buttonElement.classList.add('menu_button', 'result-control', 'wide100p');
buttonElement.dataset.result = String(result);
buttonElement.addEventListener('click', () => {
popup?.complete(result);
});
buttonElement.innerText = button;
buttonContainer.appendChild(buttonElement);
}
const popupContainer = document.createElement('div');
popupContainer.innerHTML = safeValue;
popupContainer.appendChild(buttonContainer);
popup = new Popup(popupContainer, POPUP_TYPE.TEXT, '', { okButton: 'Cancel' });
popup.show()
.then((result => resolve(typeof result === 'number' ? resultToButtonMap.get(result) ?? '' : '')))
.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 callGenericPopup(safeValue, POPUP_TYPE.TEXT, '', popupOptions);
await delay(1);
return String(value);
}
function getMessagesCallback(args, value) {
const includeNames = !isFalseBoolean(args?.names);
const includeHidden = isTrueBoolean(args?.hidden);
const role = args?.role;
const range = stringToRange(value, 0, chat.length - 1);
if (!range) {
console.warn(`WARN: Invalid range provided for /messages command: ${value}`);
return '';
}
const filterByRole = (mes) => {
if (!role) {
return true;
}
const isNarrator = mes.extra?.type === system_message_types.NARRATOR;
if (role === 'system') {
return isNarrator && !mes.is_user;
}
if (role === 'assistant') {
return !isNarrator && !mes.is_user;
}
if (role === 'user') {
return !isNarrator && mes.is_user;
}
throw new Error(`Invalid role provided. Expected one of: system, assistant, user. Got: ${role}`);
};
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 (role && !filterByRole(message)) {
console.debug(`/messages: Skipping message with ID ${messageId} due to role filter`);
continue;
}
if (!includeHidden && message.is_system) {
console.debug(`/messages: Skipping hidden message with ID ${messageId}`);
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;
closure.argumentList.forEach(arg => {
if (Object.keys(args).includes(arg.name)) {
const providedArg = new SlashCommandNamedArgumentAssignment();
providedArg.name = arg.name;
providedArg.value = args[arg.name];
closure.providedArgumentList.push(providedArg);
}
});
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');
}
}
/**
*
* @param {import('./slash-commands/SlashCommand.js').NamedArguments} param0
* @param {string} [reason]
*/
function abortCallback({ _abortController, quiet }, reason) {
if (quiet instanceof SlashCommandClosure) throw new Error('argument \'quiet\' cannot be a closure for command /abort');
_abortController.abort((reason ?? '').toString().length == 0 ? '/abort command executed' : reason, !isFalseBoolean(quiet ?? 'true'));
return '';
}
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);
return '';
}
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 callGenericPopup(safeValue, POPUP_TYPE.INPUT, defaultInput, popupOptions);
await delay(1);
return String(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) {
// Note: We don't need to sanitize input, as toastr is set up by default to escape HTML via toastr options
if (value === '') {
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(value, title);
break;
case 'warning':
toastr.warning(value, title);
break;
case 'success':
toastr.success(value, title);
break;
case 'info':
default:
toastr.info(value, 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;
}
if (!Array.isArray(lastMessage.swipe_info)) {
lastMessage.swipe_info = lastMessage.swipes.map(() => ({}));
}
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();
return '';
}
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();
return '';
}
async function askCharacter(args, 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');
toastr.warning('No text provided for /ask command');
return '';
}
let name = '';
let mesText = '';
if (args?.name) {
name = args.name.trim();
mesText = text.trim();
if (!name && !mesText) {
toastr.warning('You must specify a name and text to ask.');
return '';
}
}
mesText = getRegexedString(mesText, regex_placement.SLASH_COMMAND);
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(String(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);
return '';
}
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);
return '';
}
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 '';
}
/**
* Find persona by name.
* @param {string} name Name to search for
* @returns {string} Persona name
*/
function findPersonaByName(name) {
if (!name) {
return null;
}
for (const persona of Object.entries(power_user.personas)) {
if (persona[1].toLowerCase() === name.toLowerCase()) {
return persona[0];
}
}
return null;
}
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(args?.at);
if ('name' in args) {
const name = args.name || '';
const avatar = findPersonaByName(name) || user_avatar;
await sendMessageAsUser(text, bias, insertAt, compact, name, avatar);
}
else {
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);
return '';
}
function syncCallback() {
$('#sync_name_button').trigger('click');
return '';
}
async function lockPersonaCallback(_args, value) {
if (['toggle', 't', ''].includes(value.trim().toLowerCase())) {
await togglePersonaLock();
return '';
}
if (isTrueBoolean(value)) {
await setPersonaLockState(true);
return '';
}
if (isFalseBoolean(value)) {
await setPersonaLockState(false);
return '';
}
return '';
}
function setStoryModeCallback() {
$('#chat_display').val(chat_styles.DOCUMENT).trigger('change');
return '';
}
function setBubbleModeCallback() {
$('#chat_display').val(chat_styles.BUBBLES).trigger('change');
return '';
}
function setFlatModeCallback() {
$('#chat_display').val(chat_styles.DEFAULT).trigger('change');
return '';
}
/**
* Sets a persona name and optionally an avatar.
* @param {{mode: 'lookup' | 'temp' | 'all'}} namedArgs Named arguments
* @param {string} name Name to set
* @returns {string}
*/
function setNameCallback({ mode = 'all' }, name) {
if (!name) {
toastr.warning('You must specify a name to change to');
return '';
}
if (!['lookup', 'temp', 'all'].includes(mode)) {
toastr.warning('Mode must be one of "lookup", "temp" or "all"');
return '';
}
name = name.trim();
// If the name matches a persona avatar, or a name, auto-select it
if (['lookup', 'all'].includes(mode)) {
let persona = Object.entries(power_user.personas).find(([avatar, _]) => avatar === name)?.[1];
if (!persona) persona = Object.entries(power_user.personas).find(([_, personaName]) => personaName.toLowerCase() === name.toLowerCase())?.[1];
if (persona) {
autoSelectPersona(persona);
retriggerFirstMessageOnEmptyChat();
return '';
} else if (mode === 'lookup') {
toastr.warning(`Persona ${name} not found`);
return '';
}
}
if (['temp', 'all'].includes(mode)) {
// Otherwise, set just the name
setUserName(name); //this prevented quickReply usage
retriggerFirstMessageOnEmptyChat();
}
return '';
}
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();
return '';
}
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 namelessWarningKey = 'sendAsNamelessWarningShown';
if (localStorage.getItem(namelessWarningKey) !== 'true') {
toastr.warning('To avoid confusion, please use /sendas name="Character Name"', 'Name defaulted to {{char}}', { timeOut: 10000 });
localStorage.setItem(namelessWarningKey, 'true');
}
name = name2;
}
// 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,
api: 'manual',
model: 'slash command',
},
};
message.swipe_id = 0;
message.swipes = [message.mes];
message.swipes_info = [{
send_date: message.send_date,
gen_started: null,
gen_finished: null,
extra: {
bias: message.extra.bias,
gen_id: message.extra.gen_id,
isSmallSys: compact,
api: 'manual',
model: 'slash command',
},
}];
const insertAt = Number(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();
}
return '';
}
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,
api: 'manual',
model: 'slash command',
},
};
const insertAt = Number(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();
}
return '';
}
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(),
api: 'manual',
model: 'slash command',
},
};
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,
api: 'manual',
model: 'slash command',
},
};
const insertAt = Number(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();
}
return '';
}
/**
* 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;
}
return '';
}
$(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();
}
return '';
}
/**
* Retrieves the available model options based on the currently selected main API and its subtype
*
* @returns {{control: HTMLSelectElement, options: HTMLOptionElement[]}?} An array of objects representing the available model options, or null if not supported
*/
function getModelOptions() {
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: 'vllm_model', api: 'textgenerationwebui', type: textgen_types.VLLM },
{ 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_groq_select', api: 'openai', type: chat_completion_sources.GROQ },
{ id: 'model_01ai_select', api: 'openai', type: chat_completion_sources.ZEROONEAI },
{ 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 null;
}
const modelSelectControl = document.getElementById(modelSelectItem);
if (!(modelSelectControl instanceof HTMLSelectElement)) {
toastr.error(`Model select control not found: ${main_api}[${apiSubType}]`);
return null;
}
const options = Array.from(modelSelectControl.options);
return { control: modelSelectControl, options };
}
/**
* 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 { control: modelSelectControl, options } = getModelOptions();
// If no model was found, the reason was already logged, we just return here
if (options === null) {
return '';
}
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 '';
}
}
/**
* Sets state of prompt entries (toggles) either via identifier/uuid or name.
* @param {object} args Object containing arguments
* @param {string} args.identifier Select prompt entry using an identifier (uuid)
* @param {string} args.name Select prompt entry using name
* @param {string} targetState The targeted state of the entry/entries
* @returns {String} empty string
*/
function setPromptEntryCallback(args, targetState) {
// needs promptManager to manipulate prompt entries
const promptManager = setupChatCompletionPromptManager(oai_settings);
const prompts = promptManager.serviceSettings.prompts;
function parseArgs(arg) {
const list = [];
try {
const parsedArg = JSON.parse(arg);
list.push(...Array.isArray(parsedArg) ? parsedArg : [arg]);
} catch {
list.push(arg);
}
return list;
}
let identifiersList = parseArgs(args.identifier);
let nameList = parseArgs(args.name);
// Check if identifiers exists in prompt, else remove from list
if (identifiersList.length !== 0) {
identifiersList = identifiersList.filter(identifier => prompts.some(prompt => prompt.identifier === identifier));
}
if (nameList.length !== 0) {
nameList.forEach(name => {
// one name could potentially have multiple entries, find all identifiers that match given name
let identifiers = [];
prompts.forEach(entry => {
if (entry.name === name) {
identifiers.push(entry.identifier);
}
});
identifiersList = identifiersList.concat(identifiers);
});
}
// Remove duplicates to allow consistent 'toggle'
identifiersList = [...new Set(identifiersList)];
if (identifiersList.length === 0) return '';
// logic adapted from PromptManager.js, handleToggle
const getPromptOrderEntryState = (promptOrderEntry) => {
if (['toggle', 't', ''].includes(targetState.trim().toLowerCase())) {
return !promptOrderEntry.enabled;
}
if (isTrueBoolean(targetState)) {
return true;
}
if (isFalseBoolean(targetState)) {
return false;
}
return promptOrderEntry.enabled;
};
identifiersList.forEach(promptID => {
const promptOrderEntry = promptManager.getPromptOrderEntry(promptManager.activeCharacter, promptID);
const counts = promptManager.tokenHandler.getCounts();
counts[promptID] = null;
promptOrderEntry.enabled = getPromptOrderEntryState(promptOrderEntry);
});
// no need to render for each identifier
promptManager.render();
promptManager.saveServiceSettings();
return '';
}
export let isExecutingCommandsFromChatInput = false;
export let commandsFromChatInputAbortController;
/**
* Show command execution pause/stop buttons next to chat input.
*/
export function activateScriptButtons() {
document.querySelector('#form_sheld').classList.add('isExecutingCommandsFromChatInput');
}
/**
* Hide command execution pause/stop buttons next to chat input.
*/
export function deactivateScriptButtons() {
document.querySelector('#form_sheld').classList.remove('isExecutingCommandsFromChatInput');
}
/**
* Toggle pause/continue command execution. Only for commands executed via chat input.
*/
export function pauseScriptExecution() {
if (commandsFromChatInputAbortController) {
if (commandsFromChatInputAbortController.signal.paused) {
commandsFromChatInputAbortController.continue('Clicked pause button');
document.querySelector('#form_sheld').classList.remove('script_paused');
} else {
commandsFromChatInputAbortController.pause('Clicked pause button');
document.querySelector('#form_sheld').classList.add('script_paused');
}
}
}
/**
* Stop command execution. Only for commands executed via chat input.
*/
export function stopScriptExecution() {
commandsFromChatInputAbortController?.abort('Clicked stop button');
}
/**
* Clear up command execution progress bar above chat input.
* @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(); return result; } else { throw e; } } try { const result = await closure.execute(); if (result.isAborted && !result.isQuietlyAborted) { toastr.warning(result.abortReason, 'Command execution aborted'); closure.abortController.signal.isQuiet = true; } return result; } catch (e) { if (options.handleExecutionErrors) { toastr.error(e.message); const result = new SlashCommandClosureResult(); result.isError = true; result.errorMessage = e.message; return result; } else { throw e; } } } /** * Executes slash commands in the provided text * @deprecated Use executeSlashCommandWithOptions instead * @param {string} text Slash command text * @param {boolean} handleParserErrors Whether to handle parser errors (show toast on error) or throw * @param {SlashCommandScope} scope The scope to be used when executing the commands. * @param {boolean} handleExecutionErrors Whether to handle execution errors (show toast on error) or throw * @param {{[id:PARSER_FLAG]:boolean}} parserFlags Parser flags to apply * @param {SlashCommandAbortController} abortController Controller used to abort or pause command execution * @param {(done:number, total:number)=>void} onProgress Callback to handle progress events * @returns {Promise