3437 lines
117 KiB
JavaScript
3437 lines
117 KiB
JavaScript
import {
|
||
Generate,
|
||
activateSendButtons,
|
||
addOneMessage,
|
||
callPopup,
|
||
characters,
|
||
chat,
|
||
chat_metadata,
|
||
comment_avatar,
|
||
deactivateSendButtons,
|
||
default_avatar,
|
||
eventSource,
|
||
event_types,
|
||
extension_prompt_roles,
|
||
extension_prompt_types,
|
||
extractMessageBias,
|
||
generateQuietPrompt,
|
||
generateRaw,
|
||
getThumbnailUrl,
|
||
is_send_press,
|
||
main_api,
|
||
name1,
|
||
name2,
|
||
reloadCurrentChat,
|
||
removeMacros,
|
||
renameCharacter,
|
||
saveChatConditional,
|
||
sendMessageAsUser,
|
||
sendSystemMessage,
|
||
setActiveCharacter,
|
||
setActiveGroup,
|
||
setCharacterId,
|
||
setCharacterName,
|
||
setExtensionPrompt,
|
||
setUserName,
|
||
substituteParams,
|
||
system_avatar,
|
||
system_message_types,
|
||
this_chid,
|
||
} from '../script.js';
|
||
import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||
import { SlashCommandParserError } from './slash-commands/SlashCommandParserError.js';
|
||
import { getMessageTimeStamp } from './RossAscends-mods.js';
|
||
import { hideChatMessageRange } from './chats.js';
|
||
import { extension_settings, getContext, saveMetadataDebounced } from './extensions.js';
|
||
import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
|
||
import { findGroupMemberId, getGroupMembers, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group } from './group-chats.js';
|
||
import { chat_completion_sources, oai_settings, setupChatCompletionPromptManager } from './openai.js';
|
||
import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockState, togglePersonaLock, user_avatar } from './personas.js';
|
||
import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
|
||
import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
|
||
import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync } from './tokenizers.js';
|
||
import { debounce, delay, isFalseBoolean, isTrueBoolean, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
|
||
import { registerVariableCommands, resolveVariable } from './variables.js';
|
||
import { background_settings } from './backgrounds.js';
|
||
import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
|
||
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
|
||
import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js';
|
||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
|
||
import { AutoComplete } from './autocomplete/AutoComplete.js';
|
||
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||
import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js';
|
||
import { SlashCommandNamedArgumentAssignment } from './slash-commands/SlashCommandNamedArgumentAssignment.js';
|
||
import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
|
||
import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
|
||
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
|
||
export {
|
||
executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
|
||
};
|
||
|
||
export const parser = new SlashCommandParser();
|
||
/**
|
||
* @deprecated Use SlashCommandParser.addCommandObject() instead
|
||
*/
|
||
const registerSlashCommand = SlashCommandParser.addCommand.bind(SlashCommandParser);
|
||
const getSlashCommandsHelp = parser.getHelpString.bind(parser);
|
||
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: '?',
|
||
callback: helpCommandCallback,
|
||
aliases: ['help'],
|
||
unnamedArgumentList: [SlashCommandArgument.fromProps({
|
||
description: 'help topic',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
enumList: [
|
||
new SlashCommandEnumValue('slash', 'slash commands (STscript)', enumTypes.command, '/'),
|
||
new SlashCommandEnumValue('macros', '{{macros}} (text replacement)', enumTypes.macro, enumIcons.macro),
|
||
new SlashCommandEnumValue('format', 'chat/text formatting', enumTypes.name, '★'),
|
||
new SlashCommandEnumValue('hotkeys', 'keyboard shortcuts', enumTypes.enum, '⏎'),
|
||
],
|
||
})],
|
||
helpString: 'Get help on macros, chat formatting and commands.',
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'persona',
|
||
callback: setNameCallback,
|
||
namedArgumentList: [
|
||
new SlashCommandNamedArgument(
|
||
'mode', 'The mode for persona selection. ("lookup" = search for existing persona, "temp" = create a temporary name, set a temporary name, "all" = allow both in the same command)',
|
||
[ARGUMENT_TYPE.STRING], false, false, 'all', ['lookup', 'temp', 'all'],
|
||
),
|
||
],
|
||
unnamedArgumentList: [
|
||
SlashCommandArgument.fromProps({
|
||
description: 'persona name',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
isRequired: true,
|
||
enumProvider: commonEnumProviders.personas,
|
||
}),
|
||
],
|
||
helpString: 'Selects the given persona with its name and avatar (by name or avatar url). If no matching persona exists, applies a temporary name.',
|
||
aliases: ['name'],
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'sync',
|
||
callback: syncCallback,
|
||
helpString: 'Syncs the user persona in user-attributed messages in the current chat.',
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'lock',
|
||
callback: lockPersonaCallback,
|
||
aliases: ['bind'],
|
||
helpString: 'Locks/unlocks a persona (name and avatar) to the current chat',
|
||
unnamedArgumentList: [
|
||
SlashCommandArgument.fromProps({
|
||
description: 'state',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
isRequired: true,
|
||
defaultValue: 'toggle',
|
||
enumProvider: commonEnumProviders.boolean('onOffToggle'),
|
||
}),
|
||
],
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'bg',
|
||
callback: setBackgroundCallback,
|
||
aliases: ['background'],
|
||
returns: 'the current background',
|
||
unnamedArgumentList: [
|
||
SlashCommandArgument.fromProps({
|
||
description: 'filename',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
isRequired: true,
|
||
enumProvider: () => [...document.querySelectorAll('.bg_example')]
|
||
.map(it => new SlashCommandEnumValue(it.getAttribute('bgfile')))
|
||
.filter(it => it.value?.length),
|
||
}),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Sets a background according to the provided filename. Partial names allowed.
|
||
</div>
|
||
<div>
|
||
<strong>Example:</strong>
|
||
<ul>
|
||
<li>
|
||
<pre><code>/bg beach.jpg</code></pre>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'sendas',
|
||
callback: sendMessageAs,
|
||
namedArgumentList: [
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'name',
|
||
description: 'Character name',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
isRequired: true,
|
||
enumProvider: commonEnumProviders.characters('character'),
|
||
forceEnum: false,
|
||
}),
|
||
new SlashCommandNamedArgument(
|
||
'compact', 'Use compact layout', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',
|
||
),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'at',
|
||
description: 'position to insert the message',
|
||
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
|
||
enumProvider: commonEnumProviders.messages({ allowIdAfter: true, allowVars: 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>
|
||
</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'],
|
||
namedArgumentList: [
|
||
new SlashCommandNamedArgument(
|
||
'compact',
|
||
'compact layout',
|
||
[ARGUMENT_TYPE.BOOLEAN],
|
||
false,
|
||
false,
|
||
'false',
|
||
),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'at',
|
||
description: 'position to insert the message',
|
||
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
|
||
enumProvider: commonEnumProviders.messages({ allowIdAfter: true, allowVars: 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,
|
||
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',
|
||
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
|
||
enumProvider: commonEnumProviders.messages({ allowIdAfter: true, allowVars: 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'],
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'prompt', [ARGUMENT_TYPE.STRING], false,
|
||
),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Continues the last message in the chat, with an optional additional prompt.
|
||
</div>
|
||
<div>
|
||
<strong>Example:</strong>
|
||
<ul>
|
||
<li>
|
||
<pre><code>/continue</code></pre>
|
||
Continues the chat with no additional prompt.
|
||
</li>
|
||
<li>
|
||
<pre><code>/continue Let's explore this further...</code></pre>
|
||
Continues the chat with the provided prompt.
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
`,
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'go',
|
||
callback: goToCharacterCallback,
|
||
unnamedArgumentList: [
|
||
SlashCommandArgument.fromProps({
|
||
description: 'name',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
isRequired: true,
|
||
enumProvider: commonEnumProviders.characters('all'),
|
||
}),
|
||
],
|
||
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,
|
||
namedArgumentList: [
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'name',
|
||
description: 'character name',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
isRequired: true,
|
||
enumProvider: commonEnumProviders.characters('character'),
|
||
}),
|
||
],
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'prompt', [ARGUMENT_TYPE.STRING], true, 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: 'name',
|
||
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,
|
||
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',
|
||
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
|
||
enumProvider: commonEnumProviders.messages({ allowIdAfter: true, allowVars: true }),
|
||
}),
|
||
SlashCommandNamedArgument.fromProps({
|
||
name: 'name',
|
||
description: 'display name',
|
||
typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.VARIABLE_NAME],
|
||
defaultValue: '{{user}}',
|
||
enumProvider: () => [
|
||
...commonEnumProviders.characters('character')(),
|
||
...commonEnumProviders.variables('all')().map(x => { x.description = 'Variable'; return x; }),
|
||
],
|
||
}),
|
||
],
|
||
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',
|
||
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,
|
||
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, '✅'),
|
||
],
|
||
}),
|
||
],
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'text', [ARGUMENT_TYPE.STRING], true,
|
||
),
|
||
],
|
||
helpString: `
|
||
<div>
|
||
Echoes the provided text to a toast message. Useful for pipes debugging.
|
||
</div>
|
||
<div>
|
||
<strong>Example:</strong>
|
||
<ul>
|
||
<li>
|
||
<pre><code>/echo title="My Message" severity=info This is an info message</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 name for instruct mode',
|
||
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,
|
||
aliases: ['swipeadd'],
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'text', [ARGUMENT_TYPE.STRING], true,
|
||
),
|
||
],
|
||
helpString: 'Adds a swipe to the last chat message.',
|
||
}));
|
||
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: 'first matching item',
|
||
namedArgumentList: [
|
||
new SlashCommandNamedArgument(
|
||
'list', 'list of items to match against', [ARGUMENT_TYPE.LIST], true,
|
||
),
|
||
new SlashCommandNamedArgument(
|
||
'threshold', 'fuzzy match threshold (0.0 to 1.0)', [ARGUMENT_TYPE.NUMBER], false, false, '0.4',
|
||
),
|
||
],
|
||
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 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],
|
||
isRequired: true,
|
||
enumProvider: () => [
|
||
...commonEnumProviders.variables('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: [
|
||
new SlashCommandNamedArgument(
|
||
'large', 'show large popup', [ARGUMENT_TYPE.BOOLEAN], false, false, null, commonEnumProviders.boolean('onOff')(),
|
||
),
|
||
new SlashCommandNamedArgument(
|
||
'wide', 'show wide popup', [ARGUMENT_TYPE.BOOLEAN], false, false, null, commonEnumProviders.boolean('onOff')(),
|
||
),
|
||
new SlashCommandNamedArgument(
|
||
'okButton', 'text for the OK button', [ARGUMENT_TYPE.STRING], false,
|
||
),
|
||
],
|
||
unnamedArgumentList: [
|
||
new SlashCommandArgument(
|
||
'text', [ARGUMENT_TYPE.STRING], 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="Submit" Enter some text:</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, ARGUMENT_TYPE.VARIABLE_NAME],
|
||
isRequired: true,
|
||
|
||
enumProvider: () => [
|
||
...commonEnumProviders.injects(),
|
||
...commonEnumProviders.variables('all')().map(x => { x.description = 'Variable'; return x; }),
|
||
],
|
||
}),
|
||
new SlashCommandNamedArgument(
|
||
'position', 'injection position', [ARGUMENT_TYPE.STRING], false, false, 'after', ['before', 'after', 'chat'],
|
||
),
|
||
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',
|
||
),
|
||
],
|
||
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" (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).',
|
||
}));
|
||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||
name: 'listinjects',
|
||
callback: listInjectsCallback,
|
||
helpString: 'Lists all script injections for the current chat.',
|
||
}));
|
||
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, ARGUMENT_TYPE.VARIABLE_NAME],
|
||
defaultValue: '',
|
||
enumProvider: () => [
|
||
...commonEnumProviders.injects(),
|
||
...commonEnumProviders.variables('all')().map(x => { x.description = 'Variable'; return x; }),
|
||
],
|
||
}),
|
||
],
|
||
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',
|
||
unnamedArgumentList: [
|
||
SlashCommandArgument.fromProps({
|
||
description: 'model name',
|
||
typeList: [ARGUMENT_TYPE.STRING],
|
||
enumProvider: () => getModelOptions()?.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: '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 promptManager = setupChatCompletionPromptManager(oai_settings);
|
||
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 promptManager = setupChatCompletionPromptManager(oai_settings);
|
||
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.',
|
||
}));
|
||
|
||
registerVariableCommands();
|
||
|
||
const NARRATOR_NAME_KEY = 'narrator_name';
|
||
const NARRATOR_NAME_DEFAULT = 'System';
|
||
export const COMMENT_NAME_DEFAULT = 'Note';
|
||
const SCRIPT_PROMPT_KEY = 'script_inject_';
|
||
|
||
function injectCallback(args, value) {
|
||
const positions = {
|
||
'before': extension_prompt_types.BEFORE_PROMPT,
|
||
'after': extension_prompt_types.IN_PROMPT,
|
||
'chat': extension_prompt_types.IN_CHAT,
|
||
};
|
||
const roles = {
|
||
'system': extension_prompt_roles.SYSTEM,
|
||
'user': extension_prompt_roles.USER,
|
||
'assistant': extension_prompt_roles.ASSISTANT,
|
||
};
|
||
|
||
const id = resolveVariable(args?.id);
|
||
const ephemeral = isTrueBoolean(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(args?.scan);
|
||
value = value || '';
|
||
|
||
const prefixedId = `${SCRIPT_PROMPT_KEY}${id}`;
|
||
|
||
if (!chat_metadata.script_injects) {
|
||
chat_metadata.script_injects = {};
|
||
}
|
||
|
||
if (value) {
|
||
const inject = { value, position, depth, scan, role };
|
||
chat_metadata.script_injects[id] = inject;
|
||
} else {
|
||
delete chat_metadata.script_injects[id];
|
||
}
|
||
|
||
setExtensionPrompt(prefixedId, value, position, depth, scan, role);
|
||
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);
|
||
saveMetadataDebounced();
|
||
deleted = true;
|
||
};
|
||
eventSource.once(event_types.GENERATION_ENDED, unsetInject);
|
||
eventSource.once(event_types.GENERATION_STOPPED, unsetInject);
|
||
}
|
||
|
||
return '';
|
||
}
|
||
|
||
function listInjectsCallback() {
|
||
if (!chat_metadata.script_injects || !Object.keys(chat_metadata.script_injects).length) {
|
||
toastr.info('No script injections for the current chat');
|
||
return '';
|
||
}
|
||
|
||
const injects = Object.entries(chat_metadata.script_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');
|
||
|
||
const converter = new showdown.Converter();
|
||
const messageText = `### Script injections:\n${injects}`;
|
||
const htmlMessage = DOMPurify.sanitize(converter.makeHtml(messageText));
|
||
|
||
sendSystemMessage(system_message_types.GENERIC, htmlMessage);
|
||
}
|
||
|
||
/**
|
||
* Flushes script injections for the current chat.
|
||
* @param {import('./slash-commands/SlashCommand.js').NamedArguments} args Named arguments
|
||
* @param {string} value Unnamed argument
|
||
* @returns {string} Empty string
|
||
*/
|
||
function flushInjectsCallback(args, value) {
|
||
if (!chat_metadata.script_injects) {
|
||
return '';
|
||
}
|
||
|
||
const idArgument = resolveVariable(value, args._scope);
|
||
|
||
for (const [id, inject] of Object.entries(chat_metadata.script_injects)) {
|
||
if (idArgument && id !== idArgument) {
|
||
continue;
|
||
}
|
||
|
||
const prefixedId = `${SCRIPT_PROMPT_KEY}${id}`;
|
||
setExtensionPrompt(prefixedId, '', inject.position, inject.depth, inject.scan, inject.role);
|
||
delete chat_metadata.script_injects[id];
|
||
}
|
||
|
||
saveMetadataDebounced();
|
||
return '';
|
||
}
|
||
|
||
export function processChatSlashCommands() {
|
||
const context = getContext();
|
||
|
||
if (!(context.chatMetadata.script_injects)) {
|
||
return;
|
||
}
|
||
|
||
for (const id of Object.keys(context.extensionPrompts)) {
|
||
if (!id.startsWith(SCRIPT_PROMPT_KEY)) {
|
||
continue;
|
||
}
|
||
|
||
console.log('Removing script injection', id);
|
||
delete context.extensionPrompts[id];
|
||
}
|
||
|
||
for (const [id, inject] of Object.entries(context.chatMetadata.script_injects)) {
|
||
const prefixedId = `${SCRIPT_PROMPT_KEY}${id}`;
|
||
console.log('Adding script injection', id);
|
||
setExtensionPrompt(prefixedId, inject.value, inject.position, inject.depth, inject.scan, inject.role);
|
||
}
|
||
}
|
||
|
||
function setInputCallback(_, value) {
|
||
$('#send_textarea').val(value || '')[0].dispatchEvent(new Event('input', { bubbles: true }));
|
||
return value;
|
||
}
|
||
|
||
function trimStartCallback(_, value) {
|
||
if (!value) {
|
||
return '';
|
||
}
|
||
|
||
return trimToStartSentence(value);
|
||
}
|
||
|
||
function trimEndCallback(_, value) {
|
||
if (!value) {
|
||
return '';
|
||
}
|
||
|
||
return trimToEndSentence(value);
|
||
}
|
||
|
||
async function trimTokensCallback(arg, value) {
|
||
if (!value) {
|
||
console.warn('WARN: No argument provided for /trimtokens command');
|
||
return '';
|
||
}
|
||
|
||
const limit = Number(resolveVariable(arg.limit));
|
||
|
||
if (isNaN(limit)) {
|
||
console.warn(`WARN: Invalid limit provided for /trimtokens command: ${limit}`);
|
||
return value;
|
||
}
|
||
|
||
if (limit <= 0) {
|
||
return '';
|
||
}
|
||
|
||
const direction = arg.direction || 'end';
|
||
const tokenCount = await getTokenCountAsync(value);
|
||
|
||
// Token count is less than the limit, do nothing
|
||
if (tokenCount <= limit) {
|
||
return value;
|
||
}
|
||
|
||
const { tokenizerName, tokenizerId } = getFriendlyTokenizerName(main_api);
|
||
console.debug('Requesting tokenization for /trimtokens command', tokenizerName);
|
||
|
||
try {
|
||
const textTokens = getTextTokens(tokenizerId, value);
|
||
|
||
if (!Array.isArray(textTokens) || !textTokens.length) {
|
||
console.warn('WARN: No tokens returned for /trimtokens command, falling back to estimation');
|
||
const percentage = limit / tokenCount;
|
||
const trimIndex = Math.floor(value.length * percentage);
|
||
const trimmedText = direction === 'start' ? value.substring(trimIndex) : value.substring(0, value.length - trimIndex);
|
||
return trimmedText;
|
||
}
|
||
|
||
const sliceTokens = direction === 'start' ? textTokens.slice(0, limit) : textTokens.slice(-limit);
|
||
const { text } = decodeTextTokens(tokenizerId, sliceTokens);
|
||
return text;
|
||
} catch (error) {
|
||
console.warn('WARN: Tokenization failed for /trimtokens command, returning original', error);
|
||
return value;
|
||
}
|
||
}
|
||
|
||
async function buttonsCallback(args, text) {
|
||
try {
|
||
/** @type {string[]} */
|
||
const buttons = JSON.parse(resolveVariable(args?.labels));
|
||
|
||
if (!Array.isArray(buttons) || !buttons.length) {
|
||
console.warn('WARN: Invalid labels provided for /buttons command');
|
||
return '';
|
||
}
|
||
|
||
// Map custom buttons to results. Start at 2 because 1 and 0 are reserved for ok and cancel
|
||
const resultToButtonMap = new Map(buttons.map((button, index) => [index + 2, button]));
|
||
|
||
return new Promise(async (resolve) => {
|
||
const safeValue = DOMPurify.sanitize(text || '');
|
||
|
||
/** @type {Popup} */
|
||
let popup;
|
||
|
||
const buttonContainer = document.createElement('div');
|
||
buttonContainer.classList.add('flex-container', 'flexFlowColumn', 'wide100p', 'm-t-1');
|
||
|
||
for (const [result, button] of resultToButtonMap) {
|
||
const buttonElement = document.createElement('div');
|
||
buttonElement.classList.add('menu_button', 'result-control', 'wide100p');
|
||
buttonElement.dataset.result = String(result);
|
||
buttonElement.addEventListener('click', () => {
|
||
popup?.complete(result);
|
||
});
|
||
buttonElement.innerText = button;
|
||
buttonContainer.appendChild(buttonElement);
|
||
}
|
||
|
||
const popupContainer = document.createElement('div');
|
||
popupContainer.innerHTML = safeValue;
|
||
popupContainer.appendChild(buttonContainer);
|
||
|
||
popup = new Popup(popupContainer, POPUP_TYPE.TEXT, '', { okButton: 'Cancel' });
|
||
popup.show()
|
||
.then((result => resolve(typeof result === 'number' ? resultToButtonMap.get(result) ?? '' : '')))
|
||
.catch(() => resolve(''));
|
||
});
|
||
} catch {
|
||
return '';
|
||
}
|
||
}
|
||
|
||
async function popupCallback(args, value) {
|
||
const safeValue = DOMPurify.sanitize(value || '');
|
||
const popupOptions = {
|
||
large: isTrueBoolean(args?.large),
|
||
wide: isTrueBoolean(args?.wide),
|
||
okButton: args?.okButton !== undefined && typeof args?.okButton === 'string' ? args.okButton : 'Ok',
|
||
};
|
||
await delay(1);
|
||
await callGenericPopup(safeValue, POPUP_TYPE.TEXT, '', popupOptions);
|
||
await delay(1);
|
||
return String(value);
|
||
}
|
||
|
||
function getMessagesCallback(args, value) {
|
||
const includeNames = !isFalseBoolean(args?.names);
|
||
const includeHidden = isTrueBoolean(args?.hidden);
|
||
const role = args?.role;
|
||
const range = stringToRange(value, 0, chat.length - 1);
|
||
|
||
if (!range) {
|
||
console.warn(`WARN: Invalid range provided for /messages command: ${value}`);
|
||
return '';
|
||
}
|
||
|
||
const filterByRole = (mes) => {
|
||
if (!role) {
|
||
return true;
|
||
}
|
||
|
||
const isNarrator = mes.extra?.type === system_message_types.NARRATOR;
|
||
|
||
if (role === 'system') {
|
||
return isNarrator && !mes.is_user;
|
||
}
|
||
|
||
if (role === 'assistant') {
|
||
return !isNarrator && !mes.is_user;
|
||
}
|
||
|
||
if (role === 'user') {
|
||
return !isNarrator && mes.is_user;
|
||
}
|
||
|
||
throw new Error(`Invalid role provided. Expected one of: system, assistant, user. Got: ${role}`);
|
||
};
|
||
|
||
const messages = [];
|
||
|
||
for (let messageId = range.start; messageId <= range.end; messageId++) {
|
||
const message = chat[messageId];
|
||
if (!message) {
|
||
console.warn(`WARN: No message found with ID ${messageId}`);
|
||
continue;
|
||
}
|
||
|
||
if (role && !filterByRole(message)) {
|
||
console.debug(`/messages: Skipping message with ID ${messageId} due to role filter`);
|
||
continue;
|
||
}
|
||
|
||
if (!includeHidden && message.is_system) {
|
||
console.debug(`/messages: Skipping hidden message with ID ${messageId}`);
|
||
continue;
|
||
}
|
||
|
||
if (includeNames) {
|
||
messages.push(`${message.name}: ${message.mes}`);
|
||
} else {
|
||
messages.push(message.mes);
|
||
}
|
||
}
|
||
|
||
return messages.join('\n\n');
|
||
}
|
||
|
||
async function runCallback(args, name) {
|
||
if (!name) {
|
||
throw new Error('No name provided for /run command');
|
||
}
|
||
|
||
/**@type {SlashCommandScope} */
|
||
const scope = args._scope;
|
||
if (scope.existsVariable(name)) {
|
||
const closure = scope.getVariable(name);
|
||
if (!(closure instanceof SlashCommandClosure)) {
|
||
throw new Error(`"${name}" is not callable.`);
|
||
}
|
||
closure.scope.parent = scope;
|
||
closure.argumentList.forEach(arg => {
|
||
if (Object.keys(args).includes(arg.name)) {
|
||
const providedArg = new SlashCommandNamedArgumentAssignment();
|
||
providedArg.name = arg.name;
|
||
providedArg.value = args[arg.name];
|
||
closure.providedArgumentList.push(providedArg);
|
||
}
|
||
});
|
||
const result = await closure.execute();
|
||
return result.pipe;
|
||
}
|
||
|
||
if (typeof window['executeQuickReplyByName'] !== 'function') {
|
||
throw new Error('Quick Reply extension is not loaded');
|
||
}
|
||
|
||
try {
|
||
name = name.trim();
|
||
return await window['executeQuickReplyByName'](name, args);
|
||
} catch (error) {
|
||
throw new Error(`Error running Quick Reply "${name}": ${error.message}`, 'Error');
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {import('./slash-commands/SlashCommand.js').NamedArguments} param0
|
||
* @param {string} [reason]
|
||
*/
|
||
function abortCallback({ _abortController, quiet }, reason) {
|
||
if (quiet instanceof SlashCommandClosure) throw new Error('argument \'quiet\' cannot be a closure for command /abort');
|
||
_abortController.abort((reason ?? '').toString().length == 0 ? '/abort command executed' : reason, !isFalseBoolean(quiet ?? 'true'));
|
||
return '';
|
||
}
|
||
|
||
async function delayCallback(_, amount) {
|
||
if (!amount) {
|
||
console.warn('WARN: No amount provided for /delay command');
|
||
return '';
|
||
}
|
||
|
||
amount = Number(amount);
|
||
if (isNaN(amount)) {
|
||
amount = 0;
|
||
}
|
||
|
||
await delay(amount);
|
||
return '';
|
||
}
|
||
|
||
async function inputCallback(args, prompt) {
|
||
const safeValue = DOMPurify.sanitize(prompt || '');
|
||
const defaultInput = args?.default !== undefined && typeof args?.default === 'string' ? args.default : '';
|
||
const popupOptions = {
|
||
large: isTrueBoolean(args?.large),
|
||
wide: isTrueBoolean(args?.wide),
|
||
okButton: args?.okButton !== undefined && typeof args?.okButton === 'string' ? args.okButton : 'Ok',
|
||
rows: args?.rows !== undefined && typeof args?.rows === 'string' ? isNaN(Number(args.rows)) ? 4 : Number(args.rows) : 4,
|
||
};
|
||
// Do not remove this delay, otherwise the prompt will not show up
|
||
await delay(1);
|
||
const result = await callGenericPopup(safeValue, POPUP_TYPE.INPUT, defaultInput, popupOptions);
|
||
await delay(1);
|
||
return String(result || '');
|
||
}
|
||
|
||
/**
|
||
* Each item in "args.list" is searched within "search_item" using fuzzy search. If any matches it returns the matched "item".
|
||
* @param {FuzzyCommandArgs} args - arguments containing "list" (JSON array) and optionaly "threshold" (float between 0.0 and 1.0)
|
||
* @param {string} searchInValue - the string where items of list are searched
|
||
* @returns {string} - the matched item from the list
|
||
* @typedef {{list: string, threshold: string}} FuzzyCommandArgs - arguments for /fuzzy command
|
||
* @example /fuzzy list=["down","left","up","right"] "he looks up" | /echo // should return "up"
|
||
* @link https://www.fusejs.io/
|
||
*/
|
||
function fuzzyCallback(args, searchInValue) {
|
||
if (!searchInValue) {
|
||
console.warn('WARN: No argument provided for /fuzzy command');
|
||
return '';
|
||
}
|
||
|
||
if (!args.list) {
|
||
console.warn('WARN: No list argument provided for /fuzzy command');
|
||
return '';
|
||
}
|
||
|
||
try {
|
||
const list = JSON.parse(resolveVariable(args.list));
|
||
if (!Array.isArray(list)) {
|
||
console.warn('WARN: Invalid list argument provided for /fuzzy command');
|
||
return '';
|
||
}
|
||
|
||
const params = {
|
||
includeScore: true,
|
||
findAllMatches: true,
|
||
ignoreLocation: true,
|
||
threshold: 0.4,
|
||
};
|
||
// threshold determines how strict is the match, low threshold value is very strict, at 1 (nearly?) everything matches
|
||
if ('threshold' in args) {
|
||
params.threshold = parseFloat(resolveVariable(args.threshold));
|
||
if (isNaN(params.threshold)) {
|
||
console.warn('WARN: \'threshold\' argument must be a float between 0.0 and 1.0 for /fuzzy command');
|
||
return '';
|
||
}
|
||
if (params.threshold < 0) {
|
||
params.threshold = 0;
|
||
}
|
||
if (params.threshold > 1) {
|
||
params.threshold = 1;
|
||
}
|
||
}
|
||
|
||
const fuse = new Fuse([searchInValue], params);
|
||
// each item in the "list" is searched within "search_item", if any matches it returns the matched "item"
|
||
for (const searchItem of list) {
|
||
const result = fuse.search(searchItem);
|
||
if (result.length > 0) {
|
||
console.info('fuzzyCallback Matched: ' + searchItem);
|
||
return searchItem;
|
||
}
|
||
}
|
||
return '';
|
||
} catch {
|
||
console.warn('WARN: Invalid list argument provided for /fuzzy command');
|
||
return '';
|
||
}
|
||
}
|
||
|
||
function setEphemeralStopStrings(value) {
|
||
if (typeof value === 'string' && value.length) {
|
||
try {
|
||
const stopStrings = JSON.parse(value);
|
||
if (Array.isArray(stopStrings)) {
|
||
stopStrings.forEach(stopString => addEphemeralStoppingString(stopString));
|
||
}
|
||
} catch {
|
||
// Do nothing
|
||
}
|
||
}
|
||
}
|
||
|
||
async function generateRawCallback(args, value) {
|
||
if (!value) {
|
||
console.warn('WARN: No argument provided for /genraw command');
|
||
return;
|
||
}
|
||
|
||
// Prevent generate recursion
|
||
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
|
||
const lock = isTrueBoolean(args?.lock);
|
||
const as = args?.as || 'system';
|
||
const quietToLoud = as === 'char';
|
||
const systemPrompt = resolveVariable(args?.system) || '';
|
||
const length = Number(resolveVariable(args?.length) ?? 0) || 0;
|
||
|
||
try {
|
||
if (lock) {
|
||
deactivateSendButtons();
|
||
}
|
||
|
||
setEphemeralStopStrings(resolveVariable(args?.stop));
|
||
const result = await generateRaw(value, '', isFalseBoolean(args?.instruct), quietToLoud, systemPrompt, length);
|
||
return result;
|
||
} finally {
|
||
if (lock) {
|
||
activateSendButtons();
|
||
}
|
||
flushEphemeralStoppingStrings();
|
||
}
|
||
}
|
||
|
||
async function generateCallback(args, value) {
|
||
if (!value) {
|
||
console.warn('WARN: No argument provided for /gen command');
|
||
return;
|
||
}
|
||
|
||
// Prevent generate recursion
|
||
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
|
||
const lock = isTrueBoolean(args?.lock);
|
||
const as = args?.as || 'system';
|
||
const quietToLoud = as === 'char';
|
||
const length = Number(resolveVariable(args?.length) ?? 0) || 0;
|
||
|
||
try {
|
||
if (lock) {
|
||
deactivateSendButtons();
|
||
}
|
||
|
||
setEphemeralStopStrings(resolveVariable(args?.stop));
|
||
const name = args?.name;
|
||
const result = await generateQuietPrompt(value, quietToLoud, false, '', name, length);
|
||
return result;
|
||
} finally {
|
||
if (lock) {
|
||
activateSendButtons();
|
||
}
|
||
flushEphemeralStoppingStrings();
|
||
}
|
||
}
|
||
|
||
async function echoCallback(args, value) {
|
||
// Note: We don't need to sanitize input, as toastr is set up by default to escape HTML via toastr options
|
||
if (value === '') {
|
||
console.warn('WARN: No argument provided for /echo command');
|
||
return;
|
||
}
|
||
const title = args?.title !== undefined && typeof args?.title === 'string' ? args.title : undefined;
|
||
const severity = args?.severity !== undefined && typeof args?.severity === 'string' ? args.severity : 'info';
|
||
switch (severity) {
|
||
case 'error':
|
||
toastr.error(value, title);
|
||
break;
|
||
case 'warning':
|
||
toastr.warning(value, title);
|
||
break;
|
||
case 'success':
|
||
toastr.success(value, title);
|
||
break;
|
||
case 'info':
|
||
default:
|
||
toastr.info(value, title);
|
||
break;
|
||
}
|
||
return value;
|
||
}
|
||
|
||
async function addSwipeCallback(_, arg) {
|
||
const lastMessage = chat[chat.length - 1];
|
||
|
||
if (!lastMessage) {
|
||
toastr.warning('No messages to add swipes to.');
|
||
return '';
|
||
}
|
||
|
||
if (!arg) {
|
||
console.warn('WARN: No argument provided for /addswipe command');
|
||
return '';
|
||
}
|
||
|
||
if (lastMessage.is_user) {
|
||
toastr.warning('Can\'t add swipes to user messages.');
|
||
return '';
|
||
}
|
||
|
||
if (lastMessage.is_system) {
|
||
toastr.warning('Can\'t add swipes to system messages.');
|
||
return '';
|
||
}
|
||
|
||
if (lastMessage.extra?.image) {
|
||
toastr.warning('Can\'t add swipes to message containing an image.');
|
||
return '';
|
||
}
|
||
|
||
if (!Array.isArray(lastMessage.swipes)) {
|
||
lastMessage.swipes = [lastMessage.mes];
|
||
lastMessage.swipe_info = [{}];
|
||
lastMessage.swipe_id = 0;
|
||
}
|
||
if (!Array.isArray(lastMessage.swipe_info)) {
|
||
lastMessage.swipe_info = lastMessage.swipes.map(() => ({}));
|
||
}
|
||
|
||
lastMessage.swipes.push(arg);
|
||
lastMessage.swipe_info.push({
|
||
send_date: getMessageTimeStamp(),
|
||
gen_started: null,
|
||
gen_finished: null,
|
||
extra: {
|
||
bias: extractMessageBias(arg),
|
||
gen_id: Date.now(),
|
||
api: 'manual',
|
||
model: 'slash command',
|
||
},
|
||
});
|
||
|
||
await saveChatConditional();
|
||
await reloadCurrentChat();
|
||
|
||
return '';
|
||
}
|
||
|
||
async function deleteSwipeCallback(_, arg) {
|
||
const lastMessage = chat[chat.length - 1];
|
||
|
||
if (!lastMessage || !Array.isArray(lastMessage.swipes) || !lastMessage.swipes.length) {
|
||
toastr.warning('No messages to delete swipes from.');
|
||
return '';
|
||
}
|
||
|
||
if (lastMessage.swipes.length <= 1) {
|
||
toastr.warning('Can\'t delete the last swipe.');
|
||
return '';
|
||
}
|
||
|
||
const swipeId = arg && !isNaN(Number(arg)) ? (Number(arg) - 1) : lastMessage.swipe_id;
|
||
|
||
if (swipeId < 0 || swipeId >= lastMessage.swipes.length) {
|
||
toastr.warning(`Invalid swipe ID: ${swipeId + 1}`);
|
||
return '';
|
||
}
|
||
|
||
lastMessage.swipes.splice(swipeId, 1);
|
||
|
||
if (Array.isArray(lastMessage.swipe_info) && lastMessage.swipe_info.length) {
|
||
lastMessage.swipe_info.splice(swipeId, 1);
|
||
}
|
||
|
||
const newSwipeId = Math.min(swipeId, lastMessage.swipes.length - 1);
|
||
lastMessage.swipe_id = newSwipeId;
|
||
lastMessage.mes = lastMessage.swipes[newSwipeId];
|
||
|
||
await saveChatConditional();
|
||
await reloadCurrentChat();
|
||
|
||
return '';
|
||
}
|
||
|
||
async function askCharacter(args, text) {
|
||
// Prevent generate recursion
|
||
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
|
||
|
||
// Not supported in group chats
|
||
// TODO: Maybe support group chats?
|
||
if (selected_group) {
|
||
toastr.error('Cannot run this command in a group chat!');
|
||
return '';
|
||
}
|
||
|
||
if (!text) {
|
||
console.warn('WARN: No text provided for /ask command');
|
||
toastr.warning('No text provided for /ask command');
|
||
return '';
|
||
}
|
||
|
||
let name = '';
|
||
let mesText = '';
|
||
|
||
if (args?.name) {
|
||
name = args.name.trim();
|
||
mesText = text.trim();
|
||
|
||
if (!name && !mesText) {
|
||
toastr.warning('You must specify a name and text to ask.');
|
||
return '';
|
||
}
|
||
}
|
||
|
||
mesText = getRegexedString(mesText, regex_placement.SLASH_COMMAND);
|
||
|
||
const prevChId = this_chid;
|
||
|
||
// Find the character
|
||
const chId = characters.findIndex((e) => e.name === name);
|
||
if (!characters[chId] || chId === -1) {
|
||
toastr.error('Character not found.');
|
||
return '';
|
||
}
|
||
|
||
// Override character and send a user message
|
||
setCharacterId(String(chId));
|
||
|
||
// TODO: Maybe look up by filename instead of name
|
||
const character = characters[chId];
|
||
let force_avatar, original_avatar;
|
||
|
||
if (character && character.avatar !== 'none') {
|
||
force_avatar = getThumbnailUrl('avatar', character.avatar);
|
||
original_avatar = character.avatar;
|
||
}
|
||
else {
|
||
force_avatar = default_avatar;
|
||
original_avatar = default_avatar;
|
||
}
|
||
|
||
setCharacterName(character.name);
|
||
|
||
await sendMessageAsUser(mesText, '');
|
||
|
||
const restoreCharacter = () => {
|
||
setCharacterId(prevChId);
|
||
setCharacterName(characters[prevChId].name);
|
||
|
||
// Only force the new avatar if the character name is the same
|
||
// This skips if an error was fired
|
||
const lastMessage = chat[chat.length - 1];
|
||
if (lastMessage && lastMessage?.name === character.name) {
|
||
lastMessage.force_avatar = force_avatar;
|
||
lastMessage.original_avatar = original_avatar;
|
||
}
|
||
|
||
// Kill this callback once the event fires
|
||
eventSource.removeListener(event_types.CHARACTER_MESSAGE_RENDERED, restoreCharacter);
|
||
};
|
||
|
||
// Run generate and restore previous character on error
|
||
try {
|
||
toastr.info(`Asking ${character.name} something...`);
|
||
await Generate('ask_command');
|
||
} catch {
|
||
restoreCharacter();
|
||
}
|
||
|
||
// Restore previous character once message renders
|
||
// Hack for generate
|
||
eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, restoreCharacter);
|
||
return '';
|
||
}
|
||
|
||
async function hideMessageCallback(_, arg) {
|
||
if (!arg) {
|
||
console.warn('WARN: No argument provided for /hide command');
|
||
return '';
|
||
}
|
||
|
||
const range = stringToRange(arg, 0, chat.length - 1);
|
||
|
||
if (!range) {
|
||
console.warn(`WARN: Invalid range provided for /hide command: ${arg}`);
|
||
return '';
|
||
}
|
||
|
||
await hideChatMessageRange(range.start, range.end, false);
|
||
return '';
|
||
}
|
||
|
||
async function unhideMessageCallback(_, arg) {
|
||
if (!arg) {
|
||
console.warn('WARN: No argument provided for /unhide command');
|
||
return '';
|
||
}
|
||
|
||
const range = stringToRange(arg, 0, chat.length - 1);
|
||
|
||
if (!range) {
|
||
console.warn(`WARN: Invalid range provided for /unhide command: ${arg}`);
|
||
return '';
|
||
}
|
||
|
||
await hideChatMessageRange(range.start, range.end, true);
|
||
return '';
|
||
}
|
||
|
||
/**
|
||
* Copium for running group actions when the member is offscreen.
|
||
* @param {number} chid - character ID
|
||
* @param {string} action - one of 'enable', 'disable', 'up', 'down', 'view', 'remove'
|
||
* @returns {void}
|
||
*/
|
||
function performGroupMemberAction(chid, action) {
|
||
const memberSelector = `.group_member[chid="${chid}"]`;
|
||
// Do not optimize. Paginator gets recreated on every action
|
||
const paginationSelector = '#rm_group_members_pagination';
|
||
const pageSizeSelector = '#rm_group_members_pagination select';
|
||
let wasOffscreen = false;
|
||
let paginationValue = null;
|
||
let pageValue = null;
|
||
|
||
if ($(memberSelector).length === 0) {
|
||
wasOffscreen = true;
|
||
paginationValue = Number($(pageSizeSelector).val());
|
||
pageValue = $(paginationSelector).pagination('getCurrentPageNum');
|
||
$(pageSizeSelector).val($(pageSizeSelector).find('option').last().val()).trigger('change');
|
||
}
|
||
|
||
$(memberSelector).find(`[data-action="${action}"]`).trigger('click');
|
||
|
||
if (wasOffscreen) {
|
||
$(pageSizeSelector).val(paginationValue).trigger('change');
|
||
if ($(paginationSelector).length) {
|
||
$(paginationSelector).pagination('go', pageValue);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function disableGroupMemberCallback(_, arg) {
|
||
if (!selected_group) {
|
||
toastr.warning('Cannot run /disable command outside of a group chat.');
|
||
return '';
|
||
}
|
||
|
||
const chid = findGroupMemberId(arg);
|
||
|
||
if (chid === undefined) {
|
||
console.warn(`WARN: No group member found for argument ${arg}`);
|
||
return '';
|
||
}
|
||
|
||
performGroupMemberAction(chid, 'disable');
|
||
return '';
|
||
}
|
||
|
||
async function enableGroupMemberCallback(_, arg) {
|
||
if (!selected_group) {
|
||
toastr.warning('Cannot run /enable command outside of a group chat.');
|
||
return '';
|
||
}
|
||
|
||
const chid = findGroupMemberId(arg);
|
||
|
||
if (chid === undefined) {
|
||
console.warn(`WARN: No group member found for argument ${arg}`);
|
||
return '';
|
||
}
|
||
|
||
performGroupMemberAction(chid, 'enable');
|
||
return '';
|
||
}
|
||
|
||
async function moveGroupMemberUpCallback(_, arg) {
|
||
if (!selected_group) {
|
||
toastr.warning('Cannot run /memberup command outside of a group chat.');
|
||
return '';
|
||
}
|
||
|
||
const chid = findGroupMemberId(arg);
|
||
|
||
if (chid === undefined) {
|
||
console.warn(`WARN: No group member found for argument ${arg}`);
|
||
return '';
|
||
}
|
||
|
||
performGroupMemberAction(chid, 'up');
|
||
return '';
|
||
}
|
||
|
||
async function moveGroupMemberDownCallback(_, arg) {
|
||
if (!selected_group) {
|
||
toastr.warning('Cannot run /memberdown command outside of a group chat.');
|
||
return '';
|
||
}
|
||
|
||
const chid = findGroupMemberId(arg);
|
||
|
||
if (chid === undefined) {
|
||
console.warn(`WARN: No group member found for argument ${arg}`);
|
||
return '';
|
||
}
|
||
|
||
performGroupMemberAction(chid, 'down');
|
||
return '';
|
||
}
|
||
|
||
async function peekCallback(_, arg) {
|
||
if (!selected_group) {
|
||
toastr.warning('Cannot run /peek command outside of a group chat.');
|
||
return '';
|
||
}
|
||
|
||
if (is_group_generating) {
|
||
toastr.warning('Cannot run /peek command while the group reply is generating.');
|
||
return '';
|
||
}
|
||
|
||
const chid = findGroupMemberId(arg);
|
||
|
||
if (chid === undefined) {
|
||
console.warn(`WARN: No group member found for argument ${arg}`);
|
||
return '';
|
||
}
|
||
|
||
performGroupMemberAction(chid, 'view');
|
||
return '';
|
||
}
|
||
|
||
async function removeGroupMemberCallback(_, arg) {
|
||
if (!selected_group) {
|
||
toastr.warning('Cannot run /memberremove command outside of a group chat.');
|
||
return '';
|
||
}
|
||
|
||
if (is_group_generating) {
|
||
toastr.warning('Cannot run /memberremove command while the group reply is generating.');
|
||
return '';
|
||
}
|
||
|
||
const chid = findGroupMemberId(arg);
|
||
|
||
if (chid === undefined) {
|
||
console.warn(`WARN: No group member found for argument ${arg}`);
|
||
return '';
|
||
}
|
||
|
||
performGroupMemberAction(chid, 'remove');
|
||
return '';
|
||
}
|
||
|
||
async function addGroupMemberCallback(_, arg) {
|
||
if (!selected_group) {
|
||
toastr.warning('Cannot run /memberadd command outside of a group chat.');
|
||
return '';
|
||
}
|
||
|
||
if (!arg) {
|
||
console.warn('WARN: No argument provided for /memberadd command');
|
||
return '';
|
||
}
|
||
|
||
arg = arg.trim();
|
||
const chid = findCharacterIndex(arg);
|
||
|
||
if (chid === -1) {
|
||
console.warn(`WARN: No character found for argument ${arg}`);
|
||
return '';
|
||
}
|
||
|
||
const character = characters[chid];
|
||
const group = groups.find(x => x.id === selected_group);
|
||
|
||
if (!group || !Array.isArray(group.members)) {
|
||
console.warn(`WARN: No group found for ID ${selected_group}`);
|
||
return '';
|
||
}
|
||
|
||
const avatar = character.avatar;
|
||
|
||
if (group.members.includes(avatar)) {
|
||
toastr.warning(`${character.name} is already a member of this group.`);
|
||
return '';
|
||
}
|
||
|
||
group.members.push(avatar);
|
||
await saveGroupChat(selected_group, true);
|
||
|
||
// Trigger to reload group UI
|
||
$('#rm_button_selected_ch').trigger('click');
|
||
return character.name;
|
||
}
|
||
|
||
async function triggerGenerationCallback(args, value) {
|
||
const shouldAwait = isTrueBoolean(args?.await);
|
||
const outerPromise = new Promise((outerResolve) => setTimeout(async () => {
|
||
try {
|
||
await waitUntilCondition(() => !is_send_press && !is_group_generating, 10000, 100);
|
||
} catch {
|
||
console.warn('Timeout waiting for generation unlock');
|
||
toastr.warning('Cannot run /trigger command while the reply is being generated.');
|
||
return '';
|
||
}
|
||
|
||
// Prevent generate recursion
|
||
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
|
||
|
||
let chid = undefined;
|
||
|
||
if (selected_group && value) {
|
||
chid = findGroupMemberId(value);
|
||
|
||
if (chid === undefined) {
|
||
console.warn(`WARN: No group member found for argument ${value}`);
|
||
}
|
||
}
|
||
|
||
outerResolve(new Promise(innerResolve => setTimeout(() => innerResolve(Generate('normal', { force_chid: chid })), 100)));
|
||
}, 1));
|
||
|
||
if (shouldAwait) {
|
||
const innerPromise = await outerPromise;
|
||
await innerPromise;
|
||
}
|
||
|
||
return '';
|
||
}
|
||
/**
|
||
* Find persona by name.
|
||
* @param {string} name Name to search for
|
||
* @returns {string} Persona name
|
||
*/
|
||
function findPersonaByName(name) {
|
||
if (!name) {
|
||
return null;
|
||
}
|
||
|
||
for (const persona of Object.entries(power_user.personas)) {
|
||
if (persona[1].toLowerCase() === name.toLowerCase()) {
|
||
return persona[0];
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
async function sendUserMessageCallback(args, text) {
|
||
if (!text) {
|
||
console.warn('WARN: No text provided for /send command');
|
||
return;
|
||
}
|
||
|
||
text = text.trim();
|
||
const compact = isTrueBoolean(args?.compact);
|
||
const bias = extractMessageBias(text);
|
||
const insertAt = Number(resolveVariable(args?.at));
|
||
|
||
if ('name' in args) {
|
||
const name = resolveVariable(args.name) || '';
|
||
const avatar = findPersonaByName(name) || user_avatar;
|
||
await sendMessageAsUser(text, bias, insertAt, compact, name, avatar);
|
||
}
|
||
else {
|
||
await sendMessageAsUser(text, bias, insertAt, compact);
|
||
}
|
||
|
||
return '';
|
||
}
|
||
|
||
async function deleteMessagesByNameCallback(_, name) {
|
||
if (!name) {
|
||
console.warn('WARN: No name provided for /delname command');
|
||
return;
|
||
}
|
||
|
||
name = name.trim();
|
||
|
||
const messagesToDelete = [];
|
||
chat.forEach((value) => {
|
||
if (value.name === name) {
|
||
messagesToDelete.push(value);
|
||
}
|
||
});
|
||
|
||
if (!messagesToDelete.length) {
|
||
console.debug('/delname: Nothing to delete');
|
||
return;
|
||
}
|
||
|
||
for (const message of messagesToDelete) {
|
||
const index = chat.indexOf(message);
|
||
if (index !== -1) {
|
||
console.debug(`/delname: Deleting message #${index}`, message);
|
||
chat.splice(index, 1);
|
||
}
|
||
}
|
||
|
||
await saveChatConditional();
|
||
await reloadCurrentChat();
|
||
|
||
toastr.info(`Deleted ${messagesToDelete.length} messages from ${name}`);
|
||
return '';
|
||
}
|
||
|
||
function findCharacterIndex(name) {
|
||
const matchTypes = [
|
||
(a, b) => a === b,
|
||
(a, b) => a.startsWith(b),
|
||
(a, b) => a.includes(b),
|
||
];
|
||
|
||
const exactAvatarMatch = characters.findIndex(x => x.avatar === name);
|
||
|
||
if (exactAvatarMatch !== -1) {
|
||
return exactAvatarMatch;
|
||
}
|
||
|
||
for (const matchType of matchTypes) {
|
||
const index = characters.findIndex(x => matchType(x.name.toLowerCase(), name.toLowerCase()));
|
||
if (index !== -1) {
|
||
return index;
|
||
}
|
||
}
|
||
|
||
return -1;
|
||
}
|
||
|
||
async function goToCharacterCallback(_, name) {
|
||
if (!name) {
|
||
console.warn('WARN: No character name provided for /go command');
|
||
return;
|
||
}
|
||
|
||
name = name.trim();
|
||
const characterIndex = findCharacterIndex(name);
|
||
|
||
if (characterIndex !== -1) {
|
||
await openChat(new String(characterIndex));
|
||
setActiveCharacter(characters[characterIndex]?.avatar);
|
||
setActiveGroup(null);
|
||
return characters[characterIndex]?.name;
|
||
} else {
|
||
const group = groups.find(it => it.name.toLowerCase() == name.toLowerCase());
|
||
if (group) {
|
||
await openGroupById(group.id);
|
||
setActiveCharacter(null);
|
||
setActiveGroup(group.id);
|
||
return group.name;
|
||
} else {
|
||
console.warn(`No matches found for name "${name}"`);
|
||
return '';
|
||
}
|
||
}
|
||
}
|
||
|
||
async function openChat(id) {
|
||
resetSelectedGroup();
|
||
setCharacterId(id);
|
||
await delay(1);
|
||
await reloadCurrentChat();
|
||
}
|
||
|
||
function continueChatCallback(_, prompt) {
|
||
setTimeout(async () => {
|
||
try {
|
||
await waitUntilCondition(() => !is_send_press && !is_group_generating, 10000, 100);
|
||
} catch {
|
||
console.warn('Timeout waiting for generation unlock');
|
||
toastr.warning('Cannot run /continue command while the reply is being generated.');
|
||
}
|
||
|
||
// Prevent infinite recursion
|
||
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
|
||
$('#option_continue').trigger('click', { fromSlashCommand: true, additionalPrompt: prompt });
|
||
}, 1);
|
||
|
||
return '';
|
||
}
|
||
|
||
export async function generateSystemMessage(_, prompt) {
|
||
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
|
||
|
||
if (!prompt) {
|
||
console.warn('WARN: No prompt provided for /sysgen command');
|
||
toastr.warning('You must provide a prompt for the system message');
|
||
return '';
|
||
}
|
||
|
||
// Generate and regex the output if applicable
|
||
toastr.info('Please wait', 'Generating...');
|
||
let message = await generateQuietPrompt(prompt, false, false);
|
||
message = getRegexedString(message, regex_placement.SLASH_COMMAND);
|
||
|
||
sendNarratorMessage(_, message);
|
||
return '';
|
||
}
|
||
|
||
function syncCallback() {
|
||
$('#sync_name_button').trigger('click');
|
||
return '';
|
||
}
|
||
|
||
async function lockPersonaCallback(_args, value) {
|
||
if (['toggle', 't', ''].includes(value.trim().toLowerCase())) {
|
||
await togglePersonaLock();
|
||
return '';
|
||
}
|
||
|
||
if (isTrueBoolean(value)) {
|
||
await setPersonaLockState(true);
|
||
return '';
|
||
}
|
||
|
||
if (isFalseBoolean(value)) {
|
||
await setPersonaLockState(false);
|
||
return '';
|
||
|
||
}
|
||
|
||
return '';
|
||
}
|
||
|
||
function setStoryModeCallback() {
|
||
$('#chat_display').val(chat_styles.DOCUMENT).trigger('change');
|
||
return '';
|
||
}
|
||
|
||
function setBubbleModeCallback() {
|
||
$('#chat_display').val(chat_styles.BUBBLES).trigger('change');
|
||
return '';
|
||
}
|
||
|
||
function setFlatModeCallback() {
|
||
$('#chat_display').val(chat_styles.DEFAULT).trigger('change');
|
||
return '';
|
||
}
|
||
|
||
/**
|
||
* Sets a persona name and optionally an avatar.
|
||
* @param {{mode: 'lookup' | 'temp' | 'all'}} namedArgs Named arguments
|
||
* @param {string} name Name to set
|
||
* @returns {string}
|
||
*/
|
||
function setNameCallback({ mode = 'all' }, name) {
|
||
if (!name) {
|
||
toastr.warning('You must specify a name to change to');
|
||
return '';
|
||
}
|
||
|
||
if (!['lookup', 'temp', 'all'].includes(mode)) {
|
||
toastr.warning('Mode must be one of "lookup", "temp" or "all"');
|
||
return '';
|
||
}
|
||
|
||
name = name.trim();
|
||
|
||
// If the name matches a persona avatar, or a name, auto-select it
|
||
if (['lookup', 'all'].includes(mode)) {
|
||
let persona = Object.entries(power_user.personas).find(([avatar, _]) => avatar === name)?.[1];
|
||
if (!persona) persona = Object.entries(power_user.personas).find(([_, personaName]) => personaName.toLowerCase() === name.toLowerCase())?.[1];
|
||
if (persona) {
|
||
autoSelectPersona(persona);
|
||
retriggerFirstMessageOnEmptyChat();
|
||
return '';
|
||
} else if (mode === 'lookup') {
|
||
toastr.warning(`Persona ${name} not found`);
|
||
return '';
|
||
}
|
||
}
|
||
|
||
if (['temp', 'all'].includes(mode)) {
|
||
// Otherwise, set just the name
|
||
setUserName(name); //this prevented quickReply usage
|
||
retriggerFirstMessageOnEmptyChat();
|
||
}
|
||
|
||
return '';
|
||
}
|
||
|
||
async function setNarratorName(_, text) {
|
||
const name = text || NARRATOR_NAME_DEFAULT;
|
||
chat_metadata[NARRATOR_NAME_KEY] = name;
|
||
toastr.info(`System narrator name set to ${name}`);
|
||
await saveChatConditional();
|
||
return '';
|
||
}
|
||
|
||
export async function sendMessageAs(args, text) {
|
||
if (!text) {
|
||
return '';
|
||
}
|
||
|
||
let name;
|
||
let mesText;
|
||
|
||
if (args.name) {
|
||
name = args.name.trim();
|
||
mesText = text.trim();
|
||
|
||
if (!name && !text) {
|
||
toastr.warning('You must specify a name and text to send as');
|
||
return '';
|
||
}
|
||
} else {
|
||
const namelessWarningKey = 'sendAsNamelessWarningShown';
|
||
if (localStorage.getItem(namelessWarningKey) !== 'true') {
|
||
toastr.warning('To avoid confusion, please use /sendas name="Character Name"', 'Name defaulted to {{char}}', { timeOut: 10000 });
|
||
localStorage.setItem(namelessWarningKey, 'true');
|
||
}
|
||
name = name2;
|
||
}
|
||
|
||
// Requires a regex check after the slash command is pushed to output
|
||
mesText = getRegexedString(mesText, regex_placement.SLASH_COMMAND, { characterOverride: name });
|
||
|
||
// Messages that do nothing but set bias will be hidden from the context
|
||
const bias = extractMessageBias(mesText);
|
||
const isSystem = bias && !removeMacros(mesText).length;
|
||
const compact = isTrueBoolean(args?.compact);
|
||
|
||
const character = characters.find(x => x.name === name);
|
||
let force_avatar, original_avatar;
|
||
|
||
if (character && character.avatar !== 'none') {
|
||
force_avatar = getThumbnailUrl('avatar', character.avatar);
|
||
original_avatar = character.avatar;
|
||
}
|
||
else {
|
||
force_avatar = default_avatar;
|
||
original_avatar = default_avatar;
|
||
}
|
||
|
||
const message = {
|
||
name: name,
|
||
is_user: false,
|
||
is_system: isSystem,
|
||
send_date: getMessageTimeStamp(),
|
||
mes: substituteParams(mesText),
|
||
force_avatar: force_avatar,
|
||
original_avatar: original_avatar,
|
||
extra: {
|
||
bias: bias.trim().length ? bias : null,
|
||
gen_id: Date.now(),
|
||
isSmallSys: compact,
|
||
api: 'manual',
|
||
model: 'slash command',
|
||
},
|
||
};
|
||
|
||
message.swipe_id = 0;
|
||
message.swipes = [message.mes];
|
||
message.swipes_info = [{
|
||
send_date: message.send_date,
|
||
gen_started: null,
|
||
gen_finished: null,
|
||
extra: {
|
||
bias: message.extra.bias,
|
||
gen_id: message.extra.gen_id,
|
||
isSmallSys: compact,
|
||
api: 'manual',
|
||
model: 'slash command',
|
||
},
|
||
}];
|
||
|
||
const insertAt = Number(resolveVariable(args.at));
|
||
|
||
if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) {
|
||
chat.splice(insertAt, 0, message);
|
||
await saveChatConditional();
|
||
await eventSource.emit(event_types.MESSAGE_RECEIVED, insertAt);
|
||
await reloadCurrentChat();
|
||
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, insertAt);
|
||
} else {
|
||
chat.push(message);
|
||
await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1));
|
||
addOneMessage(message);
|
||
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, (chat.length - 1));
|
||
await saveChatConditional();
|
||
}
|
||
|
||
return '';
|
||
}
|
||
|
||
export async function sendNarratorMessage(args, text) {
|
||
if (!text) {
|
||
return '';
|
||
}
|
||
|
||
const name = chat_metadata[NARRATOR_NAME_KEY] || NARRATOR_NAME_DEFAULT;
|
||
// Messages that do nothing but set bias will be hidden from the context
|
||
const bias = extractMessageBias(text);
|
||
const isSystem = bias && !removeMacros(text).length;
|
||
const compact = isTrueBoolean(args?.compact);
|
||
|
||
const message = {
|
||
name: name,
|
||
is_user: false,
|
||
is_system: isSystem,
|
||
send_date: getMessageTimeStamp(),
|
||
mes: substituteParams(text.trim()),
|
||
force_avatar: system_avatar,
|
||
extra: {
|
||
type: system_message_types.NARRATOR,
|
||
bias: bias.trim().length ? bias : null,
|
||
gen_id: Date.now(),
|
||
isSmallSys: compact,
|
||
api: 'manual',
|
||
model: 'slash command',
|
||
},
|
||
};
|
||
|
||
const insertAt = Number(resolveVariable(args.at));
|
||
|
||
if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) {
|
||
chat.splice(insertAt, 0, message);
|
||
await saveChatConditional();
|
||
await eventSource.emit(event_types.MESSAGE_SENT, insertAt);
|
||
await reloadCurrentChat();
|
||
await eventSource.emit(event_types.USER_MESSAGE_RENDERED, insertAt);
|
||
} else {
|
||
chat.push(message);
|
||
await eventSource.emit(event_types.MESSAGE_SENT, (chat.length - 1));
|
||
addOneMessage(message);
|
||
await eventSource.emit(event_types.USER_MESSAGE_RENDERED, (chat.length - 1));
|
||
await saveChatConditional();
|
||
}
|
||
|
||
return '';
|
||
}
|
||
|
||
export async function promptQuietForLoudResponse(who, text) {
|
||
|
||
let character_id = getContext().characterId;
|
||
if (who === 'sys') {
|
||
text = 'System: ' + text;
|
||
} else if (who === 'user') {
|
||
text = name1 + ': ' + text;
|
||
} else if (who === 'char') {
|
||
text = characters[character_id].name + ': ' + text;
|
||
} else if (who === 'raw') {
|
||
// We don't need to modify the text
|
||
}
|
||
|
||
//text = `${text}${power_user.instruct.enabled ? '' : '\n'}${(power_user.always_force_name2 && who != 'raw') ? characters[character_id].name + ":" : ""}`
|
||
|
||
let reply = await generateQuietPrompt(text, true, false);
|
||
text = await getRegexedString(reply, regex_placement.SLASH_COMMAND);
|
||
|
||
const message = {
|
||
name: characters[character_id].name,
|
||
is_user: false,
|
||
is_name: true,
|
||
is_system: false,
|
||
send_date: getMessageTimeStamp(),
|
||
mes: substituteParams(text.trim()),
|
||
extra: {
|
||
type: system_message_types.COMMENT,
|
||
gen_id: Date.now(),
|
||
api: 'manual',
|
||
model: 'slash command',
|
||
},
|
||
};
|
||
|
||
chat.push(message);
|
||
await eventSource.emit(event_types.MESSAGE_SENT, (chat.length - 1));
|
||
addOneMessage(message);
|
||
await eventSource.emit(event_types.USER_MESSAGE_RENDERED, (chat.length - 1));
|
||
await saveChatConditional();
|
||
|
||
}
|
||
|
||
async function sendCommentMessage(args, text) {
|
||
if (!text) {
|
||
return '';
|
||
}
|
||
|
||
const compact = isTrueBoolean(args?.compact);
|
||
const message = {
|
||
name: COMMENT_NAME_DEFAULT,
|
||
is_user: false,
|
||
is_system: true,
|
||
send_date: getMessageTimeStamp(),
|
||
mes: substituteParams(text.trim()),
|
||
force_avatar: comment_avatar,
|
||
extra: {
|
||
type: system_message_types.COMMENT,
|
||
gen_id: Date.now(),
|
||
isSmallSys: compact,
|
||
api: 'manual',
|
||
model: 'slash command',
|
||
},
|
||
};
|
||
|
||
const insertAt = Number(resolveVariable(args.at));
|
||
|
||
if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) {
|
||
chat.splice(insertAt, 0, message);
|
||
await saveChatConditional();
|
||
await eventSource.emit(event_types.MESSAGE_SENT, insertAt);
|
||
await reloadCurrentChat();
|
||
await eventSource.emit(event_types.USER_MESSAGE_RENDERED, insertAt);
|
||
} else {
|
||
chat.push(message);
|
||
await eventSource.emit(event_types.MESSAGE_SENT, (chat.length - 1));
|
||
addOneMessage(message);
|
||
await eventSource.emit(event_types.USER_MESSAGE_RENDERED, (chat.length - 1));
|
||
await saveChatConditional();
|
||
}
|
||
|
||
return '';
|
||
}
|
||
|
||
/**
|
||
* Displays a help message from the slash command
|
||
* @param {any} _ Unused
|
||
* @param {string} type Type of help to display
|
||
*/
|
||
function helpCommandCallback(_, type) {
|
||
switch (type?.trim()?.toLowerCase()) {
|
||
case 'slash':
|
||
case 'commands':
|
||
case 'slashes':
|
||
case 'slash commands':
|
||
case '1':
|
||
sendSystemMessage(system_message_types.SLASH_COMMANDS);
|
||
break;
|
||
case 'format':
|
||
case 'formatting':
|
||
case 'formats':
|
||
case 'chat formatting':
|
||
case '2':
|
||
sendSystemMessage(system_message_types.FORMATTING);
|
||
break;
|
||
case 'hotkeys':
|
||
case 'hotkey':
|
||
case '3':
|
||
sendSystemMessage(system_message_types.HOTKEYS);
|
||
break;
|
||
case 'macros':
|
||
case 'macro':
|
||
case '4':
|
||
sendSystemMessage(system_message_types.MACROS);
|
||
break;
|
||
default:
|
||
sendSystemMessage(system_message_types.HELP);
|
||
break;
|
||
}
|
||
|
||
return '';
|
||
}
|
||
|
||
$(document).on('click', '[data-displayHelp]', function (e) {
|
||
e.preventDefault();
|
||
const page = String($(this).data('displayhelp'));
|
||
helpCommandCallback(null, page);
|
||
});
|
||
|
||
function setBackgroundCallback(_, bg) {
|
||
if (!bg) {
|
||
// allow reporting of the background name if called without args
|
||
// for use in ST Scripts via pipe
|
||
return background_settings.name;
|
||
}
|
||
|
||
console.log('Set background to ' + bg);
|
||
|
||
const bgElements = Array.from(document.querySelectorAll('.bg_example')).map((x) => ({ element: x, bgfile: x.getAttribute('bgfile') }));
|
||
|
||
const fuse = new Fuse(bgElements, { keys: ['bgfile'] });
|
||
const result = fuse.search(bg);
|
||
|
||
if (!result.length) {
|
||
toastr.error(`No background found with name "${bg}"`);
|
||
return '';
|
||
}
|
||
|
||
const bgElement = result[0].item.element;
|
||
|
||
if (bgElement instanceof HTMLElement) {
|
||
bgElement.click();
|
||
}
|
||
|
||
return '';
|
||
}
|
||
|
||
/**
|
||
* Retrieves the available model options based on the currently selected main API and its subtype
|
||
*
|
||
* @returns {{control: HTMLSelectElement, options: HTMLOptionElement[]}?} An array of objects representing the available model options, or null if not supported
|
||
*/
|
||
function getModelOptions() {
|
||
const modelSelectMap = [
|
||
{ id: 'model_togetherai_select', api: 'textgenerationwebui', type: textgen_types.TOGETHERAI },
|
||
{ id: 'openrouter_model', api: 'textgenerationwebui', type: textgen_types.OPENROUTER },
|
||
{ id: 'model_infermaticai_select', api: 'textgenerationwebui', type: textgen_types.INFERMATICAI },
|
||
{ id: 'model_dreamgen_select', api: 'textgenerationwebui', type: textgen_types.DREAMGEN },
|
||
{ id: 'mancer_model', api: 'textgenerationwebui', type: textgen_types.MANCER },
|
||
{ id: 'vllm_model', api: 'textgenerationwebui', type: textgen_types.VLLM },
|
||
{ id: 'aphrodite_model', api: 'textgenerationwebui', type: textgen_types.APHRODITE },
|
||
{ id: 'ollama_model', api: 'textgenerationwebui', type: textgen_types.OLLAMA },
|
||
{ id: 'model_openai_select', api: 'openai', type: chat_completion_sources.OPENAI },
|
||
{ id: 'model_claude_select', api: 'openai', type: chat_completion_sources.CLAUDE },
|
||
{ id: 'model_windowai_select', api: 'openai', type: chat_completion_sources.WINDOWAI },
|
||
{ id: 'model_openrouter_select', api: 'openai', type: chat_completion_sources.OPENROUTER },
|
||
{ id: 'model_ai21_select', api: 'openai', type: chat_completion_sources.AI21 },
|
||
{ id: 'model_google_select', api: 'openai', type: chat_completion_sources.MAKERSUITE },
|
||
{ id: 'model_mistralai_select', api: 'openai', type: chat_completion_sources.MISTRALAI },
|
||
{ id: 'model_custom_select', api: 'openai', type: chat_completion_sources.CUSTOM },
|
||
{ id: 'model_cohere_select', api: 'openai', type: chat_completion_sources.COHERE },
|
||
{ id: 'model_perplexity_select', api: 'openai', type: chat_completion_sources.PERPLEXITY },
|
||
{ id: 'model_groq_select', api: 'openai', type: chat_completion_sources.GROQ },
|
||
{ id: 'model_novel_select', api: 'novel', type: null },
|
||
{ id: 'horde_model', api: 'koboldhorde', type: null },
|
||
];
|
||
|
||
function getSubType() {
|
||
switch (main_api) {
|
||
case 'textgenerationwebui':
|
||
return textgenerationwebui_settings.type;
|
||
case 'openai':
|
||
return oai_settings.chat_completion_source;
|
||
default:
|
||
return null;
|
||
}
|
||
}
|
||
|
||
const apiSubType = getSubType();
|
||
const modelSelectItem = modelSelectMap.find(x => x.api == main_api && x.type == apiSubType)?.id;
|
||
|
||
if (!modelSelectItem) {
|
||
toastr.info('Setting a model for your API is not supported or not implemented yet.');
|
||
return null;
|
||
}
|
||
|
||
const modelSelectControl = document.getElementById(modelSelectItem);
|
||
|
||
if (!(modelSelectControl instanceof HTMLSelectElement)) {
|
||
toastr.error(`Model select control not found: ${main_api}[${apiSubType}]`);
|
||
return null;
|
||
}
|
||
|
||
const options = Array.from(modelSelectControl.options);
|
||
return { control: modelSelectControl, options };
|
||
}
|
||
|
||
/**
|
||
* Sets a model for the current API.
|
||
* @param {object} _ Unused
|
||
* @param {string} model New model name
|
||
* @returns {string} New or existing model name
|
||
*/
|
||
function modelCallback(_, model) {
|
||
const { control: modelSelectControl, options } = getModelOptions();
|
||
|
||
// If no model was found, the reason was already logged, we just return here
|
||
if (options === null) {
|
||
return '';
|
||
}
|
||
|
||
if (!options.length) {
|
||
toastr.warning('No model options found. Check your API settings.');
|
||
return '';
|
||
}
|
||
|
||
model = String(model || '').trim();
|
||
|
||
if (!model) {
|
||
return modelSelectControl.value;
|
||
}
|
||
|
||
console.log('Set model to ' + model);
|
||
|
||
let newSelectedOption = null;
|
||
|
||
const fuse = new Fuse(options, { keys: ['text', 'value'] });
|
||
const fuzzySearchResult = fuse.search(model);
|
||
|
||
const exactValueMatch = options.find(x => x.value.trim().toLowerCase() === model.trim().toLowerCase());
|
||
const exactTextMatch = options.find(x => x.text.trim().toLowerCase() === model.trim().toLowerCase());
|
||
|
||
if (exactValueMatch) {
|
||
newSelectedOption = exactValueMatch;
|
||
} else if (exactTextMatch) {
|
||
newSelectedOption = exactTextMatch;
|
||
} else if (fuzzySearchResult.length) {
|
||
newSelectedOption = fuzzySearchResult[0].item;
|
||
}
|
||
|
||
if (newSelectedOption) {
|
||
modelSelectControl.value = newSelectedOption.value;
|
||
$(modelSelectControl).trigger('change');
|
||
toastr.success(`Model set to "${newSelectedOption.text}"`);
|
||
return newSelectedOption.value;
|
||
} else {
|
||
toastr.warning(`No model found with name "${model}"`);
|
||
return '';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Sets state of prompt entries (toggles) either via identifier/uuid or name.
|
||
* @param {object} args Object containing arguments
|
||
* @param {string} args.identifier Select prompt entry using an identifier (uuid)
|
||
* @param {string} args.name Select prompt entry using name
|
||
* @param {string} targetState The targeted state of the entry/entries
|
||
* @returns {String} empty string
|
||
*/
|
||
function setPromptEntryCallback(args, targetState) {
|
||
// needs promptManager to manipulate prompt entries
|
||
const promptManager = setupChatCompletionPromptManager(oai_settings);
|
||
const prompts = promptManager.serviceSettings.prompts;
|
||
|
||
function parseArgs(arg) {
|
||
const list = [];
|
||
try {
|
||
const parsedArg = JSON.parse(arg);
|
||
list.push(...Array.isArray(parsedArg) ? parsedArg : [arg]);
|
||
} catch {
|
||
list.push(arg);
|
||
}
|
||
return list;
|
||
}
|
||
|
||
let identifiersList = parseArgs(args.identifier);
|
||
let nameList = parseArgs(args.name);
|
||
|
||
// Check if identifiers exists in prompt, else remove from list
|
||
if (identifiersList.length !== 0) {
|
||
identifiersList = identifiersList.filter(identifier => prompts.some(prompt => prompt.identifier === identifier));
|
||
}
|
||
|
||
if (nameList.length !== 0) {
|
||
nameList.forEach(name => {
|
||
// one name could potentially have multiple entries, find all identifiers that match given name
|
||
let identifiers = [];
|
||
prompts.forEach(entry => {
|
||
if (entry.name === name) {
|
||
identifiers.push(entry.identifier);
|
||
}
|
||
});
|
||
identifiersList = identifiersList.concat(identifiers);
|
||
});
|
||
}
|
||
|
||
// Remove duplicates to allow consistent 'toggle'
|
||
identifiersList = [...new Set(identifiersList)];
|
||
if (identifiersList.length === 0) return '';
|
||
|
||
// logic adapted from PromptManager.js, handleToggle
|
||
const getPromptOrderEntryState = (promptOrderEntry) => {
|
||
if (['toggle', 't', ''].includes(targetState.trim().toLowerCase())) {
|
||
return !promptOrderEntry.enabled;
|
||
}
|
||
|
||
if (isTrueBoolean(targetState)) {
|
||
return true;
|
||
}
|
||
|
||
if (isFalseBoolean(targetState)) {
|
||
return false;
|
||
}
|
||
|
||
return promptOrderEntry.enabled;
|
||
};
|
||
|
||
identifiersList.forEach(promptID => {
|
||
const promptOrderEntry = promptManager.getPromptOrderEntry(promptManager.activeCharacter, promptID);
|
||
const counts = promptManager.tokenHandler.getCounts();
|
||
|
||
counts[promptID] = null;
|
||
promptOrderEntry.enabled = getPromptOrderEntryState(promptOrderEntry);
|
||
});
|
||
|
||
// no need to render for each identifier
|
||
promptManager.render();
|
||
promptManager.saveServiceSettings();
|
||
return '';
|
||
}
|
||
|
||
export let isExecutingCommandsFromChatInput = false;
|
||
export let commandsFromChatInputAbortController;
|
||
|
||
/**
|
||
* Show command execution pause/stop buttons next to chat input.
|
||
*/
|
||
export function activateScriptButtons() {
|
||
document.querySelector('#form_sheld').classList.add('isExecutingCommandsFromChatInput');
|
||
}
|
||
|
||
/**
|
||
* Hide command execution pause/stop buttons next to chat input.
|
||
*/
|
||
export function deactivateScriptButtons() {
|
||
document.querySelector('#form_sheld').classList.remove('isExecutingCommandsFromChatInput');
|
||
}
|
||
|
||
/**
|
||
* Toggle pause/continue command execution. Only for commands executed via chat input.
|
||
*/
|
||
export function pauseScriptExecution() {
|
||
if (commandsFromChatInputAbortController) {
|
||
if (commandsFromChatInputAbortController.signal.paused) {
|
||
commandsFromChatInputAbortController.continue('Clicked pause button');
|
||
document.querySelector('#form_sheld').classList.remove('script_paused');
|
||
} else {
|
||
commandsFromChatInputAbortController.pause('Clicked pause button');
|
||
document.querySelector('#form_sheld').classList.add('script_paused');
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Stop command execution. Only for commands executed via chat input.
|
||
*/
|
||
export function stopScriptExecution() {
|
||
commandsFromChatInputAbortController?.abort('Clicked stop button');
|
||
}
|
||
|
||
/**
|
||
* Clear up command execution progress bar above chat input.
|
||
* @returns Promise<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 {(done:number, total:number)=>void} [onProgress] (null) Callback to handle progress events
|
||
*/
|
||
|
||
/**
|
||
* @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
|
||
*/
|
||
|
||
/**
|
||
* 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,
|
||
}, 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;
|
||
try {
|
||
commandsFromChatInputAbortController = new SlashCommandAbortController();
|
||
result = await executeSlashCommandsWithOptions(text, {
|
||
abortController: commandsFromChatInputAbortController,
|
||
onProgress: (done, total) => ta.style.setProperty('--prog', `${done / total * 100}%`),
|
||
parserFlags: options.parserFlags,
|
||
scope: options.scope,
|
||
});
|
||
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;
|
||
if (e.cause !== 'abort') {
|
||
toastr.error(e.message);
|
||
}
|
||
} 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,
|
||
onProgress: 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;
|
||
} 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) {
|
||
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)
|
||
*/
|
||
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,
|
||
);
|
||
}
|
||
/**@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;
|
||
}
|
||
});
|