Move default slash commands to respective module (#4240) * Move default slash commands to respective module * Update public/scripts/slash-commands.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

28189bb1c780130bf473345c05a269af2ec84596

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
3 files changed, +565 -577Showing whitespace changes
public/script.js+5 -559
@@ -1,7 +1,6 @@
1import {1import {
2 showdown,2 showdown,
3 moment,3 moment,
4 Fuse,
5 DOMPurify,4 DOMPurify,
6 hljs,5 hljs,
7 Handlebars,6 Handlebars,
@@ -89,7 +88,6 @@ import {
89 sortEntitiesList,88 sortEntitiesList,
90 registerDebugFunction,89 registerDebugFunction,
91 flushEphemeralStoppingStrings,90 flushEphemeralStoppingStrings,
92 context_presets,
93 resetMovableStyles,91 resetMovableStyles,
94 forceCharacterEditorTokenize,92 forceCharacterEditorTokenize,
95 applyPowerUserSettings,93 applyPowerUserSettings,
@@ -167,7 +165,6 @@ import {
167 isValidUrl,165 isValidUrl,
168 ensureImageFormatSupported,166 ensureImageFormatSupported,
169 flashHighlight,167 flashHighlight,
170 isTrueBoolean,
171 toggleDrawer,168 toggleDrawer,
172 isElementInViewport,169 isElementInViewport,
173 copyText,170 copyText,
@@ -182,7 +179,7 @@ import {
182import { debounce_timeout, IGNORE_SYMBOL } from './scripts/constants.js';179import { debounce_timeout, IGNORE_SYMBOL } from './scripts/constants.js';
183180
184import { cancelDebouncedMetadataSave, doDailyExtensionUpdatesCheck, extension_settings, initExtensions, loadExtensionSettings, runGenerationInterceptors, saveMetadataDebounced } from './scripts/extensions.js';181import { cancelDebouncedMetadataSave, doDailyExtensionUpdatesCheck, extension_settings, initExtensions, loadExtensionSettings, runGenerationInterceptors, saveMetadataDebounced } from './scripts/extensions.js';
185import { COMMENT_NAME_DEFAULT, executeSlashCommandsOnChatInput, getSlashCommandsHelp, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommands, stopScriptExecution } from './scripts/slash-commands.js';182import { COMMENT_NAME_DEFAULT, CONNECT_API_MAP, executeSlashCommandsOnChatInput, getSlashCommandsHelp, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommands, stopScriptExecution, UNIQUE_APIS } from './scripts/slash-commands.js';
186import {183import {
187 tag_map,184 tag_map,
188 tags,185 tags,
@@ -223,9 +220,6 @@ import {
223 formatInstructModeExamples,220 formatInstructModeExamples,
224 getInstructStoppingSequences,221 getInstructStoppingSequences,
225 formatInstructModeSystemPrompt,222 formatInstructModeSystemPrompt,
226 selectInstructPreset,
227 instruct_presets,
228 selectContextPreset,
229} from './scripts/instruct-mode.js';223} from './scripts/instruct-mode.js';
230import { initLocales, t } from './scripts/i18n.js';224import { initLocales, t } from './scripts/i18n.js';
231import { getFriendlyTokenizerName, getTokenCount, getTokenCountAsync, initTokenizers, saveTokenCache } from './scripts/tokenizers.js';225import { getFriendlyTokenizerName, getTokenCount, getTokenCountAsync, initTokenizers, saveTokenCache } from './scripts/tokenizers.js';
@@ -251,16 +245,11 @@ import { currentUser, setUserControls } from './scripts/user.js';
251import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';245import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';
252import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';246import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';
253import { initScrapers } from './scripts/scrapers.js';247import { initScrapers } from './scripts/scrapers.js';
254import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js';
255import { SlashCommand } from './scripts/slash-commands/SlashCommand.js';
256import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './scripts/slash-commands/SlashCommandArgument.js';
257import { SlashCommandBrowser } from './scripts/slash-commands/SlashCommandBrowser.js';248import { SlashCommandBrowser } from './scripts/slash-commands/SlashCommandBrowser.js';
258import { initCustomSelectedSamplers, validateDisabledSamplers } from './scripts/samplerSelect.js';249import { initCustomSelectedSamplers, validateDisabledSamplers } from './scripts/samplerSelect.js';
259import { DragAndDropHandler } from './scripts/dragdrop.js';250import { DragAndDropHandler } from './scripts/dragdrop.js';
260import { INTERACTABLE_CONTROL_CLASS, initKeyboard } from './scripts/keyboard.js';251import { INTERACTABLE_CONTROL_CLASS, initKeyboard } from './scripts/keyboard.js';
261import { initDynamicStyles } from './scripts/dynamic-styles.js';252import { initDynamicStyles } from './scripts/dynamic-styles.js';
262import { SlashCommandEnumValue, enumTypes } from './scripts/slash-commands/SlashCommandEnumValue.js';
263import { commonEnumProviders, enumIcons } from './scripts/slash-commands/SlashCommandCommonEnumsProvider.js';
264import { initInputMarkdown } from './scripts/input-md-formatting.js';253import { initInputMarkdown } from './scripts/input-md-formatting.js';
265import { AbortReason } from './scripts/util/AbortReason.js';254import { AbortReason } from './scripts/util/AbortReason.js';
266import { initSystemPrompts } from './scripts/sysprompt.js';255import { initSystemPrompts } from './scripts/sysprompt.js';
@@ -306,6 +295,8 @@ export {
306 koboldai_setting_names,295 koboldai_setting_names,
307 novelai_settings,296 novelai_settings,
308 novelai_setting_names,297 novelai_setting_names,
298 UNIQUE_APIS,
299 CONNECT_API_MAP,
309};300};
310301
311/**302/**
@@ -5257,7 +5248,7 @@ function addChatsSeparator(mesSendString) {
5257 }5248 }
5258}5249}
52595250
5260async function duplicateCharacter() {5251export async function duplicateCharacter() {
5261 if (this_chid === undefined || !characters[this_chid]) {5252 if (this_chid === undefined || !characters[this_chid]) {
5262 toastr.warning(t`You must first select a character to duplicate!`);5253 toastr.warning(t`You must first select a character to duplicate!`);
5263 return '';5254 return '';
@@ -7385,7 +7376,7 @@ export async function getPastCharacterChats(characterId = null) {
7385/**7376/**
7386 * Helper for `displayPastChats`, to make the same info consistently available for other functions7377 * Helper for `displayPastChats`, to make the same info consistently available for other functions
7387 */7378 */
7388function getCurrentChatDetails() {7379export function getCurrentChatDetails() {
7389 if (!characters[this_chid] && !selected_group) {7380 if (!characters[this_chid] && !selected_group) {
7390 return { sessionName: '', group: null, characterName: '', avatarImgURL: '' };7381 return { sessionName: '', group: null, characterName: '', avatarImgURL: '' };
7391 }7382 }
@@ -8931,219 +8922,6 @@ export function swipe_right(_event = null, { source, repeated } = {}) {
8931}8922}
89328923
8933/**8924/**
8934 * @typedef {object} ConnectAPIMap
8935 * @property {string} selected - API name (e.g. "textgenerationwebui", "openai")
8936 * @property {string?} [button] - CSS selector for the API button
8937 * @property {string?} [type] - API type, mostly used by text completion. (e.g. "openrouter")
8938 * @property {string?} [source] - API source, mostly used by chat completion. (e.g. "openai")
8939 */
8940
8941/**
8942 * @type {Record<string, ConnectAPIMap>}
8943 */
8944export const CONNECT_API_MAP = {
8945 // Default APIs not contined inside text gen / chat gen
8946 'kobold': {
8947 selected: 'kobold',
8948 button: '#api_button',
8949 },
8950 'horde': {
8951 selected: 'koboldhorde',
8952 },
8953 'novel': {
8954 selected: 'novel',
8955 button: '#api_button_novel',
8956 },
8957 'koboldcpp': {
8958 selected: 'textgenerationwebui',
8959 button: '#api_button_textgenerationwebui',
8960 type: textgen_types.KOBOLDCPP,
8961 },
8962 // KoboldCpp alias
8963 'kcpp': {
8964 selected: 'textgenerationwebui',
8965 button: '#api_button_textgenerationwebui',
8966 type: textgen_types.KOBOLDCPP,
8967 },
8968 'openai': {
8969 selected: 'openai',
8970 button: '#api_button_openai',
8971 source: chat_completion_sources.OPENAI,
8972 },
8973 // OpenAI alias
8974 'oai': {
8975 selected: 'openai',
8976 button: '#api_button_openai',
8977 source: chat_completion_sources.OPENAI,
8978 },
8979 // Google alias
8980 'google': {
8981 selected: 'openai',
8982 button: '#api_button_openai',
8983 source: chat_completion_sources.MAKERSUITE,
8984 },
8985 // OpenRouter special naming, to differentiate between chat comp and text comp
8986 'openrouter': {
8987 selected: 'openai',
8988 button: '#api_button_openai',
8989 source: chat_completion_sources.OPENROUTER,
8990 },
8991 'openrouter-text': {
8992 selected: 'textgenerationwebui',
8993 button: '#api_button_textgenerationwebui',
8994 type: textgen_types.OPENROUTER,
8995 },
8996};
8997
8998// Collect all unique API names in an array
8999export const UNIQUE_APIS = [...new Set(Object.values(CONNECT_API_MAP).map(x => x.selected))];
9000
9001// Fill connections map from textgen_types and chat_completion_sources
9002for (const textGenType of Object.values(textgen_types)) {
9003 if (CONNECT_API_MAP[textGenType]) continue;
9004 CONNECT_API_MAP[textGenType] = {
9005 selected: 'textgenerationwebui',
9006 button: '#api_button_textgenerationwebui',
9007 type: textGenType,
9008 };
9009}
9010for (const chatCompletionSource of Object.values(chat_completion_sources)) {
9011 if (CONNECT_API_MAP[chatCompletionSource]) continue;
9012 CONNECT_API_MAP[chatCompletionSource] = {
9013 selected: 'openai',
9014 button: '#api_button_openai',
9015 source: chatCompletionSource,
9016 };
9017}
9018
9019async function selectContextCallback(args, name) {
9020 if (!name) {
9021 return power_user.context.preset;
9022 }
9023
9024 const quiet = isTrueBoolean(args?.quiet);
9025 const contextNames = context_presets.map(preset => preset.name);
9026 const fuse = new Fuse(contextNames);
9027 const result = fuse.search(name);
9028
9029 if (result.length === 0) {
9030 !quiet && toastr.warning(t`Context template '${name}' not found`);
9031 return '';
9032 }
9033
9034 const foundName = result[0].item;
9035 selectContextPreset(foundName, { quiet: quiet });
9036 return foundName;
9037}
9038
9039async function selectInstructCallback(args, name) {
9040 if (!name) {
9041 return power_user.instruct.enabled || isTrueBoolean(args?.forceGet) ? power_user.instruct.preset : '';
9042 }
9043
9044 const quiet = isTrueBoolean(args?.quiet);
9045 const instructNames = instruct_presets.map(preset => preset.name);
9046 const fuse = new Fuse(instructNames);
9047 const result = fuse.search(name);
9048
9049 if (result.length === 0) {
9050 !quiet && toastr.warning(t`Instruct template '${name}' not found`);
9051 return '';
9052 }
9053
9054 const foundName = result[0].item;
9055 selectInstructPreset(foundName, { quiet: quiet });
9056 return foundName;
9057}
9058
9059async function enableInstructCallback() {
9060 $('#instruct_enabled').prop('checked', true).trigger('input').trigger('change');
9061 return '';
9062}
9063
9064async function disableInstructCallback() {
9065 $('#instruct_enabled').prop('checked', false).trigger('input').trigger('change');
9066 return '';
9067}
9068
9069/**
9070 * @param {string} text API name
9071 */
9072async function connectAPISlash(args, text) {
9073 if (!text.trim()) {
9074 for (const [key, config] of Object.entries(CONNECT_API_MAP)) {
9075 if (config.selected !== main_api) continue;
9076
9077 if (config.source) {
9078 if (oai_settings.chat_completion_source === config.source) {
9079 return key;
9080 } else {
9081 continue;
9082 }
9083 }
9084
9085 if (config.type) {
9086 if (textgen_settings.type === config.type) {
9087 return key;
9088 } else {
9089 continue;
9090 }
9091 }
9092
9093 return key;
9094 }
9095
9096 console.error('FIXME: The current API is not in the API map');
9097 return '';
9098 }
9099
9100 const apiConfig = CONNECT_API_MAP[text.toLowerCase()];
9101 if (!apiConfig) {
9102 toastr.error(t`Error: ${text} is not a valid API`);
9103 return '';
9104 }
9105
9106 let connectionRequired = false;
9107
9108 if (main_api !== apiConfig.selected) {
9109 $(`#main_api option[value='${apiConfig.selected || text}']`).prop('selected', true);
9110 $('#main_api').trigger('change');
9111 connectionRequired = true;
9112 }
9113
9114 if (apiConfig.source && oai_settings.chat_completion_source !== apiConfig.source) {
9115 $(`#chat_completion_source option[value='${apiConfig.source}']`).prop('selected', true);
9116 $('#chat_completion_source').trigger('change');
9117 connectionRequired = true;
9118 }
9119
9120 if (apiConfig.type && textgen_settings.type !== apiConfig.type) {
9121 $(`#textgen_type option[value='${apiConfig.type}']`).prop('selected', true);
9122 $('#textgen_type').trigger('change');
9123 connectionRequired = true;
9124 }
9125
9126 if (connectionRequired && apiConfig.button) {
9127 $(apiConfig.button).trigger('click');
9128 }
9129
9130 const quiet = isTrueBoolean(args?.quiet);
9131 const toast = quiet ? jQuery() : toastr.info(t`API set to ${text}, trying to connect..`);
9132
9133 try {
9134 if (connectionRequired) {
9135 await waitUntilCondition(() => online_status !== 'no_connection', 5000, 100);
9136 }
9137 console.log('Connection successful');
9138 } catch {
9139 console.log('Could not connect after 5 seconds, skipping.');
9140 }
9141
9142 toastr.clear(toast);
9143 return text;
9144}
9145
9146/**
9147 * Imports supported files dropped into the app window.8925 * Imports supported files dropped into the app window.
9148 * @param {File[]} files Array of files to process8926 * @param {File[]} files Array of files to process
9149 * @param {Map<File, string>} [data] Extra data to pass to the import function8927 * @param {Map<File, string>} [data] Extra data to pass to the import function
@@ -9286,32 +9064,6 @@ async function importFromURL(items, files) {
9286 }9064 }
9287}9065}
92889066
9289async function doImpersonate(args, prompt) {
9290 const options = prompt?.trim() ? { quiet_prompt: prompt.trim(), quietToLoud: true } : {};
9291 const shouldAwait = isTrueBoolean(args?.await);
9292 const outerPromise = new Promise((outerResolve) => setTimeout(async () => {
9293 try {
9294 await waitUntilCondition(() => !is_send_press && !is_group_generating, 10000, 100);
9295 } catch {
9296 console.warn('Timeout waiting for generation unlock');
9297 toastr.warning(t`Cannot run /impersonate command while the reply is being generated.`);
9298 return '';
9299 }
9300
9301 // Prevent generate recursion
9302 $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
9303
9304 outerResolve(new Promise(innerResolve => setTimeout(() => innerResolve(Generate('impersonate', options)), 1)));
9305 }, 1));
9306
9307 if (shouldAwait) {
9308 const innerPromise = await outerPromise;
9309 await innerPromise;
9310 }
9311
9312 return '';
9313}
9314
9315export async function doNewChat({ deleteCurrentChat = false } = {}) {9067export async function doNewChat({ deleteCurrentChat = false } = {}) {
9316 //Make a new chat for selected character9068 //Make a new chat for selected character
9317 if ((!selected_group && this_chid == undefined) || menu_type == 'create') {9069 if ((!selected_group && this_chid == undefined) || menu_type == 'create') {
@@ -9346,53 +9098,6 @@ export async function doNewChat({ deleteCurrentChat = false } = {}) {
93469098
9347}9099}
93489100
9349async function doDeleteChat() {
9350 return displayPastChats().then(() => new Promise((resolve) => {
9351 let resolved = false;
9352 const timeOutId = setTimeout(() => {
9353 toastr.error(t`Chat deletion timed out. Please try again.`);
9354 setResolved();
9355 }, 5000);
9356
9357 const setResolved = () => {
9358 if (resolved) {
9359 return;
9360 }
9361 resolved = true;
9362 [event_types.CHAT_DELETED, event_types.GROUP_CHAT_DELETED].forEach((eventType) => {
9363 eventSource.removeListener(eventType, setResolved);
9364 });
9365 clearTimeout(timeOutId);
9366 resolve('');
9367 };
9368
9369 [event_types.CHAT_DELETED, event_types.GROUP_CHAT_DELETED].forEach((eventType) => {
9370 eventSource.on(eventType, setResolved);
9371 });
9372
9373 const currentChatDeleteButton = $('.select_chat_block[highlight=\'true\']').parent().find('.PastChat_cross');
9374 $(currentChatDeleteButton).trigger('click', { fromSlashCommand: true });
9375 }));
9376}
9377
9378async function doRenameChat(_, chatName) {
9379 if (!chatName) {
9380 toastr.warning(t`Name must be provided as an argument to rename this chat.`);
9381 return '';
9382 }
9383
9384 const currentChatName = getCurrentChatId();
9385 if (!currentChatName) {
9386 toastr.warning(t`No chat selected that can be renamed.`);
9387 return '';
9388 }
9389
9390 await renameChat(currentChatName, chatName);
9391
9392 toastr.success(t`Successfully renamed chat to: ${chatName}`);
9393 return '';
9394}
9395
9396/**9101/**
9397 * Renames a group or character chat.9102 * Renames a group or character chat.
9398 * @param {object} param Parameters for renaming chat9103 * @param {object} param Parameters for renaming chat
@@ -9506,12 +9211,6 @@ export async function updateRemoteChatName(characterId, newName) {
9506 }9211 }
9507}9212}
95089213
9509/**
9510 * /getchatname` slash command
9511 */
9512async function doGetChatName() {
9513 return getCurrentChatDetails().sessionName;
9514}
95159214
9516function doCharListDisplaySwitch() {9215function doCharListDisplaySwitch() {
9517 power_user.charListGrid = !power_user.charListGrid;9216 power_user.charListGrid = !power_user.charListGrid;
@@ -9519,11 +9218,6 @@ function doCharListDisplaySwitch() {
9519 saveSettingsDebounced();9218 saveSettingsDebounced();
9520}9219}
95219220
9522function doCloseChat() {
9523 $('#option_close_chat').trigger('click');
9524 return '';
9525}
9526
9527/**9221/**
9528 * Function to handle the deletion of a character, given a specific popup type and character ID.9222 * Function to handle the deletion of a character, given a specific popup type and character ID.
9529 * If popup type equals "del_ch", it will proceed with deletion otherwise it will exit the function.9223 * If popup type equals "del_ch", it will proceed with deletion otherwise it will exit the function.
@@ -9634,11 +9328,6 @@ export async function newAssistantChat({ temporary = false } = {}) {
9634 sendSystemMessage(system_message_types.ASSISTANT_NOTE);9328 sendSystemMessage(system_message_types.ASSISTANT_NOTE);
9635}9329}
96369330
9637function doTogglePanels() {
9638 $('#option_settings').trigger('click');
9639 return '';
9640}
9641
9642/**9331/**
9643 * Event handler to open a navbar drawer when a drawer open button is clicked.9332 * Event handler to open a navbar drawer when a drawer open button is clicked.
9644 * Handles click events on .drawer-opener elements.9333 * Handles click events on .drawer-opener elements.
@@ -9776,249 +9465,6 @@ API Settings: ${JSON.stringify(getSettingsContents[getSettingsContents.main_api
97769465
9777// MARK: DOM Handlers Start9466// MARK: DOM Handlers Start
9778jQuery(async function () {9467jQuery(async function () {
9779 async function doForceSave() {
9780 await saveSettings();
9781 await saveChatConditional();
9782 toastr.success('Chat and settings saved.');
9783 return '';
9784 }
9785
9786 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9787 name: 'dupe',
9788 callback: duplicateCharacter,
9789 helpString: 'Duplicates the currently selected character.',
9790 }));
9791 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9792 name: 'api',
9793 callback: connectAPISlash,
9794 returns: 'the current API',
9795 namedArgumentList: [
9796 SlashCommandNamedArgument.fromProps({
9797 name: 'quiet',
9798 description: 'Suppress the toast message on connection',
9799 typeList: [ARGUMENT_TYPE.BOOLEAN],
9800 defaultValue: 'false',
9801 enumList: commonEnumProviders.boolean('trueFalse')(),
9802 }),
9803 ],
9804 unnamedArgumentList: [
9805 SlashCommandArgument.fromProps({
9806 description: 'API to connect to',
9807 typeList: [ARGUMENT_TYPE.STRING],
9808 enumList: Object.entries(CONNECT_API_MAP).map(([api, { selected }]) =>
9809 new SlashCommandEnumValue(api, selected, enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === selected)),
9810 selected[0].toUpperCase() ?? enumIcons.default)),
9811 }),
9812 ],
9813 helpString: `
9814 <div>
9815 Connect to an API. If no argument is provided, it will return the currently connected API.
9816 </div>
9817 <div>
9818 <strong>Available APIs:</strong>
9819 <pre><code>${Object.keys(CONNECT_API_MAP).join(', ')}</code></pre>
9820 </div>
9821 `,
9822 }));
9823 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9824 name: 'impersonate',
9825 callback: doImpersonate,
9826 aliases: ['imp'],
9827 namedArgumentList: [
9828 new SlashCommandNamedArgument(
9829 'await',
9830 'Whether to await for the triggered generation before continuing',
9831 [ARGUMENT_TYPE.BOOLEAN],
9832 false,
9833 false,
9834 'false',
9835 ),
9836 ],
9837 unnamedArgumentList: [
9838 new SlashCommandArgument(
9839 'prompt', [ARGUMENT_TYPE.STRING], false,
9840 ),
9841 ],
9842 helpString: `
9843 <div>
9844 Calls an impersonation response, with an optional additional prompt.
9845 </div>
9846 <div>
9847 If <code>await=true</code> named argument is passed, the command will wait for the impersonation to end before continuing.
9848 </div>
9849 <div>
9850 <strong>Example:</strong>
9851 <ul>
9852 <li>
9853 <pre><code class="language-stscript">/impersonate What is the meaning of life?</code></pre>
9854 </li>
9855 </ul>
9856 </div>
9857 `,
9858 }));
9859 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9860 name: 'delchat',
9861 callback: doDeleteChat,
9862 helpString: 'Deletes the current chat.',
9863 }));
9864 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9865 name: 'renamechat',
9866 callback: doRenameChat,
9867 unnamedArgumentList: [
9868 new SlashCommandArgument(
9869 'new chat name', [ARGUMENT_TYPE.STRING], true,
9870 ),
9871 ],
9872 helpString: 'Renames the current chat.',
9873 }));
9874 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9875 name: 'getchatname',
9876 callback: doGetChatName,
9877 returns: 'chat file name',
9878 helpString: 'Returns the name of the current chat file into the pipe.',
9879 }));
9880 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9881 name: 'closechat',
9882 callback: doCloseChat,
9883 helpString: 'Closes the current chat.',
9884 }));
9885 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9886 name: 'tempchat',
9887 callback: () => {
9888 return new Promise((resolve, reject) => {
9889 const eventCallback = async (chatId) => {
9890 if (chatId) {
9891 return reject('Not in a temporary chat');
9892 }
9893 await newAssistantChat({ temporary: true });
9894 return resolve('');
9895 };
9896 eventSource.once(event_types.CHAT_CHANGED, eventCallback);
9897 doCloseChat();
9898 setTimeout(() => {
9899 reject('Failed to open temporary chat');
9900 eventSource.removeListener(event_types.CHAT_CHANGED, eventCallback);
9901 }, debounce_timeout.relaxed);
9902 });
9903 },
9904 helpString: 'Opens a temporary chat with Assistant.',
9905 }));
9906 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9907 name: 'panels',
9908 callback: doTogglePanels,
9909 aliases: ['togglepanels'],
9910 helpString: 'Toggle UI panels on/off',
9911 }));
9912 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9913 name: 'forcesave',
9914 callback: doForceSave,
9915 helpString: 'Forces a save of the current chat and settings',
9916 }));
9917 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9918 name: 'instruct',
9919 callback: selectInstructCallback,
9920 returns: 'current template',
9921 namedArgumentList: [
9922 SlashCommandNamedArgument.fromProps({
9923 name: 'quiet',
9924 description: 'Suppress the toast message on template change',
9925 typeList: [ARGUMENT_TYPE.BOOLEAN],
9926 defaultValue: 'false',
9927 enumList: commonEnumProviders.boolean('trueFalse')(),
9928 }),
9929 SlashCommandNamedArgument.fromProps({
9930 name: 'forceGet',
9931 description: 'Force getting a name even if instruct mode is disabled',
9932 typeList: [ARGUMENT_TYPE.BOOLEAN],
9933 defaultValue: 'false',
9934 enumList: commonEnumProviders.boolean('trueFalse')(),
9935 }),
9936 ],
9937 unnamedArgumentList: [
9938 SlashCommandArgument.fromProps({
9939 description: 'instruct template name',
9940 typeList: [ARGUMENT_TYPE.STRING],
9941 enumProvider: () => instruct_presets.map(preset => new SlashCommandEnumValue(preset.name, null, enumTypes.enum, enumIcons.preset)),
9942 }),
9943 ],
9944 helpString: `
9945 <div>
9946 Selects instruct mode template by name. Enables instruct mode if not already enabled.
9947 Gets the current instruct template if no name is provided and instruct mode is enabled or <code>forceGet=true</code> is passed.
9948 </div>
9949 <div>
9950 <strong>Example:</strong>
9951 <ul>
9952 <li>
9953 <pre><code class="language-stscript">/instruct creative</code></pre>
9954 </li>
9955 </ul>
9956 </div>
9957 `,
9958 }));
9959 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9960 name: 'instruct-on',
9961 callback: enableInstructCallback,
9962 helpString: 'Enables instruct mode.',
9963 }));
9964 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9965 name: 'instruct-off',
9966 callback: disableInstructCallback,
9967 helpString: 'Disables instruct mode',
9968 }));
9969 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9970 name: 'instruct-state',
9971 aliases: ['instruct-toggle'],
9972 helpString: 'Gets the current instruct mode state. If an argument is provided, it will set the instruct mode state.',
9973 unnamedArgumentList: [
9974 SlashCommandArgument.fromProps({
9975 description: 'instruct mode state',
9976 typeList: [ARGUMENT_TYPE.BOOLEAN],
9977 enumList: commonEnumProviders.boolean('trueFalse')(),
9978 }),
9979 ],
9980 callback: async (_args, state) => {
9981 if (!state || typeof state !== 'string') {
9982 return String(power_user.instruct.enabled);
9983 }
9984
9985 const newState = isTrueBoolean(state);
9986 newState ? enableInstructCallback() : disableInstructCallback();
9987 return String(power_user.instruct.enabled);
9988 },
9989 }));
9990 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9991 name: 'context',
9992 callback: selectContextCallback,
9993 returns: 'template name',
9994 namedArgumentList: [
9995 SlashCommandNamedArgument.fromProps({
9996 name: 'quiet',
9997 description: 'Suppress the toast message on template change',
9998 typeList: [ARGUMENT_TYPE.BOOLEAN],
9999 defaultValue: 'false',
10000 enumList: commonEnumProviders.boolean('trueFalse')(),
10001 }),
10002 ],
10003 unnamedArgumentList: [
10004 SlashCommandArgument.fromProps({
10005 description: 'context template name',
10006 typeList: [ARGUMENT_TYPE.STRING],
10007 enumProvider: () => context_presets.map(preset => new SlashCommandEnumValue(preset.name, null, enumTypes.enum, enumIcons.preset)),
10008 }),
10009 ],
10010 helpString: 'Selects context template by name. Gets the current template if no name is provided',
10011 }));
10012 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
10013 name: 'chat-manager',
10014 callback: () => {
10015 $('#option_select_chat').trigger('click');
10016 return '';
10017 },
10018 aliases: ['chat-history', 'manage-chats'],
10019 helpString: 'Opens the chat manager for the current character/group.',
10020 }));
10021
10022 setTimeout(function () {9468 setTimeout(function () {
10023 $('#groupControlsToggle').trigger('click');9469 $('#groupControlsToggle').trigger('click');
10024 $('#groupCurrentMemberListToggle .inline-drawer-icon').trigger('click');9470 $('#groupCurrentMemberListToggle .inline-drawer-icon').trigger('click');
public/scripts/extensions/shared.js+1 -1
@@ -471,7 +471,7 @@ export class ConnectionManagerRequestService {
471471
472 /**472 /**
473 * @param {import('./connection-manager/index.js').ConnectionProfile?} [profile]473 * @param {import('./connection-manager/index.js').ConnectionProfile?} [profile]
474 * @return {import('../../script.js').ConnectAPIMap}474 * @return {import('../slash-commands.js').ConnectAPIMap}
475 * @throws {Error}475 * @throws {Error}
476 */476 */
477 static validateProfile(profile) {477 static validateProfile(profile) {
public/scripts/slash-commands.js+559 -17
@@ -3,7 +3,6 @@ import { copyText, flashHighlight } from './utils.js';
33
4import {4import {
5 Generate,5 Generate,
6 UNIQUE_APIS,
7 activateSendButtons,6 activateSendButtons,
8 addOneMessage,7 addOneMessage,
9 characters,8 characters,
@@ -13,6 +12,8 @@ import {
13 deactivateSendButtons,12 deactivateSendButtons,
14 default_avatar,13 default_avatar,
15 deleteSwipe,14 deleteSwipe,
15 displayPastChats,
16 duplicateCharacter,
16 eventSource,17 eventSource,
17 event_types,18 event_types,
18 extension_prompt_roles,19 extension_prompt_roles,
@@ -20,6 +21,8 @@ import {
20 extractMessageBias,21 extractMessageBias,
21 generateQuietPrompt,22 generateQuietPrompt,
22 generateRaw,23 generateRaw,
24 getCurrentChatDetails,
25 getCurrentChatId,
23 getFirstDisplayedMessageId,26 getFirstDisplayedMessageId,
24 getThumbnailUrl,27 getThumbnailUrl,
25 is_send_press,28 is_send_press,
@@ -27,10 +30,14 @@ import {
27 name1,30 name1,
28 name2,31 name2,
29 neutralCharacterName,32 neutralCharacterName,
33 newAssistantChat,
34 online_status,
30 reloadCurrentChat,35 reloadCurrentChat,
31 removeMacros,36 removeMacros,
32 renameCharacter,37 renameCharacter,
38 renameChat,
33 saveChatConditional,39 saveChatConditional,
40 saveSettings,
34 saveSettingsDebounced,41 saveSettingsDebounced,
35 sendMessageAsUser,42 sendMessageAsUser,
36 sendSystemMessage,43 sendSystemMessage,
@@ -56,7 +63,7 @@ import { getRegexedString, regex_placement } from './extensions/regex/engine.js'
56import { findGroupMemberId, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group, getGroupMembers } from './group-chats.js';63import { findGroupMemberId, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group, getGroupMembers } from './group-chats.js';
57import { chat_completion_sources, oai_settings, promptManager } from './openai.js';64import { chat_completion_sources, oai_settings, promptManager } from './openai.js';
58import { user_avatar } from './personas.js';65import { user_avatar } from './personas.js';
59import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';66import { addEphemeralStoppingString, chat_styles, context_presets, flushEphemeralStoppingStrings, power_user } from './power-user.js';
60import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';67import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
61import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';68import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';
62import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, regexFromString, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';69import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, regexFromString, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
@@ -80,6 +87,8 @@ import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugC
80import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';87import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
81import { t } from './i18n.js';88import { t } from './i18n.js';
82import { kai_settings } from './kai-settings.js';89import { kai_settings } from './kai-settings.js';
90import { instruct_presets, selectContextPreset, selectInstructPreset } from './instruct-mode.js';
91import { debounce_timeout } from './constants.js';
83export {92export {
84 executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,93 executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
85};94};
@@ -110,7 +119,536 @@ function closureToFilter(closure) {
110 };119 };
111}120}
112121
122/**
123 * @typedef {object} ConnectAPIMap
124 * @property {string} selected - API name (e.g. "textgenerationwebui", "openai")
125 * @property {string?} [button] - CSS selector for the API button
126 * @property {string?} [type] - API type, mostly used by text completion. (e.g. "openrouter")
127 * @property {string?} [source] - API source, mostly used by chat completion. (e.g. "openai")
128 */
129
130/** @type {Record<string, ConnectAPIMap>} */
131export const CONNECT_API_MAP = {};
132
133/** @type {string[]} */
134export const UNIQUE_APIS = [];
135
136function setupConnectAPIMap() {
137 /** @type {Record<string, ConnectAPIMap>} */
138 const result = {
139 // Default APIs not contained inside text gen / chat gen
140 'kobold': {
141 selected: 'kobold',
142 button: '#api_button',
143 },
144 'horde': {
145 selected: 'koboldhorde',
146 },
147 'novel': {
148 selected: 'novel',
149 button: '#api_button_novel',
150 },
151 'koboldcpp': {
152 selected: 'textgenerationwebui',
153 button: '#api_button_textgenerationwebui',
154 type: textgen_types.KOBOLDCPP,
155 },
156 // KoboldCpp alias
157 'kcpp': {
158 selected: 'textgenerationwebui',
159 button: '#api_button_textgenerationwebui',
160 type: textgen_types.KOBOLDCPP,
161 },
162 'openai': {
163 selected: 'openai',
164 button: '#api_button_openai',
165 source: chat_completion_sources.OPENAI,
166 },
167 // OpenAI alias
168 'oai': {
169 selected: 'openai',
170 button: '#api_button_openai',
171 source: chat_completion_sources.OPENAI,
172 },
173 // Google alias
174 'google': {
175 selected: 'openai',
176 button: '#api_button_openai',
177 source: chat_completion_sources.MAKERSUITE,
178 },
179 // OpenRouter special naming, to differentiate between chat comp and text comp
180 'openrouter': {
181 selected: 'openai',
182 button: '#api_button_openai',
183 source: chat_completion_sources.OPENROUTER,
184 },
185 'openrouter-text': {
186 selected: 'textgenerationwebui',
187 button: '#api_button_textgenerationwebui',
188 type: textgen_types.OPENROUTER,
189 },
190 };
191
192 // Fill connections map from textgen_types and chat_completion_sources
193 for (const textGenType of Object.values(textgen_types)) {
194 if (result[textGenType]) continue;
195 result[textGenType] = {
196 selected: 'textgenerationwebui',
197 button: '#api_button_textgenerationwebui',
198 type: textGenType,
199 };
200 }
201
202 for (const chatCompletionSource of Object.values(chat_completion_sources)) {
203 if (result[chatCompletionSource]) continue;
204 result[chatCompletionSource] = {
205 selected: 'openai',
206 button: '#api_button_openai',
207 source: chatCompletionSource,
208 };
209 }
210
211 Object.assign(CONNECT_API_MAP, result);
212 UNIQUE_APIS.push(...new Set(Object.values(CONNECT_API_MAP).map(x => x.selected)));
213}
214
113export function initDefaultSlashCommands() {215export function initDefaultSlashCommands() {
216 setupConnectAPIMap();
217
218 async function enableInstructCallback() {
219 $('#instruct_enabled').prop('checked', true).trigger('input').trigger('change');
220 return '';
221 }
222
223 async function disableInstructCallback() {
224 $('#instruct_enabled').prop('checked', false).trigger('input').trigger('change');
225 return '';
226 }
227
228 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
229 name: 'dupe',
230 callback: duplicateCharacter,
231 helpString: 'Duplicates the currently selected character.',
232 }));
233 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
234 name: 'api',
235 callback: async function (args, text) {
236 if (!text?.toString()?.trim()) {
237 for (const [key, config] of Object.entries(CONNECT_API_MAP)) {
238 if (config.selected !== main_api) continue;
239
240 if (config.source) {
241 if (oai_settings.chat_completion_source === config.source) {
242 return key;
243 } else {
244 continue;
245 }
246 }
247
248 if (config.type) {
249 if (textgenerationwebui_settings.type === config.type) {
250 return key;
251 } else {
252 continue;
253 }
254 }
255
256 return key;
257 }
258
259 console.error('FIXME: The current API is not in the API map');
260 return '';
261 }
262
263 const apiConfig = CONNECT_API_MAP[text?.toString()?.toLowerCase() ?? ''];
264 if (!apiConfig) {
265 toastr.error(t`Error: ${text} is not a valid API`);
266 return '';
267 }
268
269 let connectionRequired = false;
270
271 if (main_api !== apiConfig.selected) {
272 $(`#main_api option[value='${apiConfig.selected || text}']`).prop('selected', true);
273 $('#main_api').trigger('change');
274 connectionRequired = true;
275 }
276
277 if (apiConfig.source && oai_settings.chat_completion_source !== apiConfig.source) {
278 $(`#chat_completion_source option[value='${apiConfig.source}']`).prop('selected', true);
279 $('#chat_completion_source').trigger('change');
280 connectionRequired = true;
281 }
282
283 if (apiConfig.type && textgenerationwebui_settings.type !== apiConfig.type) {
284 $(`#textgen_type option[value='${apiConfig.type}']`).prop('selected', true);
285 $('#textgen_type').trigger('change');
286 connectionRequired = true;
287 }
288
289 if (connectionRequired && apiConfig.button) {
290 $(apiConfig.button).trigger('click');
291 }
292
293 const quiet = isTrueBoolean(args?.quiet?.toString());
294 const toast = quiet ? jQuery() : toastr.info(t`API set to ${text}, trying to connect..`);
295
296 try {
297 if (connectionRequired) {
298 await waitUntilCondition(() => online_status !== 'no_connection', 5000, 100);
299 }
300 console.log('Connection successful');
301 } catch {
302 console.log('Could not connect after 5 seconds, skipping.');
303 }
304
305 toastr.clear(toast);
306 return text?.toString()?.trim() ?? '';
307 },
308 returns: 'the current API',
309 namedArgumentList: [
310 SlashCommandNamedArgument.fromProps({
311 name: 'quiet',
312 description: 'Suppress the toast message on connection',
313 typeList: [ARGUMENT_TYPE.BOOLEAN],
314 defaultValue: 'false',
315 enumList: commonEnumProviders.boolean('trueFalse')(),
316 }),
317 ],
318 unnamedArgumentList: [
319 SlashCommandArgument.fromProps({
320 description: 'API to connect to',
321 typeList: [ARGUMENT_TYPE.STRING],
322 enumList: Object.entries(CONNECT_API_MAP).map(([api, { selected }]) =>
323 new SlashCommandEnumValue(api, selected, enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === selected)),
324 selected[0].toUpperCase() ?? enumIcons.default)),
325 }),
326 ],
327 helpString: `
328 <div>
329 Connect to an API. If no argument is provided, it will return the currently connected API.
330 </div>
331 <div>
332 <strong>Available APIs:</strong>
333 <pre><code>${Object.keys(CONNECT_API_MAP).sort((a, b) => a.localeCompare(b)).join(', ')}</code></pre>
334 </div>
335 `,
336 }));
337 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
338 name: 'impersonate',
339 callback: async function (args, prompt) {
340 const options = prompt?.toString()?.trim() ? { quiet_prompt: prompt.toString().trim(), quietToLoud: true } : {};
341 const shouldAwait = isTrueBoolean(args?.await?.toString());
342 const outerPromise = new Promise((outerResolve) => setTimeout(async () => {
343 try {
344 await waitUntilCondition(() => !is_send_press && !is_group_generating, 10000, 100);
345 } catch {
346 console.warn('Timeout waiting for generation unlock');
347 toastr.warning(t`Cannot run /impersonate command while the reply is being generated.`);
348 return '';
349 }
350
351 // Prevent generate recursion
352 $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
353
354 outerResolve(new Promise(innerResolve => setTimeout(() => innerResolve(Generate('impersonate', options)), 1)));
355 }, 1));
356
357 if (shouldAwait) {
358 const innerPromise = await outerPromise;
359 await innerPromise;
360 }
361
362 return '';
363 }
364 ,
365 aliases: ['imp'],
366 namedArgumentList: [
367 new SlashCommandNamedArgument(
368 'await',
369 'Whether to await for the triggered generation before continuing',
370 [ARGUMENT_TYPE.BOOLEAN],
371 false,
372 false,
373 'false',
374 ),
375 ],
376 unnamedArgumentList: [
377 new SlashCommandArgument(
378 'prompt', [ARGUMENT_TYPE.STRING], false,
379 ),
380 ],
381 helpString: `
382 <div>
383 Calls an impersonation response, with an optional additional prompt.
384 </div>
385 <div>
386 If <code>await=true</code> named argument is passed, the command will wait for the impersonation to end before continuing.
387 </div>
388 <div>
389 <strong>Example:</strong>
390 <ul>
391 <li>
392 <pre><code class="language-stscript">/impersonate What is the meaning of life?</code></pre>
393 </li>
394 </ul>
395 </div>
396 `,
397 }));
398 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
399 name: 'delchat',
400 callback: async function () {
401 return displayPastChats().then(() => new Promise((resolve) => {
402 let resolved = false;
403 const timeOutId = setTimeout(() => {
404 toastr.error(t`Chat deletion timed out. Please try again.`);
405 setResolved();
406 }, 5000);
407
408 const setResolved = () => {
409 if (resolved) {
410 return;
411 }
412 resolved = true;
413 [event_types.CHAT_DELETED, event_types.GROUP_CHAT_DELETED].forEach((eventType) => {
414 eventSource.removeListener(eventType, setResolved);
415 });
416 clearTimeout(timeOutId);
417 resolve('');
418 };
419
420 [event_types.CHAT_DELETED, event_types.GROUP_CHAT_DELETED].forEach((eventType) => {
421 eventSource.on(eventType, setResolved);
422 });
423
424 const currentChatDeleteButton = $('.select_chat_block[highlight=\'true\']').parent().find('.PastChat_cross');
425 $(currentChatDeleteButton).trigger('click', { fromSlashCommand: true });
426 }));
427 },
428 helpString: 'Deletes the current chat.',
429 }));
430 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
431 name: 'renamechat',
432 callback: async function doRenameChat(_, chatName) {
433 if (!chatName) {
434 toastr.warning(t`Name must be provided as an argument to rename this chat.`);
435 return '';
436 }
437
438 const currentChatName = getCurrentChatId();
439 if (!currentChatName) {
440 toastr.warning(t`No chat selected that can be renamed.`);
441 return '';
442 }
443
444 await renameChat(currentChatName, chatName.toString());
445
446 toastr.success(t`Successfully renamed chat to: ${chatName}`);
447 return '';
448 },
449 unnamedArgumentList: [
450 new SlashCommandArgument(
451 'new chat name', [ARGUMENT_TYPE.STRING], true,
452 ),
453 ],
454 helpString: 'Renames the current chat.',
455 }));
456 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
457 name: 'getchatname',
458 callback: async function doGetChatName() {
459 return getCurrentChatDetails().sessionName;
460 },
461 returns: 'chat file name',
462 helpString: 'Returns the name of the current chat file into the pipe.',
463 }));
464 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
465 name: 'closechat',
466 callback: function () {
467 $('#option_close_chat').trigger('click');
468 return '';
469 },
470 helpString: 'Closes the current chat.',
471 }));
472 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
473 name: 'tempchat',
474 callback: () => {
475 return new Promise((resolve, reject) => {
476 const eventCallback = async (chatId) => {
477 if (chatId) {
478 return reject('Not in a temporary chat');
479 }
480 await newAssistantChat({ temporary: true });
481 return resolve('');
482 };
483 eventSource.once(event_types.CHAT_CHANGED, eventCallback);
484 $('#option_close_chat').trigger('click');
485 setTimeout(() => {
486 reject('Failed to open temporary chat');
487 eventSource.removeListener(event_types.CHAT_CHANGED, eventCallback);
488 }, debounce_timeout.relaxed);
489 });
490 },
491 helpString: 'Opens a temporary chat with Assistant.',
492 }));
493 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
494 name: 'panels',
495 callback: function () {
496 $('#option_settings').trigger('click');
497 return '';
498 },
499 aliases: ['togglepanels'],
500 helpString: 'Toggle UI panels on/off',
501 }));
502 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
503 name: 'forcesave',
504 callback: async function () {
505 await saveSettings();
506 await saveChatConditional();
507 toastr.success('Chat and settings saved.');
508 return '';
509 },
510 helpString: 'Forces a save of the current chat and settings',
511 }));
512 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
513 name: 'instruct',
514 callback: async function (args, name) {
515 if (!name) {
516 return power_user.instruct.enabled || isTrueBoolean(args?.forceGet?.toString()) ? power_user.instruct.preset : '';
517 }
518
519 const quiet = isTrueBoolean(args?.quiet?.toString());
520 const instructNames = instruct_presets.map(preset => preset.name);
521 const fuse = new Fuse(instructNames);
522 const result = fuse.search(name?.toString() ?? '');
523
524 if (result.length === 0) {
525 !quiet && toastr.warning(t`Instruct template '${name}' not found`);
526 return '';
527 }
528
529 const foundName = result[0].item;
530 selectInstructPreset(foundName, { quiet: quiet });
531 return foundName;
532 },
533 returns: 'current template',
534 namedArgumentList: [
535 SlashCommandNamedArgument.fromProps({
536 name: 'quiet',
537 description: 'Suppress the toast message on template change',
538 typeList: [ARGUMENT_TYPE.BOOLEAN],
539 defaultValue: 'false',
540 enumList: commonEnumProviders.boolean('trueFalse')(),
541 }),
542 SlashCommandNamedArgument.fromProps({
543 name: 'forceGet',
544 description: 'Force getting a name even if instruct mode is disabled',
545 typeList: [ARGUMENT_TYPE.BOOLEAN],
546 defaultValue: 'false',
547 enumList: commonEnumProviders.boolean('trueFalse')(),
548 }),
549 ],
550 unnamedArgumentList: [
551 SlashCommandArgument.fromProps({
552 description: 'instruct template name',
553 typeList: [ARGUMENT_TYPE.STRING],
554 enumProvider: () => instruct_presets.map(preset => new SlashCommandEnumValue(preset.name, null, enumTypes.enum, enumIcons.preset)),
555 }),
556 ],
557 helpString: `
558 <div>
559 Selects instruct mode template by name. Enables instruct mode if not already enabled.
560 Gets the current instruct template if no name is provided and instruct mode is enabled or <code>forceGet=true</code> is passed.
561 </div>
562 <div>
563 <strong>Example:</strong>
564 <ul>
565 <li>
566 <pre><code class="language-stscript">/instruct creative</code></pre>
567 </li>
568 </ul>
569 </div>
570 `,
571 }));
572 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
573 name: 'instruct-on',
574 callback: enableInstructCallback,
575 helpString: 'Enables instruct mode.',
576 }));
577 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
578 name: 'instruct-off',
579 callback: disableInstructCallback,
580 helpString: 'Disables instruct mode',
581 }));
582 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
583 name: 'instruct-state',
584 aliases: ['instruct-toggle'],
585 helpString: 'Gets the current instruct mode state. If an argument is provided, it will set the instruct mode state.',
586 unnamedArgumentList: [
587 SlashCommandArgument.fromProps({
588 description: 'instruct mode state',
589 typeList: [ARGUMENT_TYPE.BOOLEAN],
590 enumList: commonEnumProviders.boolean('trueFalse')(),
591 }),
592 ],
593 callback: async (_args, state) => {
594 if (!state || typeof state !== 'string') {
595 return String(power_user.instruct.enabled);
596 }
597
598 const newState = isTrueBoolean(state);
599 newState ? enableInstructCallback() : disableInstructCallback();
600 return String(power_user.instruct.enabled);
601 },
602 }));
603 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
604 name: 'context',
605 callback: async function (args, name) {
606 if (!name) {
607 return power_user.context.preset;
608 }
609
610 const quiet = isTrueBoolean(args?.quiet?.toString());
611 const contextNames = context_presets.map(preset => preset.name);
612 const fuse = new Fuse(contextNames);
613 const result = fuse.search(name?.toString() ?? '');
614
615 if (result.length === 0) {
616 !quiet && toastr.warning(t`Context template '${name}' not found`);
617 return '';
618 }
619
620 const foundName = result[0].item;
621 selectContextPreset(foundName, { quiet: quiet });
622 return foundName;
623 },
624 returns: 'template name',
625 namedArgumentList: [
626 SlashCommandNamedArgument.fromProps({
627 name: 'quiet',
628 description: 'Suppress the toast message on template change',
629 typeList: [ARGUMENT_TYPE.BOOLEAN],
630 defaultValue: 'false',
631 enumList: commonEnumProviders.boolean('trueFalse')(),
632 }),
633 ],
634 unnamedArgumentList: [
635 SlashCommandArgument.fromProps({
636 description: 'context template name',
637 typeList: [ARGUMENT_TYPE.STRING],
638 enumProvider: () => context_presets.map(preset => new SlashCommandEnumValue(preset.name, null, enumTypes.enum, enumIcons.preset)),
639 }),
640 ],
641 helpString: 'Selects context template by name. Gets the current template if no name is provided',
642 }));
643 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
644 name: 'chat-manager',
645 callback: () => {
646 $('#option_select_chat').trigger('click');
647 return '';
648 },
649 aliases: ['chat-history', 'manage-chats'],
650 helpString: 'Opens the chat manager for the current character/group.',
651 }));
114 SlashCommandParser.addCommandObject(SlashCommand.fromProps({652 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
115 name: '?',653 name: '?',
116 callback: helpCommandCallback,654 callback: helpCommandCallback,
@@ -3558,7 +4096,7 @@ async function countGroupMemberCallback() {
3558 return '';4096 return '';
3559 }4097 }
35604098
3561 return getGroupMembers(selected_group).length;4099 return String(getGroupMembers(selected_group).length);
3562}4100}
35634101
3564async function removeGroupMemberCallback(_, arg) {4102async function removeGroupMemberCallback(_, arg) {
@@ -4684,18 +5222,21 @@ export function stopScriptExecution() {
4684 */5222 */
4685async function clearCommandProgress() {5223async function clearCommandProgress() {
4686 if (isExecutingCommandsFromChatInput) return;5224 if (isExecutingCommandsFromChatInput) return;
4687 document.querySelector('#send_textarea').style.setProperty('--progDone', '1');5225 const ta = document.getElementById('send_textarea');
5226 const fs = document.getElementById('form_sheld');
5227 if (!ta || !fs) return;
5228 ta.style.setProperty('--progDone', '1');
4688 await delay(250);5229 await delay(250);
4689 if (isExecutingCommandsFromChatInput) return;5230 if (isExecutingCommandsFromChatInput) return;
4690 document.querySelector('#send_textarea').style.transition = 'none';5231 ta.style.transition = 'none';
4691 await delay(1);5232 await delay(1);
4692 document.querySelector('#send_textarea').style.setProperty('--prog', '0%');5233 ta.style.setProperty('--prog', '0%');
4693 document.querySelector('#send_textarea').style.setProperty('--progDone', '0');5234 ta.style.setProperty('--progDone', '0');
4694 document.querySelector('#form_sheld').classList.remove('script_success');5235 fs.classList.remove('script_success');
4695 document.querySelector('#form_sheld').classList.remove('script_error');5236 fs.classList.remove('script_error');
4696 document.querySelector('#form_sheld').classList.remove('script_aborted');5237 fs.classList.remove('script_aborted');
4697 await delay(1);5238 await delay(1);
4698 document.querySelector('#send_textarea').style.transition = null;5239 ta.style.transition = null;
4699}5240}
4700/**5241/**
4701 * Debounced version of clearCommandProgress.5242 * Debounced version of clearCommandProgress.
@@ -4744,17 +5285,18 @@ export async function executeSlashCommandsOnChatInput(text, options = {}) {
47445285
4745 /** @type {HTMLTextAreaElement} */5286 /** @type {HTMLTextAreaElement} */
4746 const ta = document.querySelector('#send_textarea');5287 const ta = document.querySelector('#send_textarea');
5288 const fs = document.querySelector('#form_sheld');
47475289
4748 if (options.clearChatInput) {5290 if (options.clearChatInput) {
4749 ta.value = '';5291 ta.value = '';
4750 ta.dispatchEvent(new Event('input', { bubbles: true }));5292 ta.dispatchEvent(new Event('input', { bubbles: true }));
4751 }5293 }
47525294
4753 document.querySelector('#send_textarea').style.setProperty('--prog', '0%');5295 ta.style.setProperty('--prog', '0%');
4754 document.querySelector('#send_textarea').style.setProperty('--progDone', '0');5296 ta.style.setProperty('--progDone', '0');
4755 document.querySelector('#form_sheld').classList.remove('script_success');5297 fs.classList.remove('script_success');
4756 document.querySelector('#form_sheld').classList.remove('script_error');5298 fs.classList.remove('script_error');
4757 document.querySelector('#form_sheld').classList.remove('script_aborted');5299 fs.classList.remove('script_aborted');
47585300
4759 /**@type {SlashCommandClosureResult} */5301 /**@type {SlashCommandClosureResult} */
4760 let result = null;5302 let result = null;
@@ -4905,7 +5447,7 @@ async function executeSlashCommandsWithOptions(text, options = {}) {
4905 * @param {boolean} handleParserErrors Whether to handle parser errors (show toast on error) or throw5447 * @param {boolean} handleParserErrors Whether to handle parser errors (show toast on error) or throw
4906 * @param {SlashCommandScope} scope The scope to be used when executing the commands.5448 * @param {SlashCommandScope} scope The scope to be used when executing the commands.
4907 * @param {boolean} handleExecutionErrors Whether to handle execution errors (show toast on error) or throw5449 * @param {boolean} handleExecutionErrors Whether to handle execution errors (show toast on error) or throw
4908 * @param {{[id:PARSER_FLAG]:boolean}} parserFlags Parser flags to apply5450 * @param {{[id:import('./slash-commands/SlashCommandParser.js').PARSER_FLAG]:boolean}} parserFlags Parser flags to apply
4909 * @param {SlashCommandAbortController} abortController Controller used to abort or pause command execution5451 * @param {SlashCommandAbortController} abortController Controller used to abort or pause command execution
4910 * @param {(done:number, total:number)=>void} onProgress Callback to handle progress events5452 * @param {(done:number, total:number)=>void} onProgress Callback to handle progress events
4911 * @returns {Promise<SlashCommandClosureResult>}5453 * @returns {Promise<SlashCommandClosureResult>}