mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-02-22 23:18:27 +01:00
4526 lines
172 KiB
JavaScript
4526 lines
172 KiB
JavaScript
import { Fuse, DOMPurify } from '../lib.js';
|
||
|
||
import {
|
||
Generate,
|
||
UNIQUE_APIS,
|
||
activateSendButtons,
|
||
addOneMessage,
|
||
api_server,
|
||
callPopup,
|
||
characters,
|
||
chat,
|
||
chat_metadata,
|
||
comment_avatar,
|
||
deactivateSendButtons,
|
||
default_avatar,
|
||
deleteSwipe,
|
||
eventSource,
|
||
event_types,
|
||
extension_prompt_roles,
|
||
extension_prompt_types,
|
||
extractMessageBias,
|
||
generateQuietPrompt,
|
||
generateRaw,
|
||
getThumbnailUrl,
|
||
is_send_press,
|
||
main_api,
|
||
name1,
|
||
name2,
|
||
neutralCharacterName,
|
||
reloadCurrentChat,
|
||
removeMacros,
|
||
renameCharacter,
|
||
saveChatConditional,
|
||
sendMessageAsUser,
|
||
sendSystemMessage,
|
||
setActiveCharacter,
|
||
setActiveGroup,
|
||
setCharacterId,
|
||
setCharacterName,
|
||
setExtensionPrompt,
|
||
setUserName,
|
||
stopGeneration,
|
||
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, promptManager } 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 { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
|
||
import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';
|
||
import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, showFontAwesomePicker, 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';
|
||
import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
|
||
import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
|
||
import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.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);
|
||
|
||
/**
|
||
* Converts a SlashCommandClosure to a filter function that returns a boolean.
|
||
* @param {SlashCommandClosure} closure
|
||
* @returns {() => Promise<boolean>}
|
||
*/
|
||
function closureToFilter(closure) {
|
||
return async () => {
|
||
try {
|
||
const localClosure = closure.getCopy();
|
||
localClosure.onProgress = () => { };
|
||
const result = await localClosure.execute();
|
||
return isTrueBoolean(result.pipe);
|
||
} catch (e) {
|
||
console.error('Error executing filter closure', e);
|
||
return false;
|
||
}
|
||
};
|
||
}
|
||
|
||
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,
|
||
aliases: ['name'],
|
||
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.',
|
||
}));
|
||
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: 'background filename',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
enumProvider: () => [...document.querySelectorAll('.bg_example')]
|
||
.map(it => new SlashCommandEnumValue(it.getAttribute('bgfile')))
|
||
.filter(it => it.value?.length),
|
||
}),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Sets a background according to the provided filename. Partial names allowed.
|
||
</div>
|
||
<div>
|
||
If no background is provided, this will return the currently selected background.
|
||
</div>
|
||
<div>
|
||
<strong>Example:</strong>
|
||
<ul>
|
||
<li>
|
||
<pre><code>/bg beach.jpg</code></pre>
|
||
</li>
|
||
<li>
|
||
<pre><code>/bg</code></pre>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'char-find',
|
||
aliases: ['findchar'],
|
||
callback: (args, name) => {
|
||
if (typeof name !== 'string') throw new Error('name must be a string');
|
||
if (args.preferCurrent instanceof SlashCommandClosure || Array.isArray(args.preferCurrent)) throw new Error('preferCurrent cannot be a closure or array');
|
||
if (args.quiet instanceof SlashCommandClosure || Array.isArray(args.quiet)) throw new Error('quiet cannot be a closure or array');
|
||
|
||
const char = findChar({ name: name, filteredByTags: validateArrayArgString(args.tag, 'tag'), preferCurrentChar: !isFalseBoolean(args.preferCurrent), quiet: isTrueBoolean(args.quiet) });
|
||
return char?.avatar ?? '';
|
||
},
|
||
returns: 'the avatar key (unique identifier) of the character',
|
||
namedArgumentList: [
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'tag',
|
||
description: 'Supply one or more tags to filter down to the correct character for the provided name, if multiple characters have the same name.',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
enumProvider: commonEnumProviders.tags('assigned'),
|
||
acceptsMultiple: true,
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'preferCurrent',
|
||
description: 'Prefer current character or characters in a group, if multiple characters match',
|
||
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||
defaultValue: 'true',
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'quiet',
|
||
description: 'Do not show warning if multiple charactrers are found',
|
||
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||
defaultValue: 'false',
|
||
enumProvider: commonEnumProviders.boolean('trueFalse'),
|
||
}),
|
||
],
|
||
unnamedArgumentList: [
|
||
SlashCommandArgument.fromProps({
|
||
description: 'Character name - or unique character identifier (avatar key)',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
enumProvider: commonEnumProviders.characters('character'),
|
||
forceEnum: false,
|
||
}),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Searches for a character and returns its avatar key.
|
||
</div>
|
||
<div>
|
||
This can be used to choose the correct character for something like <code>/sendas</code> or other commands in need of a character name
|
||
if you have multiple characters with the same name.
|
||
</div>
|
||
<div>
|
||
<strong>Example:</strong>
|
||
<ul>
|
||
<li>
|
||
<pre><code>/char-find name="Chloe"</code></pre>
|
||
Returns the avatar key for "Chloe".
|
||
</li>
|
||
<li>
|
||
<pre><code>/search name="Chloe" tag="friend"</code></pre>
|
||
Returns the avatar key for the character "Chloe" that is tagged with "friend".
|
||
This is useful if you for example have multiple characters named "Chloe", and the others are "foe", "goddess", or anything else,
|
||
so you can actually select the character you are looking for.
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'sendas',
|
||
callback: sendMessageAs,
|
||
returns: 'Optionally the text of the sent message, if specified in the "return" argument',
|
||
namedArgumentList: [
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'name',
|
||
description: 'Character name - or unique character identifier (avatar key)',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
isRequired: true,
|
||
enumProvider: commonEnumProviders.characters('character'),
|
||
forceEnum: false,
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'avatar',
|
||
description: 'Character avatar override (Can be either avatar key or just the character name to pull the avatar from)',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
enumProvider: commonEnumProviders.characters('character'),
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'compact',
|
||
description: 'Use compact layout',
|
||
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||
defaultValue: 'false',
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'at',
|
||
description: 'position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values (including -0) are accepted and will work similarly to how \'depth\' usually works. For example, -1 will insert the message right before the last message in chat.',
|
||
typeList: [ARGUMENT_TYPE.NUMBER],
|
||
enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'return',
|
||
description: 'The way how you want the return value to be provided',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
defaultValue: 'none',
|
||
enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
|
||
forceEnum: true,
|
||
}),
|
||
],
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'text', [ARGUMENT_TYPE.STRING], true,
|
||
),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Sends a message as a specific character. Uses the character avatar if it exists in the characters list.
|
||
</div>
|
||
<div>
|
||
<strong>Example:</strong>
|
||
<ul>
|
||
<li>
|
||
<pre><code>/sendas name="Chloe" Hello, guys!</code></pre>
|
||
will send "Hello, guys!" from "Chloe".
|
||
</li>
|
||
<li>
|
||
<pre><code>/sendas name="Chloe" avatar="BigBadBoss" Hehehe, I am the big bad evil, fear me.</code></pre>
|
||
will send a message as the character "Chloe", but utilizing the avatar from a character named "BigBadBoss".
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
<div>
|
||
If "compact" is set to true, the message is sent using a compact layout.
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'sys',
|
||
callback: sendNarratorMessage,
|
||
aliases: ['nar'],
|
||
returns: 'Optionally the text of the sent message, if specified in the "return" argument',
|
||
namedArgumentList: [
|
||
new SlashCommandNamedArgument(
|
||
'compact',
|
||
'compact layout',
|
||
[ARGUMENT_TYPE.BOOLEAN],
|
||
false,
|
||
false,
|
||
'false',
|
||
),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'at',
|
||
description: 'position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values (including -0) are accepted and will work similarly to how \'depth\' usually works. For example, -1 will insert the message right before the last message in chat.',
|
||
typeList: [ARGUMENT_TYPE.NUMBER],
|
||
enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'return',
|
||
description: 'The way how you want the return value to be provided',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
defaultValue: 'none',
|
||
enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
|
||
forceEnum: true,
|
||
}),
|
||
],
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'text', [ARGUMENT_TYPE.STRING], true,
|
||
),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Sends a message as a system narrator.
|
||
</div>
|
||
<div>
|
||
If <code>compact</code> is set to <code>true</code>, the message is sent using a compact layout.
|
||
</div>
|
||
<div>
|
||
<strong>Example:</strong>
|
||
<ul>
|
||
<li>
|
||
<pre><code>/sys The sun sets in the west.</code></pre>
|
||
</li>
|
||
<li>
|
||
<pre><code>/sys compact=true A brief note.</code></pre>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'sysname',
|
||
callback: setNarratorName,
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'name', [ARGUMENT_TYPE.STRING], false,
|
||
),
|
||
],
|
||
helpString: 'Sets a name for future system narrator messages in this chat (display only). Default: System. Leave empty to reset.',
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'comment',
|
||
callback: sendCommentMessage,
|
||
returns: 'Optionally the text of the sent message, if specified in the "return" argument',
|
||
namedArgumentList: [
|
||
new SlashCommandNamedArgument(
|
||
'compact',
|
||
'Whether to use a compact layout',
|
||
[ARGUMENT_TYPE.BOOLEAN],
|
||
false,
|
||
false,
|
||
'false',
|
||
),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'at',
|
||
description: 'position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values (including -0) are accepted and will work similarly to how \'depth\' usually works. For example, -1 will insert the message right before the last message in chat.',
|
||
typeList: [ARGUMENT_TYPE.NUMBER],
|
||
enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'return',
|
||
description: 'The way how you want the return value to be provided',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
defaultValue: 'none',
|
||
enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
|
||
forceEnum: true,
|
||
}),
|
||
],
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'text',
|
||
[ARGUMENT_TYPE.STRING],
|
||
true,
|
||
),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Adds a note/comment message not part of the chat.
|
||
</div>
|
||
<div>
|
||
If <code>compact</code> is set to <code>true</code>, the message is sent using a compact layout.
|
||
</div>
|
||
<div>
|
||
<strong>Example:</strong>
|
||
<ul>
|
||
<li>
|
||
<pre><code>/comment This is a comment</code></pre>
|
||
</li>
|
||
<li>
|
||
<pre><code>/comment compact=true This is a compact comment</code></pre>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'single',
|
||
callback: setStoryModeCallback,
|
||
aliases: ['story'],
|
||
helpString: 'Sets the message style to single document mode without names or avatars visible.',
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'bubble',
|
||
callback: setBubbleModeCallback,
|
||
aliases: ['bubbles'],
|
||
helpString: 'Sets the message style to bubble chat mode.',
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'flat',
|
||
callback: setFlatModeCallback,
|
||
aliases: ['default'],
|
||
helpString: 'Sets the message style to flat chat mode.',
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'continue',
|
||
callback: continueChatCallback,
|
||
aliases: ['cont'],
|
||
namedArgumentList: [
|
||
new SlashCommandNamedArgument(
|
||
'await',
|
||
'Whether to await for the continued generation before proceeding',
|
||
[ARGUMENT_TYPE.BOOLEAN],
|
||
false,
|
||
false,
|
||
'false',
|
||
),
|
||
],
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'prompt', [ARGUMENT_TYPE.STRING], false,
|
||
),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Continues the last message in the chat, with an optional additional prompt.
|
||
</div>
|
||
<div>
|
||
If <code>await=true</code> named argument is passed, the command will await for the continued generation before proceeding.
|
||
</div>
|
||
<div>
|
||
<strong>Example:</strong>
|
||
<ul>
|
||
<li>
|
||
<pre><code>/continue</code></pre>
|
||
Continues the chat with no additional prompt and immediately proceeds to the next command.
|
||
</li>
|
||
<li>
|
||
<pre><code>/continue await=true Let's explore this further...</code></pre>
|
||
Continues the chat with the provided prompt and waits for the generation to finish.
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'go',
|
||
callback: goToCharacterCallback,
|
||
returns: 'The character/group name',
|
||
unnamedArgumentList: [
|
||
SlashCommandArgument.fromProps({
|
||
description: 'Character name - or unique character identifier (avatar key)',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
isRequired: true,
|
||
enumProvider: commonEnumProviders.characters('all'),
|
||
forceEnum: true,
|
||
}),
|
||
],
|
||
helpString: 'Opens up a chat with the character or group by its name',
|
||
aliases: ['char'],
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'rename-char',
|
||
/** @param {{silent: string, chats: string}} options @param {string} name */
|
||
callback: async ({ silent = 'true', chats = null }, name) => {
|
||
const renamed = await renameCharacter(name, { silent: isTrueBoolean(silent), renameChats: chats !== null ? isTrueBoolean(chats) : null });
|
||
return String(renamed);
|
||
},
|
||
returns: 'true/false - Whether the rename was successful',
|
||
namedArgumentList: [
|
||
new SlashCommandNamedArgument(
|
||
'silent', 'Hide any blocking popups. (if false, the name is optional. If not supplied, a popup asking for it will appear)', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true',
|
||
),
|
||
new SlashCommandNamedArgument(
|
||
'chats', 'Rename char in all previous chats', [ARGUMENT_TYPE.BOOLEAN], false, false, '<null>',
|
||
),
|
||
],
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'new char name', [ARGUMENT_TYPE.STRING], true,
|
||
),
|
||
],
|
||
helpString: 'Renames the current character.',
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'sysgen',
|
||
callback: generateSystemMessage,
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'prompt', [ARGUMENT_TYPE.STRING], true,
|
||
),
|
||
],
|
||
helpString: 'Generates a system message using a specified prompt.',
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'ask',
|
||
callback: askCharacter,
|
||
returns: 'Optionally the text of the sent message, if specified in the "return" argument',
|
||
namedArgumentList: [
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'name',
|
||
description: 'Character name - or unique character identifier (avatar key)',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
isRequired: true,
|
||
enumProvider: commonEnumProviders.characters('character'),
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'return',
|
||
description: 'The way how you want the return value to be provided',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
defaultValue: 'pipe',
|
||
enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
|
||
forceEnum: true,
|
||
}),
|
||
],
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'prompt', [ARGUMENT_TYPE.STRING], false, false,
|
||
),
|
||
],
|
||
helpString: 'Asks a specified character card a prompt. Character name must be provided in a named argument.',
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'delname',
|
||
callback: deleteMessagesByNameCallback,
|
||
namedArgumentList: [],
|
||
unnamedArgumentList: [
|
||
SlashCommandArgument.fromProps({
|
||
description: 'Character name - or unique character identifier (avatar key)',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
isRequired: true,
|
||
enumProvider: commonEnumProviders.characters('character'),
|
||
}),
|
||
],
|
||
aliases: ['cancel'],
|
||
helpString: `
|
||
<div>
|
||
Deletes all messages attributed to a specified name.
|
||
</div>
|
||
<div>
|
||
<strong>Example:</strong>
|
||
<ul>
|
||
<li>
|
||
<pre><code>/delname John</code></pre>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'send',
|
||
callback: sendUserMessageCallback,
|
||
returns: 'Optionally the text of the sent message, if specified in the "return" argument',
|
||
namedArgumentList: [
|
||
new SlashCommandNamedArgument(
|
||
'compact',
|
||
'whether to use a compact layout',
|
||
[ARGUMENT_TYPE.BOOLEAN],
|
||
false,
|
||
false,
|
||
'false',
|
||
),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'at',
|
||
description: 'position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values (including -0) are accepted and will work similarly to how \'depth\' usually works. For example, -1 will insert the message right before the last message in chat.',
|
||
typeList: [ARGUMENT_TYPE.NUMBER],
|
||
enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'name',
|
||
description: 'display name',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
defaultValue: '{{user}}',
|
||
enumProvider: commonEnumProviders.personas,
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'return',
|
||
description: 'The way how you want the return value to be provided',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
defaultValue: 'none',
|
||
enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
|
||
forceEnum: true,
|
||
}),
|
||
],
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'text',
|
||
[ARGUMENT_TYPE.STRING],
|
||
true,
|
||
),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Adds a user message to the chat log without triggering a generation.
|
||
</div>
|
||
<div>
|
||
If <code>compact</code> is set to <code>true</code>, the message is sent using a compact layout.
|
||
</div>
|
||
<div>
|
||
If <code>name</code> is set, it will be displayed as the message sender. Can be an empty for no name.
|
||
</div>
|
||
<div>
|
||
<strong>Example:</strong>
|
||
<ul>
|
||
<li>
|
||
<pre><code>/send Hello there!</code></pre>
|
||
</li>
|
||
<li>
|
||
<pre><code>/send compact=true Hi</code></pre>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'trigger',
|
||
callback: triggerGenerationCallback,
|
||
namedArgumentList: [
|
||
new SlashCommandNamedArgument(
|
||
'await',
|
||
'Whether to await for the triggered generation before continuing',
|
||
[ARGUMENT_TYPE.BOOLEAN],
|
||
false,
|
||
false,
|
||
'false',
|
||
),
|
||
],
|
||
unnamedArgumentList: [
|
||
SlashCommandArgument.fromProps({
|
||
description: 'group member index (starts with 0) or name',
|
||
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING],
|
||
isRequired: false,
|
||
enumProvider: commonEnumProviders.groupMembers(),
|
||
}),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Triggers a message generation. If in group, can trigger a message for the specified group member index or name.
|
||
</div>
|
||
<div>
|
||
If <code>await=true</code> named argument is passed, the command will await for the triggered generation before continuing.
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'hide',
|
||
callback: hideMessageCallback,
|
||
unnamedArgumentList: [
|
||
SlashCommandArgument.fromProps({
|
||
description: 'message index (starts with 0) or range',
|
||
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE],
|
||
isRequired: true,
|
||
enumProvider: commonEnumProviders.messages(),
|
||
}),
|
||
],
|
||
helpString: 'Hides a chat message from the prompt.',
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'unhide',
|
||
callback: unhideMessageCallback,
|
||
unnamedArgumentList: [
|
||
SlashCommandArgument.fromProps({
|
||
description: 'message index (starts with 0) or range',
|
||
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE],
|
||
isRequired: true,
|
||
enumProvider: commonEnumProviders.messages(),
|
||
}),
|
||
],
|
||
helpString: 'Unhides a message from the prompt.',
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'member-disable',
|
||
callback: disableGroupMemberCallback,
|
||
aliases: ['disable', 'disablemember', 'memberdisable'],
|
||
unnamedArgumentList: [
|
||
SlashCommandArgument.fromProps({
|
||
description: 'member index (starts with 0) or name',
|
||
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING],
|
||
isRequired: true,
|
||
enumProvider: commonEnumProviders.groupMembers(),
|
||
}),
|
||
],
|
||
helpString: 'Disables a group member from being drafted for replies.',
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'member-enable',
|
||
aliases: ['enable', 'enablemember', 'memberenable'],
|
||
callback: enableGroupMemberCallback,
|
||
unnamedArgumentList: [
|
||
SlashCommandArgument.fromProps({
|
||
description: 'member index (starts with 0) or name',
|
||
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING],
|
||
isRequired: true,
|
||
enumProvider: commonEnumProviders.groupMembers(),
|
||
}),
|
||
],
|
||
helpString: 'Enables a group member to be drafted for replies.',
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'member-add',
|
||
callback: addGroupMemberCallback,
|
||
aliases: ['addmember', 'memberadd'],
|
||
unnamedArgumentList: [
|
||
SlashCommandArgument.fromProps({
|
||
description: 'Character name - or unique character identifier (avatar key)',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
isRequired: true,
|
||
enumProvider: () => selected_group ? commonEnumProviders.characters('character')() : [],
|
||
}),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Adds a new group member to the group chat.
|
||
</div>
|
||
<div>
|
||
<strong>Example:</strong>
|
||
<ul>
|
||
<li>
|
||
<pre><code>/member-add John Doe</code></pre>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'member-remove',
|
||
callback: removeGroupMemberCallback,
|
||
aliases: ['removemember', 'memberremove'],
|
||
unnamedArgumentList: [
|
||
SlashCommandArgument.fromProps({
|
||
description: 'member index (starts with 0) or name',
|
||
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING],
|
||
isRequired: true,
|
||
enumProvider: commonEnumProviders.groupMembers(),
|
||
}),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Removes a group member from the group chat.
|
||
</div>
|
||
<div>
|
||
<strong>Example:</strong>
|
||
<ul>
|
||
<li>
|
||
<pre><code>/member-remove 2</code></pre>
|
||
<pre><code>/member-remove John Doe</code></pre>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'member-up',
|
||
callback: moveGroupMemberUpCallback,
|
||
aliases: ['upmember', 'memberup'],
|
||
unnamedArgumentList: [
|
||
SlashCommandArgument.fromProps({
|
||
description: 'member index (starts with 0) or name',
|
||
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING],
|
||
isRequired: true,
|
||
enumProvider: commonEnumProviders.groupMembers(),
|
||
}),
|
||
],
|
||
helpString: 'Moves a group member up in the group chat list.',
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'member-down',
|
||
callback: moveGroupMemberDownCallback,
|
||
aliases: ['downmember', 'memberdown'],
|
||
unnamedArgumentList: [
|
||
SlashCommandArgument.fromProps({
|
||
description: 'member index (starts with 0) or name',
|
||
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING],
|
||
isRequired: true,
|
||
enumProvider: commonEnumProviders.groupMembers(),
|
||
}),
|
||
],
|
||
helpString: 'Moves a group member down in the group chat list.',
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'peek',
|
||
callback: peekCallback,
|
||
unnamedArgumentList: [
|
||
SlashCommandArgument.fromProps({
|
||
description: 'member index (starts with 0) or name',
|
||
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING],
|
||
isRequired: true,
|
||
enumProvider: commonEnumProviders.groupMembers(),
|
||
}),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Shows a group member character card without switching chats.
|
||
</div>
|
||
<div>
|
||
<strong>Examples:</strong>
|
||
<ul>
|
||
<li>
|
||
<pre><code>/peek Gloria</code></pre>
|
||
Shows the character card for the character named "Gloria".
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'delswipe',
|
||
callback: deleteSwipeCallback,
|
||
returns: 'the new, currently selected swipe id',
|
||
aliases: ['swipedel'],
|
||
unnamedArgumentList: [
|
||
SlashCommandArgument.fromProps({
|
||
description: '1-based swipe id',
|
||
typeList: [ARGUMENT_TYPE.NUMBER],
|
||
isRequired: true,
|
||
enumProvider: () => Array.isArray(chat[chat.length - 1]?.swipes) ?
|
||
chat[chat.length - 1].swipes.map((/** @type {string} */ swipe, /** @type {number} */ i) => new SlashCommandEnumValue(String(i + 1), swipe, enumTypes.enum, enumIcons.message))
|
||
: [],
|
||
}),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Deletes a swipe from the last chat message. If swipe id is not provided, it deletes the current swipe.
|
||
</div>
|
||
<div>
|
||
<strong>Example:</strong>
|
||
<ul>
|
||
<li>
|
||
<pre><code>/delswipe</code></pre>
|
||
Deletes the current swipe.
|
||
</li>
|
||
<li>
|
||
<pre><code>/delswipe 2</code></pre>
|
||
Deletes the second swipe from the last chat message.
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'echo',
|
||
callback: echoCallback,
|
||
returns: 'the text',
|
||
namedArgumentList: [
|
||
new SlashCommandNamedArgument(
|
||
'title', 'title of the toast message', [ARGUMENT_TYPE.STRING], false,
|
||
),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'severity',
|
||
description: 'severity level of the toast message',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
defaultValue: 'info',
|
||
enumProvider: () => [
|
||
new SlashCommandEnumValue('info', 'info', enumTypes.macro, 'ℹ️'),
|
||
new SlashCommandEnumValue('warning', 'warning', enumTypes.enum, '⚠️'),
|
||
new SlashCommandEnumValue('error', 'error', enumTypes.enum, '❗'),
|
||
new SlashCommandEnumValue('success', 'success', enumTypes.enum, '✅'),
|
||
],
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'timeout',
|
||
description: 'time in milliseconds to display the toast message. Set this and \'extendedTimeout\' to 0 to show indefinitely until dismissed.',
|
||
typeList: [ARGUMENT_TYPE.NUMBER],
|
||
defaultValue: `${toastr.options.timeOut}`,
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'extendedTimeout',
|
||
description: 'time in milliseconds to display the toast message. Set this and \'timeout\' to 0 to show indefinitely until dismissed.',
|
||
typeList: [ARGUMENT_TYPE.NUMBER],
|
||
defaultValue: `${toastr.options.extendedTimeOut}`,
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'preventDuplicates',
|
||
description: 'prevent duplicate toasts with the same message from being displayed.',
|
||
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||
defaultValue: 'false',
|
||
enumList: commonEnumProviders.boolean('trueFalse')(),
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'awaitDismissal',
|
||
description: 'wait for the toast to be dismissed before continuing.',
|
||
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||
defaultValue: 'false',
|
||
enumList: commonEnumProviders.boolean('trueFalse')(),
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'cssClass',
|
||
description: 'additional CSS class to add to the toast message (e.g. for custom styling)',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'color',
|
||
description: 'custom CSS color of the toast message. Accepts all valid CSS color values (e.g. \'red\', \'#FF0000\', \'rgb(255, 0, 0)\').<br />>Can be more customizable with the \'cssClass\' argument and custom classes.',
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'escapeHtml',
|
||
description: 'whether to escape HTML in the toast message.',
|
||
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||
defaultValue: 'true',
|
||
enumList: commonEnumProviders.boolean('trueFalse')(),
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'onClick',
|
||
description: 'a closure to call when the toast is clicked. This executed closure receives scope as provided in the script. Careful about possible side effects when manipulating variables and more.',
|
||
typeList: [ARGUMENT_TYPE.CLOSURE],
|
||
}),
|
||
],
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'text', [ARGUMENT_TYPE.STRING], true,
|
||
),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Echoes the provided text to a toast message. Can be used to display informational messages or for pipes debugging.
|
||
</div>
|
||
<div>
|
||
<strong>Example:</strong>
|
||
<ul>
|
||
<li>
|
||
<pre><code>/echo title="My Message" severity=warning This is a warning message</code></pre>
|
||
</li>
|
||
<li>
|
||
<pre><code>/echo color=purple This message is purple</code></pre>
|
||
</li>
|
||
<li>
|
||
<pre><code>/echo onClick={: /echo escapeHtml=false color=transparent cssClass=wider_dialogue_popup <img src="/img/five.png" /> :} timeout=5000 Clicking on this message within 5 seconds will open the image.</code></pre>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'gen',
|
||
callback: generateCallback,
|
||
returns: 'generated text',
|
||
namedArgumentList: [
|
||
new SlashCommandNamedArgument(
|
||
'lock', 'lock user input during generation', [ARGUMENT_TYPE.BOOLEAN], false, false, null, commonEnumProviders.boolean('onOff')(),
|
||
),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'name',
|
||
description: 'in-prompt character name for instruct mode (or unique character identifier (avatar key), which will be used as name)',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
defaultValue: 'System',
|
||
enumProvider: () => [...commonEnumProviders.characters('character')(), new SlashCommandEnumValue('System', null, enumTypes.enum, enumIcons.assistant)],
|
||
forceEnum: false,
|
||
}),
|
||
new SlashCommandNamedArgument(
|
||
'length', 'API response length in tokens', [ARGUMENT_TYPE.NUMBER], false,
|
||
),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'as',
|
||
description: 'role of the output prompt',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
enumList: [
|
||
new SlashCommandEnumValue('system', null, enumTypes.enum, enumIcons.assistant),
|
||
new SlashCommandEnumValue('char', null, enumTypes.enum, enumIcons.character),
|
||
],
|
||
}),
|
||
],
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'prompt', [ARGUMENT_TYPE.STRING], true,
|
||
),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating and allowing to configure the in-prompt name for instruct mode (default = "System").
|
||
</div>
|
||
<div>
|
||
"as" argument controls the role of the output prompt: system (default) or char. If "length" argument is provided as a number in tokens, allows to temporarily override an API response length.
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'genraw',
|
||
callback: generateRawCallback,
|
||
returns: 'generated text',
|
||
namedArgumentList: [
|
||
new SlashCommandNamedArgument(
|
||
'lock', 'lock user input during generation', [ARGUMENT_TYPE.BOOLEAN], false, false, null, commonEnumProviders.boolean('onOff')(),
|
||
),
|
||
new SlashCommandNamedArgument(
|
||
'instruct', 'use instruct mode', [ARGUMENT_TYPE.BOOLEAN], false, false, 'on', commonEnumProviders.boolean('onOff')(),
|
||
),
|
||
new SlashCommandNamedArgument(
|
||
'stop', 'one-time custom stop strings', [ARGUMENT_TYPE.LIST], false,
|
||
),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'as',
|
||
description: 'role of the output prompt',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
enumList: [
|
||
new SlashCommandEnumValue('system', null, enumTypes.enum, enumIcons.assistant),
|
||
new SlashCommandEnumValue('char', null, enumTypes.enum, enumIcons.character),
|
||
],
|
||
}),
|
||
new SlashCommandNamedArgument(
|
||
'system', 'system prompt at the start', [ARGUMENT_TYPE.STRING], false,
|
||
),
|
||
new SlashCommandNamedArgument(
|
||
'length', 'API response length in tokens', [ARGUMENT_TYPE.NUMBER], false,
|
||
),
|
||
],
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'prompt', [ARGUMENT_TYPE.STRING], true,
|
||
),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating. Does not include chat history or character card.
|
||
</div>
|
||
<div>
|
||
Use instruct=off to skip instruct formatting, e.g. <pre><code>/genraw instruct=off Why is the sky blue?</code></pre>
|
||
</div>
|
||
<div>
|
||
Use stop=... with a JSON-serialized array to add one-time custom stop strings, e.g. <pre><code>/genraw stop=["\\n"] Say hi</code></pre>
|
||
</div>
|
||
<div>
|
||
"as" argument controls the role of the output prompt: system (default) or char. "system" argument adds an (optional) system prompt at the start.
|
||
</div>
|
||
<div>
|
||
If "length" argument is provided as a number in tokens, allows to temporarily override an API response length.
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'addswipe',
|
||
callback: addSwipeCallback,
|
||
returns: 'the new swipe id',
|
||
aliases: ['swipeadd'],
|
||
namedArgumentList: [
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'switch',
|
||
description: 'switch to the new swipe',
|
||
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||
enumList: commonEnumProviders.boolean()(),
|
||
}),
|
||
],
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'text', [ARGUMENT_TYPE.STRING], true,
|
||
),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Adds a swipe to the last chat message.
|
||
</div>
|
||
<div>
|
||
Use switch=true to switch to directly switch to the new swipe.
|
||
</div>`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'stop',
|
||
callback: () => {
|
||
const stopped = stopGeneration();
|
||
return String(stopped);
|
||
},
|
||
returns: 'true/false, whether the generation was running and got stopped',
|
||
helpString: `
|
||
<div>
|
||
Stops the generation and any streaming if it is currently running.
|
||
</div>
|
||
<div>
|
||
Note: This command cannot be executed from the chat input, as sending any message or script from there is blocked during generation.
|
||
But it can be executed via automations or QR scripts/buttons.
|
||
</div>
|
||
`,
|
||
aliases: ['generate-stop'],
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'abort',
|
||
callback: abortCallback,
|
||
namedArgumentList: [
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'quiet',
|
||
description: 'Whether to suppress the toast message notifying about the /abort call.',
|
||
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||
defaultValue: 'true',
|
||
}),
|
||
],
|
||
unnamedArgumentList: [
|
||
SlashCommandArgument.fromProps({
|
||
description: 'The reason for aborting command execution. Shown when quiet=false',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
}),
|
||
],
|
||
helpString: 'Aborts the slash command batch execution.',
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'fuzzy',
|
||
callback: fuzzyCallback,
|
||
returns: 'matching item',
|
||
namedArgumentList: [
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'list',
|
||
description: 'list of items to match against',
|
||
acceptsMultiple: false,
|
||
isRequired: true,
|
||
typeList: [ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.VARIABLE_NAME],
|
||
enumProvider: commonEnumProviders.variables('all'),
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'threshold',
|
||
description: 'fuzzy match threshold (0.0 to 1.0)',
|
||
typeList: [ARGUMENT_TYPE.NUMBER],
|
||
isRequired: false,
|
||
defaultValue: '0.4',
|
||
acceptsMultiple: false,
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'mode',
|
||
description: 'fuzzy match mode',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
isRequired: false,
|
||
defaultValue: 'first',
|
||
acceptsMultiple: false,
|
||
enumList: [
|
||
new SlashCommandEnumValue('first', 'first match below the threshold', enumTypes.enum, enumIcons.default),
|
||
new SlashCommandEnumValue('best', 'best match below the threshold', enumTypes.enum, enumIcons.default),
|
||
],
|
||
}),
|
||
],
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'text to search', [ARGUMENT_TYPE.STRING], true,
|
||
),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Performs a fuzzy match of each item in the <code>list</code> against the <code>text to search</code>.
|
||
If any item matches, then its name is returned. If no item matches the text, no value is returned.
|
||
</div>
|
||
<div>
|
||
The optional <code>threshold</code> (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.
|
||
</div>
|
||
<div>
|
||
The optional <code>mode</code> argument allows to control the behavior when multiple items match the text.
|
||
<ul>
|
||
<li><code>first</code> (default) returns the first match below the threshold.</li>
|
||
<li><code>best</code> returns the best match below the threshold.</li>
|
||
</ul>
|
||
</div>
|
||
<div>
|
||
The returned value passes to the next command through the pipe.
|
||
</div>
|
||
<div>
|
||
<strong>Example:</strong>
|
||
<ul>
|
||
<li>
|
||
<pre><code>/fuzzy list=["a","b","c"] threshold=0.4 abc</code></pre>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'pass',
|
||
callback: (_, arg) => {
|
||
// We do not support arrays of closures. Arrays of strings will be send as JSON
|
||
if (Array.isArray(arg) && arg.some(x => x instanceof SlashCommandClosure)) throw new Error('Command /pass does not support multiple closures');
|
||
if (Array.isArray(arg)) return JSON.stringify(arg);
|
||
return arg;
|
||
},
|
||
returns: 'the provided value',
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'text', [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.BOOLEAN, ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.DICTIONARY, ARGUMENT_TYPE.CLOSURE], true,
|
||
),
|
||
],
|
||
aliases: ['return'],
|
||
helpString: `
|
||
<div>
|
||
<pre><span class="monospace">/pass (text)</span> – passes the text to the next command through the pipe.</pre>
|
||
</div>
|
||
<div>
|
||
<strong>Example:</strong>
|
||
<ul>
|
||
<li><pre><code>/pass Hello world</code></pre></li>
|
||
</ul>
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'delay',
|
||
callback: delayCallback,
|
||
aliases: ['wait', 'sleep'],
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'milliseconds', [ARGUMENT_TYPE.NUMBER], true,
|
||
),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Delays the next command in the pipe by the specified number of milliseconds.
|
||
</div>
|
||
<div>
|
||
<strong>Example:</strong>
|
||
<ul>
|
||
<li>
|
||
<pre><code>/delay 1000</code></pre>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'input',
|
||
aliases: ['prompt'],
|
||
callback: inputCallback,
|
||
returns: 'user input',
|
||
namedArgumentList: [
|
||
new SlashCommandNamedArgument(
|
||
'default', 'default value of the input field', [ARGUMENT_TYPE.STRING], false, false, '"string"',
|
||
),
|
||
new SlashCommandNamedArgument(
|
||
'large', 'show large input field', [ARGUMENT_TYPE.BOOLEAN], false, false, 'off', commonEnumProviders.boolean('onOff')(),
|
||
),
|
||
new SlashCommandNamedArgument(
|
||
'wide', 'show wide input field', [ARGUMENT_TYPE.BOOLEAN], false, false, 'off', commonEnumProviders.boolean('onOff')(),
|
||
),
|
||
new SlashCommandNamedArgument(
|
||
'okButton', 'text for the ok button', [ARGUMENT_TYPE.STRING], false,
|
||
),
|
||
new SlashCommandNamedArgument(
|
||
'rows', 'number of rows for the input field', [ARGUMENT_TYPE.NUMBER], false,
|
||
),
|
||
],
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'text to display', [ARGUMENT_TYPE.STRING], false,
|
||
),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Shows a popup with the provided text and an input field.
|
||
The <code>default</code> argument is the default value of the input field, and the text argument is the text to display.
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'run',
|
||
aliases: ['call', 'exec'],
|
||
callback: runCallback,
|
||
returns: 'result of the executed closure of QR',
|
||
namedArgumentList: [
|
||
new SlashCommandNamedArgument(
|
||
'args', 'named arguments', [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.BOOLEAN, ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.DICTIONARY], false, true,
|
||
),
|
||
],
|
||
unnamedArgumentList: [
|
||
SlashCommandArgument.fromProps({
|
||
description: 'scoped variable or qr label',
|
||
typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.CLOSURE],
|
||
isRequired: true,
|
||
enumProvider: (executor, scope) => [
|
||
...commonEnumProviders.variables('scope')(executor, scope),
|
||
...(typeof window['qrEnumProviderExecutables'] === 'function') ? window['qrEnumProviderExecutables']() : [],
|
||
],
|
||
}),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Runs a closure from a scoped variable, or a Quick Reply with the specified name from a currently active preset or from another preset.
|
||
Named arguments can be referenced in a QR with <code>{{arg::key}}</code>.
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'messages',
|
||
callback: getMessagesCallback,
|
||
aliases: ['message'],
|
||
namedArgumentList: [
|
||
new SlashCommandNamedArgument(
|
||
'names', 'show message author names', [ARGUMENT_TYPE.BOOLEAN], false, false, 'off', commonEnumProviders.boolean('onOff')(),
|
||
),
|
||
new SlashCommandNamedArgument(
|
||
'hidden', 'include hidden messages', [ARGUMENT_TYPE.BOOLEAN], false, false, 'on', commonEnumProviders.boolean('onOff')(),
|
||
),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'role',
|
||
description: 'filter messages by role',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
enumList: [
|
||
new SlashCommandEnumValue('system', null, enumTypes.enum, enumIcons.system),
|
||
new SlashCommandEnumValue('assistant', null, enumTypes.enum, enumIcons.assistant),
|
||
new SlashCommandEnumValue('user', null, enumTypes.enum, enumIcons.user),
|
||
],
|
||
}),
|
||
],
|
||
unnamedArgumentList: [
|
||
SlashCommandArgument.fromProps({
|
||
description: 'message index (starts with 0) or range',
|
||
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE],
|
||
isRequired: true,
|
||
enumProvider: commonEnumProviders.messages(),
|
||
}),
|
||
],
|
||
returns: 'the specified message or range of messages as a string',
|
||
helpString: `
|
||
<div>
|
||
Returns the specified message or range of messages as a string.
|
||
</div>
|
||
<div>
|
||
Use the <code>hidden=off</code> argument to exclude hidden messages.
|
||
</div>
|
||
<div>
|
||
Use the <code>role</code> argument to filter messages by role. Possible values are: system, assistant, user.
|
||
</div>
|
||
<div>
|
||
<strong>Examples:</strong>
|
||
<ul>
|
||
<li>
|
||
<pre><code>/messages 10</code></pre>
|
||
Returns the 10th message.
|
||
</li>
|
||
<li>
|
||
<pre><code>/messages names=on 5-10</code></pre>
|
||
Returns messages 5 through 10 with author names.
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'setinput',
|
||
callback: setInputCallback,
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'text', [ARGUMENT_TYPE.STRING], true,
|
||
),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Sets the user input to the specified text and passes it to the next command through the pipe.
|
||
</div>
|
||
<div>
|
||
<strong>Example:</strong>
|
||
<ul>
|
||
<li>
|
||
<pre><code>/setinput Hello world</code></pre>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'popup',
|
||
callback: popupCallback,
|
||
returns: 'popup text',
|
||
namedArgumentList: [
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'scroll',
|
||
description: 'allows vertical scrolling of the content',
|
||
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||
enumList: commonEnumProviders.boolean('trueFalse')(),
|
||
defaultValue: 'true',
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'large',
|
||
description: 'show large popup',
|
||
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||
enumList: commonEnumProviders.boolean('trueFalse')(),
|
||
defaultValue: 'false',
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'wide',
|
||
description: 'show wide popup',
|
||
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||
enumList: commonEnumProviders.boolean('trueFalse')(),
|
||
defaultValue: 'false',
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'wider',
|
||
description: 'show wider popup',
|
||
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||
enumList: commonEnumProviders.boolean('trueFalse')(),
|
||
defaultValue: 'false',
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'transparent',
|
||
description: 'show transparent popup',
|
||
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||
enumList: commonEnumProviders.boolean('trueFalse')(),
|
||
defaultValue: 'false',
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'okButton',
|
||
description: 'text for the OK button',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
defaultValue: 'OK',
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'cancelButton',
|
||
description: 'text for the Cancel button',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'result',
|
||
description: 'if enabled, returns the popup result (as an integer) instead of the popup text. Resolves to 1 for OK and 0 cancel button, empty string for exiting out.',
|
||
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||
enumList: commonEnumProviders.boolean('trueFalse')(),
|
||
defaultValue: 'false',
|
||
}),
|
||
],
|
||
unnamedArgumentList: [
|
||
SlashCommandArgument.fromProps({
|
||
description: 'popup text',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
isRequired: true,
|
||
}),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Shows a blocking popup with the specified text and buttons.
|
||
Returns the popup text.
|
||
</div>
|
||
<div>
|
||
<strong>Example:</strong>
|
||
<ul>
|
||
<li>
|
||
<pre><code>/popup large=on wide=on okButton="Confirm" Please confirm this action.</code></pre>
|
||
</li>
|
||
<li>
|
||
<pre><code>/popup okButton="Left" cancelButton="Right" result=true Do you want to go left or right? | /echo 0 means right, 1 means left. Choice: {{pipe}}</code></pre>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'buttons',
|
||
callback: buttonsCallback,
|
||
returns: 'clicked button label',
|
||
namedArgumentList: [
|
||
new SlashCommandNamedArgument(
|
||
'labels', 'button labels', [ARGUMENT_TYPE.LIST], true,
|
||
),
|
||
],
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'text', [ARGUMENT_TYPE.STRING], true,
|
||
),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Shows a blocking popup with the specified text and buttons.
|
||
Returns the clicked button label into the pipe or empty string if canceled.
|
||
</div>
|
||
<div>
|
||
<strong>Example:</strong>
|
||
<ul>
|
||
<li>
|
||
<pre><code>/buttons labels=["Yes","No"] Do you want to continue?</code></pre>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'trimtokens',
|
||
callback: trimTokensCallback,
|
||
returns: 'trimmed text',
|
||
namedArgumentList: [
|
||
new SlashCommandNamedArgument(
|
||
'limit', 'number of tokens to keep', [ARGUMENT_TYPE.NUMBER], true,
|
||
),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'direction',
|
||
description: 'trim direction',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
isRequired: true,
|
||
enumList: [
|
||
new SlashCommandEnumValue('start', null, enumTypes.enum, '⏪'),
|
||
new SlashCommandEnumValue('end', null, enumTypes.enum, '⏩'),
|
||
],
|
||
}),
|
||
],
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'text', [ARGUMENT_TYPE.STRING], false,
|
||
),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Trims the start or end of text to the specified number of tokens.
|
||
</div>
|
||
<div>
|
||
<strong>Example:</strong>
|
||
<ul>
|
||
<li>
|
||
<pre><code>/trimtokens limit=5 direction=start This is a long sentence with many words</code></pre>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'trimstart',
|
||
callback: trimStartCallback,
|
||
returns: 'trimmed text',
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'text', [ARGUMENT_TYPE.STRING], true,
|
||
),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Trims the text to the start of the first full sentence.
|
||
</div>
|
||
<div>
|
||
<strong>Example:</strong>
|
||
<ul>
|
||
<li>
|
||
<pre><code>/trimstart This is a sentence. And here is another sentence.</code></pre>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'trimend',
|
||
callback: trimEndCallback,
|
||
returns: 'trimmed text',
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'text', [ARGUMENT_TYPE.STRING], true,
|
||
),
|
||
],
|
||
helpString: 'Trims the text to the end of the last full sentence.',
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'inject',
|
||
callback: injectCallback,
|
||
namedArgumentList: [
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'id',
|
||
description: 'injection ID or variable name pointing to ID',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
isRequired: true,
|
||
enumProvider: commonEnumProviders.injects,
|
||
}),
|
||
new SlashCommandNamedArgument(
|
||
'position', 'injection position', [ARGUMENT_TYPE.STRING], false, false, 'after', ['before', 'after', 'chat', 'none'],
|
||
),
|
||
new SlashCommandNamedArgument(
|
||
'depth', 'injection depth', [ARGUMENT_TYPE.NUMBER], false, false, '4',
|
||
),
|
||
new SlashCommandNamedArgument(
|
||
'scan', 'include injection content into World Info scans', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',
|
||
),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'role',
|
||
description: 'role for in-chat injections',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
isRequired: false,
|
||
enumList: [
|
||
new SlashCommandEnumValue('system', null, enumTypes.enum, enumIcons.system),
|
||
new SlashCommandEnumValue('assistant', null, enumTypes.enum, enumIcons.assistant),
|
||
new SlashCommandEnumValue('user', null, enumTypes.enum, enumIcons.user),
|
||
],
|
||
}),
|
||
new SlashCommandNamedArgument(
|
||
'ephemeral', 'remove injection after generation', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',
|
||
),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'filter',
|
||
description: 'if a filter is defined, an injection will only be performed if the closure returns true',
|
||
typeList: [ARGUMENT_TYPE.CLOSURE],
|
||
isRequired: false,
|
||
acceptsMultiple: false,
|
||
}),
|
||
],
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'text', [ARGUMENT_TYPE.STRING], false,
|
||
),
|
||
],
|
||
helpString: 'Injects a text into the LLM prompt for the current chat. Requires a unique injection ID. Positions: "before" main prompt, "after" main prompt, in-"chat", hidden with "none" (default: after). Depth: injection depth for the prompt (default: 4). Role: role for in-chat injections (default: system). Scan: include injection content into World Info scans (default: false). Hidden injects in "none" position are not inserted into the prompt but can be used for triggering WI entries.',
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'listinjects',
|
||
callback: listInjectsCallback,
|
||
helpString: 'Lists all script injections for the current chat. Displays injects in a popup by default. Use the <code>return</code> argument to change the return type.',
|
||
returns: 'Optionalls the JSON object of script injections',
|
||
namedArgumentList: [
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'return',
|
||
description: 'The way how you want the return value to be provided',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
defaultValue: 'popup-html',
|
||
enumList: slashCommandReturnHelper.enumList({ allowPipe: false, allowObject: true, allowChat: true, allowPopup: true, allowTextVersion: false }),
|
||
forceEnum: true,
|
||
}),
|
||
// TODO remove some day
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'format',
|
||
description: '!!! DEPRECATED - use "return" instead !!! output format',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
isRequired: true,
|
||
forceEnum: true,
|
||
enumList: [
|
||
new SlashCommandEnumValue('popup', 'Show injects in a popup.', enumTypes.enum, enumIcons.default),
|
||
new SlashCommandEnumValue('chat', 'Post a system message to the chat.', enumTypes.enum, enumIcons.default),
|
||
new SlashCommandEnumValue('none', 'Just return the injects as a JSON object.', enumTypes.enum, enumIcons.default),
|
||
],
|
||
}),
|
||
],
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'flushinject',
|
||
aliases: ['flushinjects'],
|
||
unnamedArgumentList: [
|
||
SlashCommandArgument.fromProps({
|
||
description: 'injection ID or a variable name pointing to ID',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
defaultValue: '',
|
||
enumProvider: commonEnumProviders.injects,
|
||
}),
|
||
],
|
||
callback: flushInjectsCallback,
|
||
helpString: 'Removes a script injection for the current chat. If no ID is provided, removes all script injections.',
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'tokens',
|
||
callback: (_, text) => {
|
||
if (text instanceof SlashCommandClosure || Array.isArray(text)) throw new Error('Unnamed argument cannot be a closure for command /tokens');
|
||
return getTokenCountAsync(text).then(count => String(count));
|
||
},
|
||
returns: 'number of tokens',
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'text', [ARGUMENT_TYPE.STRING], true,
|
||
),
|
||
],
|
||
helpString: 'Counts the number of tokens in the provided text.',
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'model',
|
||
callback: modelCallback,
|
||
returns: 'current model',
|
||
namedArgumentList: [
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'quiet',
|
||
description: 'suppress the toast message on model change',
|
||
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||
defaultValue: 'false',
|
||
enumList: commonEnumProviders.boolean('trueFalse')(),
|
||
}),
|
||
],
|
||
unnamedArgumentList: [
|
||
SlashCommandArgument.fromProps({
|
||
description: 'model name',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
enumProvider: () => getModelOptions(true)?.options?.map(option => new SlashCommandEnumValue(option.value, option.value !== option.text ? option.text : null)) ?? [],
|
||
}),
|
||
],
|
||
helpString: 'Sets the model for the current API. Gets the current model name if no argument is provided.',
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'getpromptentry',
|
||
aliases: ['getpromptentries'],
|
||
callback: getPromptEntryCallback,
|
||
returns: 'true/false state of prompt(s)',
|
||
namedArgumentList: [
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'identifier',
|
||
description: 'Prompt entry identifier(s) to retrieve',
|
||
typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.LIST],
|
||
acceptsMultiple: true,
|
||
enumProvider: () =>
|
||
promptManager.serviceSettings.prompts
|
||
.map(prompt => prompt.identifier)
|
||
.map(identifier => new SlashCommandEnumValue(identifier)),
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'name',
|
||
description: 'Prompt entry name(s) to retrieve',
|
||
typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.LIST],
|
||
acceptsMultiple: true,
|
||
enumProvider: () =>
|
||
promptManager.serviceSettings.prompts
|
||
.map(prompt => prompt.name)
|
||
.map(name => new SlashCommandEnumValue(name)),
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'return',
|
||
description: 'Whether the return will be simple, a list, or a dict.',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
defaultValue: 'simple',
|
||
enumList: ['simple', 'list', 'dict'],
|
||
}),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Gets the state of the specified prompt entries.
|
||
</div>
|
||
<div>
|
||
If <code>return</code> is <code>simple</code> (default) then the return will be a single value if only one value was retrieved; otherwise uses a dict (if the identifier parameter was used) or a list.
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'setpromptentry',
|
||
aliases: ['setpromptentries'],
|
||
callback: setPromptEntryCallback,
|
||
namedArgumentList: [
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'identifier',
|
||
description: 'Prompt entry identifier(s) to target',
|
||
typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.LIST],
|
||
acceptsMultiple: true,
|
||
enumProvider: () => {
|
||
const prompts = promptManager.serviceSettings.prompts;
|
||
return prompts.map(prompt => new SlashCommandEnumValue(prompt.identifier, prompt.name, enumTypes.enum));
|
||
},
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'name',
|
||
description: 'Prompt entry name(s) to target',
|
||
typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.LIST],
|
||
acceptsMultiple: true,
|
||
enumProvider: () => {
|
||
const prompts = promptManager.serviceSettings.prompts;
|
||
return prompts.map(prompt => new SlashCommandEnumValue(prompt.name, prompt.identifier, enumTypes.enum));
|
||
},
|
||
}),
|
||
],
|
||
unnamedArgumentList: [
|
||
SlashCommandArgument.fromProps({
|
||
description: 'Set entry/entries on or off',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
isRequired: true,
|
||
acceptsMultiple: false,
|
||
defaultValue: 'toggle', // unnamed arguments don't support default values yet
|
||
enumList: commonEnumProviders.boolean('onOffToggle')(),
|
||
}),
|
||
],
|
||
helpString: 'Sets the specified prompt manager entry/entries on or off.',
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'pick-icon',
|
||
callback: async () => ((await showFontAwesomePicker()) ?? false).toString(),
|
||
returns: 'The chosen icon name or false if cancelled.',
|
||
helpString: `
|
||
<div>Opens a popup with all the available Font Awesome icons and returns the selected icon's name.</div>
|
||
<div>
|
||
<strong>Example:</strong>
|
||
<ul>
|
||
<li>
|
||
<pre><code>/pick-icon |\n/if left={{pipe}} rule=eq right=false\n\telse={: /echo chosen icon: "{{pipe}}" :}\n\t{: /echo cancelled icon selection :}\n|</code></pre>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'api-url',
|
||
callback: setApiUrlCallback,
|
||
returns: 'the current API url',
|
||
aliases: ['server'],
|
||
namedArgumentList: [
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'api',
|
||
description: 'API to set/get the URL for - if not provided, current API is used',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
enumList: [
|
||
new SlashCommandEnumValue('custom', 'custom OpenAI-compatible', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'openai')), 'O'),
|
||
new SlashCommandEnumValue('kobold', 'KoboldAI Classic', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'kobold')), 'K'),
|
||
...Object.values(textgen_types).map(api => new SlashCommandEnumValue(api, null, enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'textgenerationwebui')), 'T')),
|
||
],
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'connect',
|
||
description: 'Whether to auto-connect to the API after setting the URL',
|
||
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||
defaultValue: 'true',
|
||
enumList: commonEnumProviders.boolean('trueFalse')(),
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'quiet',
|
||
description: 'suppress the toast message on API change',
|
||
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||
defaultValue: 'false',
|
||
enumList: commonEnumProviders.boolean('trueFalse')(),
|
||
}),
|
||
],
|
||
unnamedArgumentList: [
|
||
SlashCommandArgument.fromProps({
|
||
description: 'API url to connect to',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
}),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Set the API url / server url for the currently selected API, including the port. If no argument is provided, it will return the current API url.
|
||
</div>
|
||
<div>
|
||
If a manual API is provided to <b>set</b> the URL, make sure to set <code>connect=false</code>, as auto-connect only works for the currently selected API,
|
||
or consider switching to it with <code>/api</code> first.
|
||
</div>
|
||
<div>
|
||
This slash command works for most of the Text Completion sources, KoboldAI Classic, and also Custom OpenAI compatible for the Chat Completion sources. If unsure which APIs are supported,
|
||
check the auto-completion of the optional <code>api</code> argument of this command.
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'tokenizer',
|
||
callback: selectTokenizerCallback,
|
||
returns: 'current tokenizer',
|
||
unnamedArgumentList: [
|
||
SlashCommandArgument.fromProps({
|
||
description: 'tokenizer name',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
enumList: getAvailableTokenizers().map(tokenizer =>
|
||
new SlashCommandEnumValue(tokenizer.tokenizerKey, tokenizer.tokenizerName, enumTypes.enum, enumIcons.default)),
|
||
}),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Selects tokenizer by name. Gets the current tokenizer if no name is provided.
|
||
</div>
|
||
<div>
|
||
<strong>Available tokenizers:</strong>
|
||
<pre><code>${getAvailableTokenizers().map(t => t.tokenizerKey).join(', ')}</code></pre>
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'upper',
|
||
aliases: ['uppercase', 'to-upper'],
|
||
callback: (_, text) => typeof text === 'string' ? text.toUpperCase() : '',
|
||
returns: 'uppercase string',
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'string', [ARGUMENT_TYPE.STRING], true, false,
|
||
),
|
||
],
|
||
helpString: 'Converts the provided string to uppercase.',
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'lower',
|
||
aliases: ['lowercase', 'to-lower'],
|
||
callback: (_, text) => typeof text === 'string' ? text.toLowerCase() : '',
|
||
returns: 'lowercase string',
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'string', [ARGUMENT_TYPE.STRING], true, false,
|
||
),
|
||
],
|
||
helpString: 'Converts the provided string to lowercase.',
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'substr',
|
||
aliases: ['substring'],
|
||
callback: (arg, text) => typeof text === 'string' ? text.slice(...[Number(arg.start), arg.end && Number(arg.end)]) : '',
|
||
returns: 'substring',
|
||
namedArgumentList: [
|
||
new SlashCommandNamedArgument(
|
||
'start', 'start index', [ARGUMENT_TYPE.NUMBER], false, false,
|
||
),
|
||
new SlashCommandNamedArgument(
|
||
'end', 'end index', [ARGUMENT_TYPE.NUMBER], false, false,
|
||
),
|
||
],
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'string', [ARGUMENT_TYPE.STRING], true, false,
|
||
),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Extracts text from the provided string.
|
||
</div>
|
||
<div>
|
||
If <code>start</code> is omitted, it's treated as 0.<br />
|
||
If <code>start</code> < 0, the index is counted from the end of the string.<br />
|
||
If <code>start</code> >= the string's length, an empty string is returned.<br />
|
||
If <code>end</code> is omitted, or if <code>end</code> >= the string's length, extracts to the end of the string.<br />
|
||
If <code>end</code> < 0, the index is counted from the end of the string.<br />
|
||
If <code>end</code> <= <code>start</code> after normalizing negative values, an empty string is returned.
|
||
</div>
|
||
<div>
|
||
<strong>Example:</strong>
|
||
<pre>/let x The morning is upon us. || </pre>
|
||
<pre>/substr start=-3 {{var::x}} | /echo |/# us. ||</pre>
|
||
<pre>/substr start=-3 end=-1 {{var::x}} | /echo |/# us ||</pre>
|
||
<pre>/substr end=-1 {{var::x}} | /echo |/# The morning is upon us ||</pre>
|
||
<pre>/substr start=4 end=-1 {{var::x}} | /echo |/# morning is upon us ||</pre>
|
||
</div>
|
||
`,
|
||
}));
|
||
|
||
registerVariableCommands();
|
||
}
|
||
|
||
const NARRATOR_NAME_KEY = 'narrator_name';
|
||
const NARRATOR_NAME_DEFAULT = 'System';
|
||
export const COMMENT_NAME_DEFAULT = 'Note';
|
||
const SCRIPT_PROMPT_KEY = 'script_inject_';
|
||
|
||
/**
|
||
* Adds a new script injection to the chat.
|
||
* @param {import('./slash-commands/SlashCommand.js').NamedArguments} args Named arguments
|
||
* @param {import('./slash-commands/SlashCommand.js').UnnamedArguments} value Unnamed argument
|
||
*/
|
||
function injectCallback(args, value) {
|
||
const positions = {
|
||
'before': extension_prompt_types.BEFORE_PROMPT,
|
||
'after': extension_prompt_types.IN_PROMPT,
|
||
'chat': extension_prompt_types.IN_CHAT,
|
||
'none': extension_prompt_types.NONE,
|
||
};
|
||
const roles = {
|
||
'system': extension_prompt_roles.SYSTEM,
|
||
'user': extension_prompt_roles.USER,
|
||
'assistant': extension_prompt_roles.ASSISTANT,
|
||
};
|
||
|
||
const id = String(args?.id);
|
||
const ephemeral = isTrueBoolean(String(args?.ephemeral));
|
||
|
||
if (!id) {
|
||
console.warn('WARN: No ID provided for /inject command');
|
||
toastr.warning('No ID provided for /inject command');
|
||
return '';
|
||
}
|
||
|
||
const defaultPosition = 'after';
|
||
const defaultDepth = 4;
|
||
const positionValue = args?.position ?? defaultPosition;
|
||
const position = positions[positionValue] ?? positions[defaultPosition];
|
||
const depthValue = Number(args?.depth) ?? defaultDepth;
|
||
const depth = isNaN(depthValue) ? defaultDepth : depthValue;
|
||
const roleValue = typeof args?.role === 'string' ? args.role.toLowerCase().trim() : Number(args?.role ?? extension_prompt_roles.SYSTEM);
|
||
const role = roles[roleValue] ?? roles[extension_prompt_roles.SYSTEM];
|
||
const scan = isTrueBoolean(String(args?.scan));
|
||
const filter = args?.filter instanceof SlashCommandClosure ? args.filter.rawText : null;
|
||
const filterFunction = args?.filter instanceof SlashCommandClosure ? closureToFilter(args.filter) : null;
|
||
value = value || '';
|
||
|
||
if (args?.filter && !String(filter ?? '').trim()) {
|
||
throw new Error('Failed to parse the filter argument. Make sure it is a valid non-empty closure.');
|
||
}
|
||
|
||
const prefixedId = `${SCRIPT_PROMPT_KEY}${id}`;
|
||
|
||
if (!chat_metadata.script_injects) {
|
||
chat_metadata.script_injects = {};
|
||
}
|
||
|
||
if (value) {
|
||
const inject = { value, position, depth, scan, role, filter };
|
||
chat_metadata.script_injects[id] = inject;
|
||
} else {
|
||
delete chat_metadata.script_injects[id];
|
||
}
|
||
|
||
setExtensionPrompt(prefixedId, String(value), position, depth, scan, role, filterFunction);
|
||
saveMetadataDebounced();
|
||
|
||
if (ephemeral) {
|
||
let deleted = false;
|
||
const unsetInject = () => {
|
||
if (deleted) {
|
||
return;
|
||
}
|
||
console.log('Removing ephemeral script injection', id);
|
||
delete chat_metadata.script_injects[id];
|
||
setExtensionPrompt(prefixedId, '', position, depth, scan, role, filterFunction);
|
||
saveMetadataDebounced();
|
||
deleted = true;
|
||
};
|
||
eventSource.once(event_types.GENERATION_ENDED, unsetInject);
|
||
eventSource.once(event_types.GENERATION_STOPPED, unsetInject);
|
||
}
|
||
|
||
return '';
|
||
}
|
||
|
||
async function listInjectsCallback(args) {
|
||
/** @type {import('./slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */
|
||
let returnType = args.return;
|
||
|
||
// Old legacy return type handling
|
||
if (args.format) {
|
||
toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning');
|
||
const type = String(args?.format).toLowerCase().trim();
|
||
if (!chat_metadata.script_injects || !Object.keys(chat_metadata.script_injects).length) {
|
||
type !== 'none' && toastr.info('No script injections for the current chat');
|
||
}
|
||
switch (type) {
|
||
case 'none':
|
||
returnType = 'none';
|
||
break;
|
||
case 'chat':
|
||
returnType = 'chat-html';
|
||
break;
|
||
case 'popup':
|
||
default:
|
||
returnType = 'popup-html';
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Now the actual new return type handling
|
||
const buildTextValue = (injects) => {
|
||
const injectsStr = Object.entries(injects)
|
||
.map(([id, inject]) => {
|
||
const position = Object.entries(extension_prompt_types);
|
||
const positionName = position.find(([_, value]) => value === inject.position)?.[0] ?? 'unknown';
|
||
return `* **${id}**: <code>${inject.value}</code> (${positionName}, depth: ${inject.depth}, scan: ${inject.scan ?? false}, role: ${inject.role ?? extension_prompt_roles.SYSTEM})`;
|
||
})
|
||
.join('\n');
|
||
return `### Script injections:\n${injectsStr || 'No script injections for the current chat'}`;
|
||
};
|
||
|
||
return await slashCommandReturnHelper.doReturn(returnType ?? 'popup-html', chat_metadata.script_injects ?? {}, { objectToStringFunc: buildTextValue });
|
||
}
|
||
|
||
/**
|
||
* 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)) {
|
||
/**
|
||
* Rehydrates a filter closure from a string.
|
||
* @returns {SlashCommandClosure | null}
|
||
*/
|
||
function reviveFilterClosure() {
|
||
if (!inject.filter) {
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
return new SlashCommandParser().parse(inject.filter, true);
|
||
} catch (error) {
|
||
console.warn('Failed to revive filter closure for script injection', id, error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
const prefixedId = `${SCRIPT_PROMPT_KEY}${id}`;
|
||
const filterClosure = reviveFilterClosure();
|
||
const filter = filterClosure ? closureToFilter(filterClosure) : null;
|
||
console.log('Adding script injection', id);
|
||
setExtensionPrompt(prefixedId, inject.value, inject.position, inject.depth, inject.scan, inject.role, filter);
|
||
}
|
||
}
|
||
|
||
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');
|
||
|
||
const scrollableContainer = document.createElement('div');
|
||
scrollableContainer.classList.add('scrollable-buttons-container');
|
||
|
||
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', async () => {
|
||
await popup.complete(result);
|
||
});
|
||
buttonElement.innerText = button;
|
||
buttonContainer.appendChild(buttonElement);
|
||
}
|
||
|
||
scrollableContainer.appendChild(buttonContainer);
|
||
|
||
const popupContainer = document.createElement('div');
|
||
popupContainer.innerHTML = safeValue;
|
||
popupContainer.appendChild(scrollableContainer);
|
||
|
||
// Ensure the popup uses flex layout
|
||
popupContainer.style.display = 'flex';
|
||
popupContainer.style.flexDirection = 'column';
|
||
popupContainer.style.maxHeight = '80vh'; // Limit the overall height of the popup
|
||
|
||
popup = new Popup(popupContainer, POPUP_TYPE.TEXT, '', { okButton: 'Cancel', allowVerticalScrolling: true });
|
||
popup.show()
|
||
.then((result => resolve(typeof result === 'number' ? resultToButtonMap.get(result) ?? '' : '')))
|
||
.catch(() => resolve(''));
|
||
});
|
||
} catch {
|
||
return '';
|
||
}
|
||
}
|
||
|
||
async function popupCallback(args, value) {
|
||
const safeBody = DOMPurify.sanitize(value || '');
|
||
const safeHeader = args?.header && typeof args?.header === 'string' ? DOMPurify.sanitize(args.header) : null;
|
||
const requestedResult = isTrueBoolean(args?.result);
|
||
|
||
/** @type {import('./popup.js').PopupOptions} */
|
||
const popupOptions = {
|
||
allowVerticalScrolling: !isFalseBoolean(args?.scroll),
|
||
large: isTrueBoolean(args?.large),
|
||
wide: isTrueBoolean(args?.wide),
|
||
wider: isTrueBoolean(args?.wider),
|
||
transparent: isTrueBoolean(args?.transparent),
|
||
okButton: args?.okButton !== undefined && typeof args?.okButton === 'string' ? args.okButton : 'Ok',
|
||
cancelButton: args?.cancelButton !== undefined && typeof args?.cancelButton === 'string' ? args.cancelButton : null,
|
||
};
|
||
const result = await Popup.show.text(safeHeader, safeBody, popupOptions);
|
||
return String(requestedResult ? result ?? '' : value);
|
||
}
|
||
|
||
async 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 processMessage = async (mesId) => {
|
||
const msg = chat[mesId];
|
||
if (!msg) {
|
||
console.warn(`WARN: No message found with ID ${mesId}`);
|
||
return null;
|
||
}
|
||
|
||
if (role && !filterByRole(msg)) {
|
||
console.debug(`/messages: Skipping message with ID ${mesId} due to role filter`);
|
||
return null;
|
||
}
|
||
|
||
if (!includeHidden && msg.is_system) {
|
||
console.debug(`/messages: Skipping hidden message with ID ${mesId}`);
|
||
return null;
|
||
}
|
||
|
||
return includeNames ? `${msg.name}: ${msg.mes}` : msg.mes;
|
||
};
|
||
|
||
const messagePromises = [];
|
||
|
||
for (let rInd = range.start; rInd <= range.end; ++rInd)
|
||
messagePromises.push(processMessage(rInd));
|
||
|
||
const messages = await Promise.all(messagePromises);
|
||
|
||
return messages.filter(m => m !== null).join('\n\n');
|
||
}
|
||
|
||
async function runCallback(args, name) {
|
||
if (!name) {
|
||
throw new Error('No name provided for /run command');
|
||
}
|
||
|
||
if (name instanceof SlashCommandClosure) {
|
||
name.breakController = new SlashCommandBreakController();
|
||
return (await name.execute())?.pipe;
|
||
}
|
||
|
||
/**@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.breakController = new SlashCommandBreakController();
|
||
if (args._debugController && !closure.debugController) {
|
||
closure.debugController = args._debugController;
|
||
}
|
||
while (closure.providedArgumentList.pop());
|
||
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();
|
||
/**@type {ExecuteSlashCommandsOptions} */
|
||
const options = {
|
||
abortController: args._abortController,
|
||
debugController: args._debugController,
|
||
};
|
||
return await window['executeQuickReplyByName'](name, args, options);
|
||
} catch (error) {
|
||
throw new Error(`Error running Quick Reply "${name}": ${error.message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @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, mode: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(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;
|
||
}
|
||
}
|
||
|
||
function getFirstMatch() {
|
||
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);
|
||
console.debug('/fuzzy: result', result);
|
||
if (result.length > 0) {
|
||
console.info('/fuzzy: first matched', searchItem);
|
||
return searchItem;
|
||
}
|
||
}
|
||
|
||
console.info('/fuzzy: no match');
|
||
return '';
|
||
}
|
||
|
||
function getBestMatch() {
|
||
const fuse = new Fuse(list, params);
|
||
const result = fuse.search(searchInValue);
|
||
console.debug('/fuzzy: result', result);
|
||
if (result.length > 0) {
|
||
console.info('/fuzzy: best matched', result[0].item);
|
||
return result[0].item;
|
||
}
|
||
|
||
console.info('/fuzzy: no match');
|
||
return '';
|
||
}
|
||
|
||
switch (String(args.mode).trim().toLowerCase()) {
|
||
case 'best':
|
||
return getBestMatch();
|
||
case 'first':
|
||
default:
|
||
return getFirstMatch();
|
||
}
|
||
} 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;
|
||
} catch (err) {
|
||
console.error('Error on /genraw generation', err);
|
||
toastr.error(err.message, 'API Error', { preventDuplicates: true });
|
||
} finally {
|
||
if (lock) {
|
||
activateSendButtons();
|
||
}
|
||
flushEphemeralStoppingStrings();
|
||
}
|
||
return '';
|
||
}
|
||
|
||
/**
|
||
* Callback for the /gen command
|
||
* @param {object} args Named arguments
|
||
* @param {string} value Unnamed argument
|
||
* @returns {Promise<string>} The generated text
|
||
*/
|
||
async function generateCallback(args, value) {
|
||
// 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 char = findChar({ name: name });
|
||
const result = await generateQuietPrompt(value, quietToLoud, false, '', char?.name ?? name, length);
|
||
return result;
|
||
} catch (err) {
|
||
console.error('Error on /gen generation', err);
|
||
toastr.error(err.message, 'API Error', { preventDuplicates: true });
|
||
} finally {
|
||
if (lock) {
|
||
activateSendButtons();
|
||
}
|
||
flushEphemeralStoppingStrings();
|
||
}
|
||
return '';
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {{title?: string, severity?: string, timeout?: string, extendedTimeout?: string, preventDuplicates?: string, awaitDismissal?: string, cssClass?: string, color?: string, escapeHtml?: string, onClick?: SlashCommandClosure}} args - named arguments from the slash command
|
||
* @param {string} value - The string to echo (unnamed argument from the slash command)
|
||
* @returns {Promise<string>} The text that was echoed
|
||
*/
|
||
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 '';
|
||
}
|
||
|
||
if (args.severity && !['error', 'warning', 'success', 'info'].includes(args.severity)) {
|
||
toastr.warning(`Invalid severity provided for /echo command: ${args.severity}`);
|
||
args.severity = null;
|
||
}
|
||
|
||
// Make sure that the value is a string
|
||
value = String(value);
|
||
|
||
let title = args.title ? args.title : undefined;
|
||
const severity = args.severity ? args.severity : 'info';
|
||
|
||
/** @type {ToastrOptions} */
|
||
const options = {};
|
||
if (args.timeout && !isNaN(parseInt(args.timeout))) options.timeOut = parseInt(args.timeout);
|
||
if (args.extendedTimeout && !isNaN(parseInt(args.extendedTimeout))) options.extendedTimeOut = parseInt(args.extendedTimeout);
|
||
if (isTrueBoolean(args.preventDuplicates)) options.preventDuplicates = true;
|
||
if (args.cssClass) options.toastClass = args.cssClass;
|
||
options.escapeHtml = args.escapeHtml !== undefined ? isTrueBoolean(args.escapeHtml) : true;
|
||
|
||
// Prepare possible await handling
|
||
let awaitDismissal = isTrueBoolean(args.awaitDismissal);
|
||
let resolveToastDismissal;
|
||
|
||
if (awaitDismissal) {
|
||
options.onHidden = () => resolveToastDismissal(value);
|
||
}
|
||
if (args.onClick) {
|
||
if (args.onClick instanceof SlashCommandClosure) {
|
||
options.onclick = async () => {
|
||
// Execute the slash command directly, with its internal scope and everything. Clear progress handler so it doesn't interfere with command execution progress.
|
||
args.onClick.onProgress = null;
|
||
await args.onClick.execute();
|
||
};
|
||
} else {
|
||
toastr.warning('Invalid onClick provided for /echo command. This is not a closure');
|
||
}
|
||
}
|
||
|
||
// If we allow HTML, we need to sanitize it to prevent security risks
|
||
if (!options.escapeHtml) {
|
||
if (title) title = DOMPurify.sanitize(title, { FORBID_TAGS: ['style'] });
|
||
value = DOMPurify.sanitize(value, { FORBID_TAGS: ['style'] });
|
||
}
|
||
|
||
let toast;
|
||
switch (severity) {
|
||
case 'error':
|
||
toast = toastr.error(value, title, options);
|
||
break;
|
||
case 'warning':
|
||
toast = toastr.warning(value, title, options);
|
||
break;
|
||
case 'success':
|
||
toast = toastr.success(value, title, options);
|
||
break;
|
||
case 'info':
|
||
default:
|
||
toast = toastr.info(value, title, options);
|
||
break;
|
||
}
|
||
|
||
if (args.color) {
|
||
toast.css('background-color', args.color);
|
||
}
|
||
|
||
if (awaitDismissal) {
|
||
return new Promise((resolve) => {
|
||
resolveToastDismissal = resolve;
|
||
});
|
||
} else {
|
||
return value;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {{switch?: string}} args - named arguments
|
||
* @param {string} value - The swipe text to add (unnamed argument)
|
||
*/
|
||
async function addSwipeCallback(args, value) {
|
||
const lastMessage = chat[chat.length - 1];
|
||
|
||
if (!lastMessage) {
|
||
toastr.warning('No messages to add swipes to.');
|
||
return '';
|
||
}
|
||
|
||
if (!value) {
|
||
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(value);
|
||
lastMessage.swipe_info.push({
|
||
send_date: getMessageTimeStamp(),
|
||
gen_started: null,
|
||
gen_finished: null,
|
||
extra: {
|
||
bias: extractMessageBias(value),
|
||
gen_id: Date.now(),
|
||
api: 'manual',
|
||
model: 'slash command',
|
||
},
|
||
});
|
||
|
||
const newSwipeId = lastMessage.swipes.length - 1;
|
||
|
||
if (isTrueBoolean(args.switch)) {
|
||
lastMessage.swipe_id = newSwipeId;
|
||
lastMessage.mes = lastMessage.swipes[newSwipeId];
|
||
}
|
||
|
||
await saveChatConditional();
|
||
await reloadCurrentChat();
|
||
|
||
return String(newSwipeId);
|
||
}
|
||
|
||
async function deleteSwipeCallback(_, arg) {
|
||
// Take the provided argument. Null if none provided, which will target the current swipe.
|
||
const swipeId = arg && !isNaN(Number(arg)) ? (Number(arg) - 1) : null;
|
||
|
||
const newSwipeId = await deleteSwipe(swipeId);
|
||
|
||
return String(newSwipeId);
|
||
}
|
||
|
||
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.warning('Cannot run /ask command in a group chat!');
|
||
return '';
|
||
}
|
||
|
||
if (!args.name) {
|
||
toastr.warning('You must specify a name of the character to ask.');
|
||
return '';
|
||
}
|
||
|
||
const prevChId = this_chid;
|
||
|
||
// Find the character
|
||
const character = findChar({ name: args?.name });
|
||
if (!character) {
|
||
toastr.error('Character not found.');
|
||
return '';
|
||
}
|
||
|
||
const chId = getCharIndex(character);
|
||
|
||
if (text) {
|
||
const mesText = getRegexedString(text.trim(), regex_placement.SLASH_COMMAND);
|
||
// Sending a message implicitly saves the chat, so this needs to be done before changing the character
|
||
// Otherwise, a corruption will occur
|
||
await sendMessageAsUser(mesText, '');
|
||
}
|
||
|
||
// Override character and send a user message
|
||
setCharacterId(String(chId));
|
||
|
||
const { name, force_avatar, original_avatar } = getNameAndAvatarForMessage(character, args?.name);
|
||
|
||
setCharacterName(name);
|
||
|
||
const restoreCharacter = () => {
|
||
if (String(this_chid) !== String(chId)) {
|
||
return;
|
||
}
|
||
|
||
if (prevChId !== undefined) {
|
||
setCharacterId(prevChId);
|
||
setCharacterName(characters[prevChId].name);
|
||
} else {
|
||
setCharacterId(undefined);
|
||
setCharacterName(neutralCharacterName);
|
||
}
|
||
|
||
// 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 === name) {
|
||
lastMessage.force_avatar = force_avatar;
|
||
lastMessage.original_avatar = original_avatar;
|
||
}
|
||
};
|
||
|
||
let askResult = '';
|
||
|
||
// Run generate and restore previous character
|
||
try {
|
||
eventSource.once(event_types.MESSAGE_RECEIVED, restoreCharacter);
|
||
toastr.info(`Asking ${name} something...`);
|
||
askResult = await Generate('ask_command');
|
||
} catch (error) {
|
||
restoreCharacter();
|
||
console.error('Error running /ask command', error);
|
||
} finally {
|
||
if (String(this_chid) === String(prevChId)) {
|
||
await saveChatConditional();
|
||
} else {
|
||
toastr.error('It is strongly recommended to reload the page.', 'Something went wrong');
|
||
}
|
||
}
|
||
|
||
const message = askResult ? chat[chat.length - 1] : null;
|
||
|
||
return await slashCommandReturnHelper.doReturn(args.return ?? 'pipe', message, { objectToStringFunc: x => x.mes });
|
||
}
|
||
|
||
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(_, name) {
|
||
if (!selected_group) {
|
||
toastr.warning('Cannot run /memberadd command outside of a group chat.');
|
||
return '';
|
||
}
|
||
|
||
if (!name) {
|
||
console.warn('WARN: No argument provided for /memberadd command');
|
||
return '';
|
||
}
|
||
|
||
const character = findChar({ name: name, preferCurrentChar: false });
|
||
if (!character) {
|
||
console.warn(`WARN: No character found for argument ${name}`);
|
||
return '';
|
||
}
|
||
|
||
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.');
|
||
outerResolve(Promise.resolve(''));
|
||
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 (equalsIgnoreCaseAndAccents(persona[1], name)) {
|
||
return persona[0];
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
async function sendUserMessageCallback(args, text) {
|
||
if (!text) {
|
||
toastr.warning('You must specify text to send');
|
||
return;
|
||
}
|
||
|
||
text = text.trim();
|
||
const compact = isTrueBoolean(args?.compact);
|
||
const bias = extractMessageBias(text);
|
||
|
||
let insertAt = Number(args?.at);
|
||
|
||
// Convert possible depth parameter to index
|
||
if (!isNaN(insertAt) && (insertAt < 0 || Object.is(insertAt, -0))) {
|
||
// Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)
|
||
insertAt = chat.length + insertAt;
|
||
}
|
||
|
||
let message;
|
||
if ('name' in args) {
|
||
const name = args.name || '';
|
||
const avatar = findPersonaByName(name) || user_avatar;
|
||
message = await sendMessageAsUser(text, bias, insertAt, compact, name, avatar);
|
||
}
|
||
else {
|
||
message = await sendMessageAsUser(text, bias, insertAt, compact);
|
||
}
|
||
|
||
return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
|
||
}
|
||
|
||
async function deleteMessagesByNameCallback(_, name) {
|
||
if (!name) {
|
||
console.warn('WARN: No name provided for /delname command');
|
||
return;
|
||
}
|
||
|
||
// Search for a matching character to get the real name, or take the name provided
|
||
const character = findChar({ name: name });
|
||
name = character?.name || name;
|
||
|
||
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 '';
|
||
}
|
||
|
||
async function goToCharacterCallback(_, name) {
|
||
if (!name) {
|
||
console.warn('WARN: No character name provided for /go command');
|
||
return;
|
||
}
|
||
|
||
const character = findChar({ name: name });
|
||
if (character) {
|
||
const chid = getCharIndex(character);
|
||
await openChat(new String(chid));
|
||
setActiveCharacter(character.avatar);
|
||
setActiveGroup(null);
|
||
return character.name;
|
||
}
|
||
const group = groups.find(it => equalsIgnoreCaseAndAccents(it.name, name));
|
||
if (group) {
|
||
await openGroupById(group.id);
|
||
setActiveCharacter(null);
|
||
setActiveGroup(group.id);
|
||
return group.name;
|
||
}
|
||
console.warn(`No matches found for name "${name}"`);
|
||
return '';
|
||
}
|
||
|
||
async function openChat(chid) {
|
||
resetSelectedGroup();
|
||
setCharacterId(chid);
|
||
await delay(1);
|
||
await reloadCurrentChat();
|
||
}
|
||
|
||
async function continueChatCallback(args, prompt) {
|
||
const shouldAwait = isTrueBoolean(args?.await);
|
||
|
||
const outerPromise = new Promise(async (resolve, reject) => {
|
||
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.');
|
||
return reject();
|
||
}
|
||
|
||
try {
|
||
// Prevent infinite recursion
|
||
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
|
||
|
||
const options = prompt?.trim() ? { quiet_prompt: prompt.trim(), quietToLoud: true } : {};
|
||
await Generate('continue', options);
|
||
|
||
resolve();
|
||
} catch (error) {
|
||
console.error('Error running /continue command:', error);
|
||
reject(error);
|
||
}
|
||
});
|
||
|
||
if (shouldAwait) {
|
||
await outerPromise;
|
||
}
|
||
|
||
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 '';
|
||
}
|
||
|
||
/**
|
||
* Checks if an argument is a string array (or undefined), and if not, throws an error
|
||
* @param {string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined} arg The named argument to check
|
||
* @param {string} name The name of the argument for the error message
|
||
* @param {object} [options={}] - The optional arguments
|
||
* @param {boolean} [options.allowUndefined=false] - Whether the argument can be undefined
|
||
* @throws {Error} If the argument is not an array
|
||
* @returns {string[]}
|
||
*/
|
||
export function validateArrayArgString(arg, name, { allowUndefined = true } = {}) {
|
||
if (arg === undefined) {
|
||
if (allowUndefined) return undefined;
|
||
throw new Error(`Argument "${name}" is undefined, but must be a string array`);
|
||
}
|
||
if (!Array.isArray(arg)) throw new Error(`Argument "${name}" must be an array`);
|
||
if (!arg.every(x => typeof x === 'string')) throw new Error(`Argument "${name}" must be an array of strings`);
|
||
return arg;
|
||
}
|
||
|
||
/**
|
||
* Checks if an argument is a string or closure array (or undefined), and if not, throws an error
|
||
* @param {string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined} arg The named argument to check
|
||
* @param {string} name The name of the argument for the error message
|
||
* @param {object} [options={}] - The optional arguments
|
||
* @param {boolean} [options.allowUndefined=false] - Whether the argument can be undefined
|
||
* @throws {Error} If the argument is not an array of strings or closures
|
||
* @returns {(string|SlashCommandClosure)[]}
|
||
*/
|
||
export function validateArrayArg(arg, name, { allowUndefined = true } = {}) {
|
||
if (arg === undefined) {
|
||
if (allowUndefined) return [];
|
||
throw new Error(`Argument "${name}" is undefined, but must be an array of strings or closures`);
|
||
}
|
||
if (!Array.isArray(arg)) throw new Error(`Argument "${name}" must be an array`);
|
||
if (!arg.every(x => typeof x === 'string' || x instanceof SlashCommandClosure)) throw new Error(`Argument "${name}" must be an array of strings or closures`);
|
||
return arg;
|
||
}
|
||
|
||
|
||
/**
|
||
* Retrieves the name and avatar information for a message
|
||
*
|
||
* The name of the character will always have precendence over the one given as argument. If you want to specify a different name for the message,
|
||
* explicitly implement this in the code using this.
|
||
*
|
||
* @param {object?} character - The character object to get the avatar data for
|
||
* @param {string?} name - The name to get the avatar data for
|
||
* @returns {{name: string, force_avatar: string, original_avatar: string}} An object containing the name for the message, forced avatar URL, and original avatar
|
||
*/
|
||
export function getNameAndAvatarForMessage(character, name = null) {
|
||
const isNeutralCharacter = !character && name2 === neutralCharacterName && name === neutralCharacterName;
|
||
const currentChar = characters[this_chid];
|
||
|
||
let force_avatar, original_avatar;
|
||
if (character?.avatar === currentChar?.avatar || isNeutralCharacter) {
|
||
// If the targeted character is the currently selected one in a solo chat, we don't need to force any avatars
|
||
}
|
||
else if (character && character.avatar !== 'none') {
|
||
force_avatar = getThumbnailUrl('avatar', character.avatar);
|
||
original_avatar = character.avatar;
|
||
}
|
||
else {
|
||
force_avatar = default_avatar;
|
||
original_avatar = default_avatar;
|
||
}
|
||
|
||
return {
|
||
name: character?.name || name,
|
||
force_avatar: force_avatar,
|
||
original_avatar: original_avatar,
|
||
};
|
||
}
|
||
|
||
export async function sendMessageAs(args, text) {
|
||
if (!text) {
|
||
toastr.warning('You must specify text to send as');
|
||
return '';
|
||
}
|
||
|
||
let name = args.name?.trim();
|
||
let mesText;
|
||
|
||
if (!name) {
|
||
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;
|
||
}
|
||
|
||
mesText = text.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 = findChar({ name: name });
|
||
|
||
const avatarCharacter = args.avatar ? findChar({ name: args.avatar }) : character;
|
||
if (args.avatar && !avatarCharacter) {
|
||
toastr.warning(`Character for avatar ${args.avatar} not found`);
|
||
return '';
|
||
}
|
||
|
||
const { name: avatarCharName, force_avatar, original_avatar } = getNameAndAvatarForMessage(avatarCharacter, name);
|
||
|
||
const message = {
|
||
name: character?.name || name || avatarCharName,
|
||
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',
|
||
},
|
||
}];
|
||
|
||
let insertAt = Number(args.at);
|
||
|
||
// Convert possible depth parameter to index
|
||
if (!isNaN(insertAt) && (insertAt < 0 || Object.is(insertAt, -0))) {
|
||
// Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)
|
||
insertAt = chat.length + insertAt;
|
||
}
|
||
|
||
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 await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
|
||
}
|
||
|
||
export async function sendNarratorMessage(args, text) {
|
||
if (!text) {
|
||
toastr.warning('You must specify text to send');
|
||
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',
|
||
},
|
||
};
|
||
|
||
let insertAt = Number(args.at);
|
||
|
||
// Convert possible depth parameter to index
|
||
if (!isNaN(insertAt) && (insertAt < 0 || Object.is(insertAt, -0))) {
|
||
// Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)
|
||
insertAt = chat.length + insertAt;
|
||
}
|
||
|
||
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 await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
|
||
}
|
||
|
||
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) {
|
||
toastr.warning('You must specify text to send');
|
||
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',
|
||
},
|
||
};
|
||
|
||
let insertAt = Number(args.at);
|
||
|
||
// Convert possible depth parameter to index
|
||
if (!isNaN(insertAt) && (insertAt < 0 || Object.is(insertAt, -0))) {
|
||
// Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)
|
||
insertAt = chat.length + insertAt;
|
||
}
|
||
|
||
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 await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
|
||
}
|
||
|
||
/**
|
||
* 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
|
||
* @param {boolean} quiet - Whether to suppress toasts
|
||
*
|
||
* @returns {{control: HTMLSelectElement|HTMLInputElement, options: HTMLOptionElement[]}?} An array of objects representing the available model options, or null if not supported
|
||
*/
|
||
function getModelOptions(quiet) {
|
||
const nullResult = { control: null, options: null };
|
||
const modelSelectMap = [
|
||
{ id: 'generic_model_textgenerationwebui', api: 'textgenerationwebui', type: textgen_types.GENERIC },
|
||
{ id: 'custom_model_textgenerationwebui', api: 'textgenerationwebui', type: textgen_types.OOBA },
|
||
{ 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: 'tabby_model', api: 'textgenerationwebui', type: textgen_types.TABBY },
|
||
{ id: 'featherless_model', api: 'textgenerationwebui', type: textgen_types.FEATHERLESS },
|
||
{ 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: 'custom_model_id', 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_nanogpt_select', api: 'openai', type: chat_completion_sources.NANOGPT },
|
||
{ id: 'model_01ai_select', api: 'openai', type: chat_completion_sources.ZEROONEAI },
|
||
{ id: 'model_blockentropy_select', api: 'openai', type: chat_completion_sources.BLOCKENTROPY },
|
||
{ 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) {
|
||
!quiet && toastr.info('Setting a model for your API is not supported or not implemented yet.');
|
||
return nullResult;
|
||
}
|
||
|
||
const modelSelectControl = document.getElementById(modelSelectItem);
|
||
|
||
if (!(modelSelectControl instanceof HTMLSelectElement) && !(modelSelectControl instanceof HTMLInputElement)) {
|
||
!quiet && toastr.error(`Model select control not found: ${main_api}[${apiSubType}]`);
|
||
return nullResult;
|
||
}
|
||
|
||
/**
|
||
* Get options from a HTMLSelectElement or HTMLInputElement with a list.
|
||
* @param {HTMLSelectElement | HTMLInputElement} control Control containing the options
|
||
* @returns {HTMLOptionElement[]} Array of options
|
||
*/
|
||
const getOptions = (control) => {
|
||
if (control instanceof HTMLSelectElement) {
|
||
return Array.from(control.options);
|
||
}
|
||
|
||
const valueOption = new Option(control.value, control.value);
|
||
|
||
if (control instanceof HTMLInputElement && control.list instanceof HTMLDataListElement) {
|
||
return [valueOption, ...Array.from(control.list.options)];
|
||
}
|
||
|
||
return [valueOption];
|
||
};
|
||
|
||
const options = getOptions(modelSelectControl).filter(x => x.value).filter(onlyUnique);
|
||
return { control: modelSelectControl, options };
|
||
}
|
||
|
||
/**
|
||
* Sets a model for the current API.
|
||
* @param {object} args Named arguments
|
||
* @param {string} model New model name
|
||
* @returns {string} New or existing model name
|
||
*/
|
||
function modelCallback(args, model) {
|
||
const quiet = isTrueBoolean(args?.quiet);
|
||
const { control: modelSelectControl, options } = getModelOptions(quiet);
|
||
|
||
// If no model was found, the reason was already logged, we just return here
|
||
if (options === null) {
|
||
return '';
|
||
}
|
||
|
||
model = String(model || '').trim();
|
||
|
||
if (!model) {
|
||
return modelSelectControl.value;
|
||
}
|
||
|
||
console.log('Set model to ' + model);
|
||
|
||
if (modelSelectControl instanceof HTMLInputElement) {
|
||
modelSelectControl.value = model;
|
||
$(modelSelectControl).trigger('input');
|
||
!quiet && toastr.success(`Model set to "${model}"`);
|
||
return model;
|
||
}
|
||
|
||
if (!options.length) {
|
||
!quiet && toastr.warning('No model options found. Check your API settings.');
|
||
return '';
|
||
}
|
||
|
||
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');
|
||
!quiet && toastr.success(`Model set to "${newSelectedOption.text}"`);
|
||
return newSelectedOption.value;
|
||
} else {
|
||
!quiet && toastr.warning(`No model found with name "${model}"`);
|
||
return '';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Gets the 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} args.return The type of return value to use (simple, list, dict)
|
||
* @returns {Object} An object containing the states of the requested prompt entries
|
||
*/
|
||
function getPromptEntryCallback(args) {
|
||
const prompts = promptManager.serviceSettings.prompts;
|
||
let returnType = args.return ?? 'simple';
|
||
|
||
function parseArgs(arg) {
|
||
// Arg is already an array
|
||
if (Array.isArray(arg)) {
|
||
return arg;
|
||
}
|
||
const list = [];
|
||
try {
|
||
// Arg is a JSON-stringified array
|
||
const parsedArg = JSON.parse(arg);
|
||
list.push(...Array.isArray(parsedArg) ? parsedArg : [arg]);
|
||
} catch {
|
||
// Arg is a string
|
||
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 => {
|
||
let identifiers = prompts
|
||
.filter(entry => entry.name === name)
|
||
.map(entry => entry.identifier);
|
||
identifiersList = identifiersList.concat(identifiers);
|
||
});
|
||
}
|
||
|
||
// Get the state for each prompt entry
|
||
let promptStates = new Map();
|
||
identifiersList.forEach(identifier => {
|
||
const promptOrderEntry = promptManager.getPromptOrderEntry(promptManager.activeCharacter, identifier);
|
||
if (promptOrderEntry) {
|
||
promptStates.set(identifier, promptOrderEntry.enabled);
|
||
}
|
||
});
|
||
|
||
// If return is simple (default) but more than one prompt state was retrieved, then change return type
|
||
if (returnType === 'simple' && promptStates.size > 1) {
|
||
returnType = args.identifier ? 'dict' : 'list';
|
||
}
|
||
|
||
const result = (() => {
|
||
if (returnType === 'list') return [...promptStates.values()];
|
||
if (returnType === 'dict') return Object.fromEntries(promptStates);
|
||
return [...promptStates.values()][0];
|
||
})();
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 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 prompts = promptManager.serviceSettings.prompts;
|
||
|
||
function parseArgs(arg) {
|
||
// Arg is already an array
|
||
if (Array.isArray(arg)) {
|
||
return arg;
|
||
}
|
||
const list = [];
|
||
try {
|
||
// Arg is a JSON-stringified array
|
||
const parsedArg = JSON.parse(arg);
|
||
list.push(...Array.isArray(parsedArg) ? parsedArg : [arg]);
|
||
} catch {
|
||
// Arg is a string
|
||
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 '';
|
||
}
|
||
|
||
/**
|
||
* Sets the API URL and triggers the text generation web UI button click.
|
||
*
|
||
* @param {object} args - named args
|
||
* @param {string?} [args.api=null] - the API name to set/get the URL for
|
||
* @param {string?} [args.connect=true] - whether to connect to the API after setting
|
||
* @param {string?} [args.quiet=false] - whether to suppress toasts
|
||
* @param {string} url - the API URL to set
|
||
* @returns {Promise<string>}
|
||
*/
|
||
async function setApiUrlCallback({ api = null, connect = 'true', quiet = 'false' }, url) {
|
||
const isQuiet = isTrueBoolean(quiet);
|
||
const autoConnect = isTrueBoolean(connect);
|
||
|
||
// Special handling for Chat Completion Custom OpenAI compatible, that one can also support API url handling
|
||
const isCurrentlyCustomOpenai = main_api === 'openai' && oai_settings.chat_completion_source === chat_completion_sources.CUSTOM;
|
||
if (api === chat_completion_sources.CUSTOM || (!api && isCurrentlyCustomOpenai)) {
|
||
if (!url) {
|
||
return oai_settings.custom_url ?? '';
|
||
}
|
||
|
||
if (!isCurrentlyCustomOpenai && autoConnect) {
|
||
toastr.warning('Custom OpenAI API is not the currently selected API, so we cannot do an auto-connect. Consider switching to it via /api beforehand.');
|
||
return '';
|
||
}
|
||
|
||
$('#custom_api_url_text').val(url).trigger('input');
|
||
|
||
if (autoConnect) {
|
||
$('#api_button_openai').trigger('click');
|
||
}
|
||
|
||
return url;
|
||
}
|
||
|
||
// Special handling for Kobold Classic API
|
||
const isCurrentlyKoboldClassic = main_api === 'kobold';
|
||
if (api === 'kobold' || (!api && isCurrentlyKoboldClassic)) {
|
||
if (!url) {
|
||
return api_server ?? '';
|
||
}
|
||
|
||
if (!isCurrentlyKoboldClassic && autoConnect) {
|
||
toastr.warning('Kobold Classic API is not the currently selected API, so we cannot do an auto-connect. Consider switching to it via /api beforehand.');
|
||
return '';
|
||
}
|
||
|
||
$('#api_url_text').val(url).trigger('input');
|
||
// trigger blur debounced, so we hide the autocomplete menu
|
||
setTimeout(() => $('#api_url_text').trigger('blur'), 1);
|
||
|
||
if (autoConnect) {
|
||
$('#api_button').trigger('click');
|
||
}
|
||
|
||
return api_server ?? '';
|
||
}
|
||
|
||
// Do some checks and get the api type we are targeting with this command
|
||
if (api && !Object.values(textgen_types).includes(api)) {
|
||
!isQuiet && toastr.warning(`API '${api}' is not a valid text_gen API.`);
|
||
return '';
|
||
}
|
||
if (!api && !Object.values(textgen_types).includes(textgenerationwebui_settings.type)) {
|
||
!isQuiet && toastr.warning(`API '${textgenerationwebui_settings.type}' is not a valid text_gen API.`);
|
||
return '';
|
||
}
|
||
if (!api && main_api !== 'textgenerationwebui') {
|
||
!isQuiet && toastr.warning(`API type '${main_api}' does not support setting the server URL.`);
|
||
return '';
|
||
}
|
||
if (api && url && autoConnect && api !== textgenerationwebui_settings.type) {
|
||
!isQuiet && toastr.warning(`API '${api}' is not the currently selected API, so we cannot do an auto-connect. Consider switching to it via /api beforehand.`);
|
||
return '';
|
||
}
|
||
const type = api || textgenerationwebui_settings.type;
|
||
|
||
const inputSelector = SERVER_INPUTS[type];
|
||
if (!inputSelector) {
|
||
!isQuiet && toastr.warning(`API '${type}' does not have a server url input.`);
|
||
return '';
|
||
}
|
||
|
||
// If no url was provided, return the current one
|
||
if (!url) {
|
||
return textgenerationwebui_settings.server_urls[type] ?? '';
|
||
}
|
||
|
||
// else, we want to actually set the url
|
||
$(inputSelector).val(url).trigger('input');
|
||
// trigger blur debounced, so we hide the autocomplete menu
|
||
setTimeout(() => $(inputSelector).trigger('blur'), 1);
|
||
|
||
// Trigger the auto connect via connect button, if requested
|
||
if (autoConnect) {
|
||
$('#api_button_textgenerationwebui').trigger('click');
|
||
}
|
||
|
||
// We still re-acquire the value, as it might have been modified by the validation on connect
|
||
return textgenerationwebui_settings.server_urls[type] ?? '';
|
||
}
|
||
|
||
async function selectTokenizerCallback(_, name) {
|
||
if (!name) {
|
||
return getAvailableTokenizers().find(tokenizer => tokenizer.tokenizerId === power_user.tokenizer)?.tokenizerKey ?? '';
|
||
}
|
||
|
||
const tokenizers = getAvailableTokenizers();
|
||
const fuse = new Fuse(tokenizers, { keys: ['tokenizerKey', 'tokenizerName'] });
|
||
const result = fuse.search(name);
|
||
|
||
if (result.length === 0) {
|
||
toastr.warning(`Tokenizer "${name}" not found`);
|
||
return '';
|
||
}
|
||
|
||
/** @type {import('./tokenizers.js').Tokenizer} */
|
||
const foundTokenizer = result[0].item;
|
||
selectTokenizer(foundTokenizer.tokenizerId);
|
||
|
||
return foundTokenizer.tokenizerKey;
|
||
}
|
||
|
||
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<void>
|
||
*/
|
||
async function clearCommandProgress() {
|
||
if (isExecutingCommandsFromChatInput) return;
|
||
document.querySelector('#send_textarea').style.setProperty('--progDone', '1');
|
||
await delay(250);
|
||
if (isExecutingCommandsFromChatInput) return;
|
||
document.querySelector('#send_textarea').style.transition = 'none';
|
||
await delay(1);
|
||
document.querySelector('#send_textarea').style.setProperty('--prog', '0%');
|
||
document.querySelector('#send_textarea').style.setProperty('--progDone', '0');
|
||
document.querySelector('#form_sheld').classList.remove('script_success');
|
||
document.querySelector('#form_sheld').classList.remove('script_error');
|
||
document.querySelector('#form_sheld').classList.remove('script_aborted');
|
||
await delay(1);
|
||
document.querySelector('#send_textarea').style.transition = null;
|
||
}
|
||
/**
|
||
* Debounced version of clearCommandProgress.
|
||
*/
|
||
const clearCommandProgressDebounced = debounce(clearCommandProgress);
|
||
|
||
/**
|
||
* @typedef ExecuteSlashCommandsOptions
|
||
* @prop {boolean} [handleParserErrors] (true) Whether to handle parser errors (show toast on error) or throw.
|
||
* @prop {SlashCommandScope} [scope] (null) The scope to be used when executing the commands.
|
||
* @prop {boolean} [handleExecutionErrors] (false) Whether to handle execution errors (show toast on error) or throw
|
||
* @prop {{[id:PARSER_FLAG]:boolean}} [parserFlags] (null) Parser flags to apply
|
||
* @prop {SlashCommandAbortController} [abortController] (null) Controller used to abort or pause command execution
|
||
* @prop {SlashCommandDebugController} [debugController] (null) Controller used to control debug execution
|
||
* @prop {(done:number, total:number)=>void} [onProgress] (null) Callback to handle progress events
|
||
* @prop {string} [source] (null) String indicating where the code come from (e.g., QR name)
|
||
*/
|
||
|
||
/**
|
||
* @typedef ExecuteSlashCommandsOnChatInputOptions
|
||
* @prop {SlashCommandScope} [scope] (null) The scope to be used when executing the commands.
|
||
* @prop {{[id:PARSER_FLAG]:boolean}} [parserFlags] (null) Parser flags to apply
|
||
* @prop {boolean} [clearChatInput] (false) Whether to clear the chat input textarea
|
||
* @prop {string} [source] (null) String indicating where the code come from (e.g., QR name)
|
||
*/
|
||
|
||
/**
|
||
* Execute slash commands while showing progress indicator and pause/stop buttons on
|
||
* chat input.
|
||
* @param {string} text Slash command text
|
||
* @param {ExecuteSlashCommandsOnChatInputOptions} options
|
||
*/
|
||
export async function executeSlashCommandsOnChatInput(text, options = {}) {
|
||
if (isExecutingCommandsFromChatInput) return null;
|
||
|
||
options = Object.assign({
|
||
scope: null,
|
||
parserFlags: null,
|
||
clearChatInput: false,
|
||
source: null,
|
||
}, options);
|
||
|
||
isExecutingCommandsFromChatInput = true;
|
||
commandsFromChatInputAbortController?.abort('processCommands was called');
|
||
activateScriptButtons();
|
||
|
||
/**@type {HTMLTextAreaElement}*/
|
||
const ta = document.querySelector('#send_textarea');
|
||
|
||
if (options.clearChatInput) {
|
||
ta.value = '';
|
||
ta.dispatchEvent(new Event('input', { bubbles: true }));
|
||
}
|
||
|
||
document.querySelector('#send_textarea').style.setProperty('--prog', '0%');
|
||
document.querySelector('#send_textarea').style.setProperty('--progDone', '0');
|
||
document.querySelector('#form_sheld').classList.remove('script_success');
|
||
document.querySelector('#form_sheld').classList.remove('script_error');
|
||
document.querySelector('#form_sheld').classList.remove('script_aborted');
|
||
|
||
/**@type {SlashCommandClosureResult} */
|
||
let result = null;
|
||
let currentProgress = 0;
|
||
try {
|
||
commandsFromChatInputAbortController = new SlashCommandAbortController();
|
||
result = await executeSlashCommandsWithOptions(text, {
|
||
abortController: commandsFromChatInputAbortController,
|
||
onProgress: (done, total) => {
|
||
const newProgress = done / total;
|
||
if (newProgress > currentProgress) {
|
||
currentProgress = newProgress;
|
||
ta.style.setProperty('--prog', `${newProgress * 100}%`);
|
||
}
|
||
},
|
||
parserFlags: options.parserFlags,
|
||
scope: options.scope,
|
||
source: options.source,
|
||
});
|
||
if (commandsFromChatInputAbortController.signal.aborted) {
|
||
document.querySelector('#form_sheld').classList.add('script_aborted');
|
||
} else {
|
||
document.querySelector('#form_sheld').classList.add('script_success');
|
||
}
|
||
} catch (e) {
|
||
document.querySelector('#form_sheld').classList.add('script_error');
|
||
result = new SlashCommandClosureResult();
|
||
result.isError = true;
|
||
result.errorMessage = e.message || 'An unknown error occurred';
|
||
if (e.cause !== 'abort') {
|
||
if (e instanceof SlashCommandExecutionError) {
|
||
/**@type {SlashCommandExecutionError}*/
|
||
const ex = e;
|
||
const toast = `
|
||
<div>${ex.message}</div>
|
||
<div>Line: ${ex.line} Column: ${ex.column}</div>
|
||
<pre style="text-align:left;">${ex.hint}</pre>
|
||
`;
|
||
const clickHint = '<p>Click to see details</p>';
|
||
toastr.error(
|
||
`${toast}${clickHint}`,
|
||
'SlashCommandExecutionError',
|
||
{ escapeHtml: false, timeOut: 10000, onclick: () => callPopup(toast, 'text') },
|
||
);
|
||
} else {
|
||
toastr.error(result.errorMessage);
|
||
}
|
||
}
|
||
} finally {
|
||
delay(1000).then(() => clearCommandProgressDebounced());
|
||
|
||
commandsFromChatInputAbortController = null;
|
||
deactivateScriptButtons();
|
||
isExecutingCommandsFromChatInput = false;
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {string} text Slash command text
|
||
* @param {ExecuteSlashCommandsOptions} [options]
|
||
* @returns {Promise<SlashCommandClosureResult>}
|
||
*/
|
||
async function executeSlashCommandsWithOptions(text, options = {}) {
|
||
if (!text) {
|
||
return null;
|
||
}
|
||
options = Object.assign({
|
||
handleParserErrors: true,
|
||
scope: null,
|
||
handleExecutionErrors: false,
|
||
parserFlags: null,
|
||
abortController: null,
|
||
debugController: null,
|
||
onProgress: null,
|
||
source: null,
|
||
}, options);
|
||
|
||
let closure;
|
||
try {
|
||
closure = parser.parse(text, true, options.parserFlags, options.abortController ?? new SlashCommandAbortController());
|
||
closure.scope.parent = options.scope;
|
||
closure.onProgress = options.onProgress;
|
||
closure.debugController = options.debugController;
|
||
closure.source = options.source;
|
||
} catch (e) {
|
||
if (options.handleParserErrors && e instanceof SlashCommandParserError) {
|
||
/**@type {SlashCommandParserError}*/
|
||
const ex = e;
|
||
const toast = `
|
||
<div>${ex.message}</div>
|
||
<div>Line: ${ex.line} Column: ${ex.column}</div>
|
||
<pre style="text-align:left;">${ex.hint}</pre>
|
||
`;
|
||
const clickHint = '<p>Click to see details</p>';
|
||
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) {
|
||
if (e instanceof SlashCommandExecutionError) {
|
||
/**@type {SlashCommandExecutionError}*/
|
||
const ex = e;
|
||
const toast = `
|
||
<div>${ex.message}</div>
|
||
<div>Line: ${ex.line} Column: ${ex.column}</div>
|
||
<pre style="text-align:left;">${ex.hint}</pre>
|
||
`;
|
||
const clickHint = '<p>Click to see details</p>';
|
||
toastr.error(
|
||
`${toast}${clickHint}`,
|
||
'SlashCommandExecutionError',
|
||
{ escapeHtml: false, timeOut: 10000, onclick: () => callPopup(toast, 'text') },
|
||
);
|
||
} else {
|
||
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<SlashCommandClosureResult>}
|
||
*/
|
||
async function executeSlashCommands(text, handleParserErrors = true, scope = null, handleExecutionErrors = false, parserFlags = null, abortController = null, onProgress = null) {
|
||
return executeSlashCommandsWithOptions(text, {
|
||
handleParserErrors,
|
||
scope,
|
||
handleExecutionErrors,
|
||
parserFlags,
|
||
abortController,
|
||
onProgress,
|
||
});
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @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)
|
||
* @returns {Promise<AutoComplete>}
|
||
*/
|
||
export async function setSlashCommandAutoComplete(textarea, isFloating = false) {
|
||
function canUseNegativeLookbehind() {
|
||
try {
|
||
new RegExp('(?<!_)');
|
||
return true;
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (!canUseNegativeLookbehind()) {
|
||
console.warn('Cannot use negative lookbehind in this browser');
|
||
return;
|
||
}
|
||
|
||
const parser = new SlashCommandParser();
|
||
const ac = new AutoComplete(
|
||
textarea,
|
||
() => ac.text[0] == '/',
|
||
async (text, index) => await parser.getNameAt(text, index),
|
||
isFloating,
|
||
);
|
||
return ac;
|
||
}
|
||
/**@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;
|
||
}
|
||
});
|