Blame Raw
Cohee · 51ad27fb · · 7095 lines (287.0 KB)
4 contributors
1import { Fuse, DOMPurify } from '../lib.js';
2import { canUseNegativeLookbehind, copyText, findPersona, flashHighlight, resolveAvatarData } from './utils.js';
3
4import {
5 Generate,
6 activateSendButtons,
7 addOneMessage,
8 characters,
9 chat,
10 chatElement,
11 chat_metadata,
12 comment_avatar,
13 deactivateSendButtons,
14 default_avatar,
15 deleteCharacter,
16 deleteSwipe,
17 displayPastChats,
18 duplicateCharacter,
19 eventSource,
20 event_types,
21 extension_prompt_roles,
22 extension_prompt_types,
23 extractMessageBias,
24 generateQuietPrompt,
25 generateRaw,
26 getCharacters,
27 getCurrentChatDetails,
28 getCurrentChatId,
29 getFirstDisplayedMessageId,
30 getOneCharacter,
31 getRequestHeaders,
32 getThumbnailUrl,
33 is_send_press,
34 main_api,
35 name1,
36 name2,
37 neutralCharacterName,
38 newAssistantChat,
39 online_status,
40 reloadCurrentChat,
41 removeMacros,
42 renameCharacter,
43 renameChat,
44 saveChatConditional,
45 saveSettings,
46 saveSettingsDebounced,
47 selectCharacterById,
48 select_selected_character,
49 sendMessageAsUser,
50 sendSystemMessage,
51 setActiveCharacter,
52 setActiveGroup,
53 setCharacterId,
54 setCharacterName,
55 setExtensionPrompt,
56 showMoreMessages,
57 swipe,
58 stopGeneration,
59 substituteParams,
60 syncMesToSwipe,
61 system_avatar,
62 system_message_types,
63 this_chid,
64 updateMessageElement,
65} from '../script.js';
66import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
67import { SlashCommandParserError } from './slash-commands/SlashCommandParserError.js';
68import { getMessageTimeStamp, isMobile } from './RossAscends-mods.js';
69import { hideChatMessageRange } from './chats.js';
70import { getContext, saveMetadataDebounced } from './extensions.js';
71import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
72import { findGroupMemberId, groups, is_group_generating, openGroupById, regenerateGroup, resetSelectedGroup, saveGroupChat, selected_group, getGroupMembers } from './group-chats.js';
73import { chat_completion_sources, MINIMAX_ENDPOINT, oai_settings, promptManager, SILICONFLOW_ENDPOINT, ZAI_ENDPOINT } from './openai.js';
74import { user_avatar } from './personas.js';
75import { addEphemeralStoppingString, chat_styles, context_presets, flushEphemeralStoppingStrings, playMessageSound, power_user } from './power-user.js';
76import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
77import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';
78import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, regexFromString, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
79import { registerVariableCommands, resolveVariable } from './variables.js';
80import { registerActionLoaderSlashCommands } from './action-loader-slashcommands.js';
81import { background_settings } from './backgrounds.js';
82import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
83import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js';
84import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
85import { AutoComplete, AUTOCOMPLETE_STATE } from './autocomplete/AutoComplete.js';
86import { SlashCommand } from './slash-commands/SlashCommand.js';
87import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js';
88import { SlashCommandNamedArgumentAssignment } from './slash-commands/SlashCommandNamedArgumentAssignment.js';
89import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
90import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
91import { commonEnumProviders, enumIcons, commonEnumMatchProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
92import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
93import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
94import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';
95import { accountStorage } from './util/AccountStorage.js';
96import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js';
97import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
98import { t } from './i18n.js';
99import { kai_settings } from './kai-settings.js';
100import { instruct_presets, selectContextPreset, selectInstructPreset } from './instruct-mode.js';
101import { debounce_timeout, SWIPE_DIRECTION, SWIPE_SOURCE } from './constants.js';
102export {
103 executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
104};
105
106export const parser = new SlashCommandParser();
107/**
108 * @deprecated Use SlashCommandParser.addCommandObject() instead
109 */
110const registerSlashCommand = SlashCommandParser.addCommand.bind(SlashCommandParser);
111const getSlashCommandsHelp = parser.getHelpString.bind(parser);
112
113/**
114 * Converts a SlashCommandClosure to a filter function that returns a boolean.
115 * @param {SlashCommandClosure} closure
116 * @returns {() => Promise<boolean>}
117 */
118function closureToFilter(closure) {
119 return async () => {
120 try {
121 const localClosure = closure.getCopy();
122 localClosure.onProgress = () => { };
123 const result = await localClosure.execute();
124 return isTrueBoolean(result.pipe);
125 } catch (e) {
126 console.error('Error executing filter closure', e);
127 return false;
128 }
129 };
130}
131
132/**
133 * @typedef {object} ConnectAPIMap
134 * @property {string} selected - API name (e.g. "textgenerationwebui", "openai")
135 * @property {string?} [button] - CSS selector for the API button
136 * @property {string?} [type] - API type, mostly used by text completion. (e.g. "openrouter")
137 * @property {string?} [source] - API source, mostly used by chat completion. (e.g. "openai")
138 */
139
140/** @type {Record<string, ConnectAPIMap>} */
141export const CONNECT_API_MAP = {};
142
143/** @type {string[]} */
144export const UNIQUE_APIS = [];
145
146function setupConnectAPIMap() {
147 /** @type {Record<string, ConnectAPIMap>} */
148 const result = {
149 // Default APIs not contained inside text gen / chat gen
150 'kobold': {
151 selected: 'kobold',
152 button: '#api_button',
153 },
154 'horde': {
155 selected: 'koboldhorde',
156 },
157 'novel': {
158 selected: 'novel',
159 button: '#api_button_novel',
160 },
161 'koboldcpp': {
162 selected: 'textgenerationwebui',
163 button: '#api_button_textgenerationwebui',
164 type: textgen_types.KOBOLDCPP,
165 },
166 // KoboldCpp alias
167 'kcpp': {
168 selected: 'textgenerationwebui',
169 button: '#api_button_textgenerationwebui',
170 type: textgen_types.KOBOLDCPP,
171 },
172 'openai': {
173 selected: 'openai',
174 button: '#api_button_openai',
175 source: chat_completion_sources.OPENAI,
176 },
177 // OpenAI alias
178 'oai': {
179 selected: 'openai',
180 button: '#api_button_openai',
181 source: chat_completion_sources.OPENAI,
182 },
183 // Google alias
184 'google': {
185 selected: 'openai',
186 button: '#api_button_openai',
187 source: chat_completion_sources.MAKERSUITE,
188 },
189 // OpenRouter special naming, to differentiate between chat comp and text comp
190 'openrouter': {
191 selected: 'openai',
192 button: '#api_button_openai',
193 source: chat_completion_sources.OPENROUTER,
194 },
195 'openrouter-text': {
196 selected: 'textgenerationwebui',
197 button: '#api_button_textgenerationwebui',
198 type: textgen_types.OPENROUTER,
199 },
200 };
201
202 // Fill connections map from textgen_types and chat_completion_sources
203 for (const textGenType of Object.values(textgen_types)) {
204 if (result[textGenType]) continue;
205 result[textGenType] = {
206 selected: 'textgenerationwebui',
207 button: '#api_button_textgenerationwebui',
208 type: textGenType,
209 };
210 }
211
212 for (const chatCompletionSource of Object.values(chat_completion_sources)) {
213 if (result[chatCompletionSource]) continue;
214 result[chatCompletionSource] = {
215 selected: 'openai',
216 button: '#api_button_openai',
217 source: chatCompletionSource,
218 };
219 }
220
221 Object.assign(CONNECT_API_MAP, result);
222 UNIQUE_APIS.push(...new Set(Object.values(CONNECT_API_MAP).map(x => x.selected)));
223}
224
225export function initDefaultSlashCommands() {
226 eventSource.on(event_types.CHAT_CHANGED, processChatSlashCommands);
227 setupConnectAPIMap();
228
229 async function enableInstructCallback() {
230 $('#instruct_enabled').prop('checked', true).trigger('input').trigger('change');
231 return '';
232 }
233
234 async function disableInstructCallback() {
235 $('#instruct_enabled').prop('checked', false).trigger('input').trigger('change');
236 return '';
237 }
238
239 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
240 name: 'api',
241 callback: async function (args, text) {
242 if (!text?.toString()?.trim()) {
243 for (const [key, config] of Object.entries(CONNECT_API_MAP)) {
244 if (config.selected !== main_api) continue;
245
246 if (config.source) {
247 if (oai_settings.chat_completion_source === config.source) {
248 return key;
249 } else {
250 continue;
251 }
252 }
253
254 if (config.type) {
255 if (textgenerationwebui_settings.type === config.type) {
256 return key;
257 } else {
258 continue;
259 }
260 }
261
262 return key;
263 }
264
265 console.error('FIXME: The current API is not in the API map');
266 return '';
267 }
268
269 const apiConfig = CONNECT_API_MAP[text?.toString()?.toLowerCase() ?? ''];
270 if (!apiConfig) {
271 toastr.error(t`Error: ${text} is not a valid API`);
272 return '';
273 }
274
275 let connectionRequired = false;
276
277 if (main_api !== apiConfig.selected) {
278 $(`#main_api option[value='${apiConfig.selected || text}']`).prop('selected', true);
279 $('#main_api').trigger('change');
280 connectionRequired = true;
281 }
282
283 if (apiConfig.source && oai_settings.chat_completion_source !== apiConfig.source) {
284 $(`#chat_completion_source option[value='${apiConfig.source}']`).prop('selected', true);
285 $('#chat_completion_source').trigger('change');
286 connectionRequired = true;
287 }
288
289 if (apiConfig.type && textgenerationwebui_settings.type !== apiConfig.type) {
290 $(`#textgen_type option[value='${apiConfig.type}']`).prop('selected', true);
291 $('#textgen_type').trigger('change');
292 connectionRequired = true;
293 }
294
295 if (connectionRequired && apiConfig.button) {
296 $(apiConfig.button).trigger('click');
297 }
298
299 const quiet = isTrueBoolean(args?.quiet?.toString());
300 const toast = quiet ? jQuery() : toastr.info(t`API set to ${text}, trying to connect..`);
301
302 try {
303 if (connectionRequired) {
304 await waitUntilCondition(() => online_status !== 'no_connection', 5000, 100);
305 }
306 console.log('Connection successful');
307 } catch {
308 console.log('Could not connect after 5 seconds, skipping.');
309 }
310
311 toastr.clear(toast);
312 return text?.toString()?.trim() ?? '';
313 },
314 returns: t`the current API`,
315 namedArgumentList: [
316 SlashCommandNamedArgument.fromProps({
317 name: 'quiet',
318 description: t`Suppress the toast message on connection`,
319 typeList: [ARGUMENT_TYPE.BOOLEAN],
320 defaultValue: 'false',
321 enumList: commonEnumProviders.boolean('trueFalse')(),
322 }),
323 ],
324 unnamedArgumentList: [
325 SlashCommandArgument.fromProps({
326 description: t`API to connect to`,
327 typeList: [ARGUMENT_TYPE.STRING],
328 enumList: Object.entries(CONNECT_API_MAP).sort(([a], [b]) => a.localeCompare(b)).map(([api, { selected }]) =>
329 new SlashCommandEnumValue(api, selected, enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === selected)),
330 selected[0].toUpperCase() ?? enumIcons.default)),
331 }),
332 ],
333 helpString: `
334 <div>
335 ${t`Connect to an API. If no argument is provided, it will return the currently connected API.`}
336 </div>
337 <div>
338 <strong>${t`Available APIs:`}</strong>
339 <pre><code>${Object.keys(CONNECT_API_MAP).sort((a, b) => a.localeCompare(b)).join(', ')}</code></pre>
340 </div>
341 `,
342 }));
343 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
344 name: 'impersonate',
345 callback: async function (args, prompt) {
346 const options = prompt?.toString()?.trim() ? { quiet_prompt: prompt.toString().trim(), quietToLoud: true } : {};
347 const shouldAwait = isTrueBoolean(args?.await?.toString());
348 const outerPromise = new Promise((outerResolve) => setTimeout(async () => {
349 try {
350 await waitUntilCondition(() => !is_send_press && !is_group_generating, 10000, 100);
351 } catch {
352 console.warn('Timeout waiting for generation unlock');
353 toastr.warning(t`Cannot run /impersonate command while the reply is being generated.`);
354 return '';
355 }
356
357 // Prevent generate recursion
358 $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
359
360 outerResolve(new Promise(innerResolve => setTimeout(() => innerResolve(Generate('impersonate', options)), 1)));
361 }, 1));
362
363 if (shouldAwait) {
364 const innerPromise = await outerPromise;
365 await innerPromise;
366 }
367
368 return '';
369 }
370 ,
371 aliases: ['imp'],
372 namedArgumentList: [
373 new SlashCommandNamedArgument(
374 'await',
375 t`Whether to await for the triggered generation before continuing`,
376 [ARGUMENT_TYPE.BOOLEAN],
377 false,
378 false,
379 'false',
380 ),
381 ],
382 unnamedArgumentList: [
383 new SlashCommandArgument(
384 'prompt', [ARGUMENT_TYPE.STRING], false,
385 ),
386 ],
387 helpString: `
388 <div>
389 ${t`Calls an impersonation response, with an optional additional prompt.`}
390 </div>
391 <div>
392 ${t`If <code>await=true</code> named argument is passed, the command will wait for the impersonation to end before continuing.`}
393 </div>
394 <div>
395 <strong>${t`Example:`}</strong>
396 <ul>
397 <li>
398 <pre><code class="language-stscript">/impersonate What is the meaning of life?</code></pre>
399 </li>
400 </ul>
401 </div>
402 `,
403 }));
404 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
405 name: 'delchat',
406 callback: async function () {
407 return displayPastChats().then(() => new Promise((resolve) => {
408 let resolved = false;
409 const timeOutId = setTimeout(() => {
410 toastr.error(t`Chat deletion timed out. Please try again.`);
411 setResolved();
412 }, 5000);
413
414 const setResolved = () => {
415 if (resolved) {
416 return;
417 }
418 resolved = true;
419 [event_types.CHAT_DELETED, event_types.GROUP_CHAT_DELETED].forEach((eventType) => {
420 eventSource.removeListener(eventType, setResolved);
421 });
422 clearTimeout(timeOutId);
423 resolve('');
424 };
425
426 [event_types.CHAT_DELETED, event_types.GROUP_CHAT_DELETED].forEach((eventType) => {
427 eventSource.on(eventType, setResolved);
428 });
429
430 const currentChatDeleteButton = $('.select_chat_block[highlight=\'true\']').parent().find('.PastChat_cross');
431 $(currentChatDeleteButton).trigger('click', { fromSlashCommand: true });
432 }));
433 },
434 helpString: t`Deletes the current chat.`,
435 }));
436 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
437 name: 'renamechat',
438 callback: async function doRenameChat(_, chatName) {
439 if (!chatName) {
440 toastr.warning(t`Name must be provided as an argument to rename this chat.`);
441 return '';
442 }
443
444 const currentChatName = getCurrentChatId();
445 if (!currentChatName) {
446 toastr.warning(t`No chat selected that can be renamed.`);
447 return '';
448 }
449
450 await renameChat(currentChatName, chatName.toString());
451
452 toastr.success(t`Successfully renamed chat to: ${chatName}`);
453 return '';
454 },
455 unnamedArgumentList: [
456 new SlashCommandArgument(
457 t`new chat name`, [ARGUMENT_TYPE.STRING], true,
458 ),
459 ],
460 helpString: t`Renames the current chat.`,
461 }));
462 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
463 name: 'getchatname',
464 callback: async function doGetChatName() {
465 return getCurrentChatDetails().sessionName;
466 },
467 returns: t`chat file name`,
468 helpString: t`Returns the name of the current chat file into the pipe.`,
469 }));
470 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
471 name: 'closechat',
472 callback: function () {
473 $('#option_close_chat').trigger('click');
474 return '';
475 },
476 helpString: t`Closes the current chat.`,
477 }));
478 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
479 name: 'tempchat',
480 callback: () => {
481 return new Promise((resolve, reject) => {
482 const eventCallback = async (chatId) => {
483 if (chatId) {
484 return reject(t`Not in a temporary chat`);
485 }
486 await newAssistantChat({ temporary: true });
487 return resolve('');
488 };
489 eventSource.once(event_types.CHAT_CHANGED, eventCallback);
490 $('#option_close_chat').trigger('click');
491 setTimeout(() => {
492 reject(t`Failed to open temporary chat`);
493 eventSource.removeListener(event_types.CHAT_CHANGED, eventCallback);
494 }, debounce_timeout.relaxed);
495 });
496 },
497 helpString: t`Opens a temporary chat with Assistant.`,
498 }));
499 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
500 name: 'panels',
501 callback: function () {
502 $('#option_settings').trigger('click');
503 return '';
504 },
505 aliases: ['togglepanels'],
506 helpString: t`Toggle UI panels on/off`,
507 }));
508 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
509 name: 'forcesave',
510 callback: async function () {
511 await saveSettings();
512 await saveChatConditional();
513 toastr.success(t`Chat and settings saved.`);
514 return '';
515 },
516 helpString: t`Forces a save of the current chat and settings`,
517 }));
518 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
519 name: 'instruct',
520 callback: async function (args, name) {
521 if (!name) {
522 return power_user.instruct.enabled || isTrueBoolean(args?.forceGet?.toString()) ? power_user.instruct.preset : '';
523 }
524
525 const quiet = isTrueBoolean(args?.quiet?.toString());
526 const instructNames = instruct_presets.map(preset => preset.name);
527 const fuse = new Fuse(instructNames);
528 const result = fuse.search(name?.toString() ?? '');
529
530 if (result.length === 0) {
531 !quiet && toastr.warning(t`Instruct template '${name}' not found`);
532 return '';
533 }
534
535 const foundName = result[0].item;
536 selectInstructPreset(foundName, { quiet: quiet });
537 return foundName;
538 },
539 returns: t`current template`,
540 namedArgumentList: [
541 SlashCommandNamedArgument.fromProps({
542 name: 'quiet',
543 description: t`Suppress the toast message on template change`,
544 typeList: [ARGUMENT_TYPE.BOOLEAN],
545 defaultValue: 'false',
546 enumList: commonEnumProviders.boolean('trueFalse')(),
547 }),
548 SlashCommandNamedArgument.fromProps({
549 name: 'forceGet',
550 description: t`Force getting a name even if instruct mode is disabled`,
551 typeList: [ARGUMENT_TYPE.BOOLEAN],
552 defaultValue: 'false',
553 enumList: commonEnumProviders.boolean('trueFalse')(),
554 }),
555 ],
556 unnamedArgumentList: [
557 SlashCommandArgument.fromProps({
558 description: t`instruct template name`,
559 typeList: [ARGUMENT_TYPE.STRING],
560 enumProvider: () => instruct_presets.map(preset => new SlashCommandEnumValue(preset.name, null, enumTypes.enum, enumIcons.preset)),
561 }),
562 ],
563 helpString: `
564 <div>
565 ${t`Selects instruct mode template by name. Enables instruct mode if not already enabled.`}
566 ${t`Gets the current instruct template if no name is provided and instruct mode is enabled or <code>forceGet=true</code> is passed.`}
567 </div>
568 <div>
569 <strong>${t`Example:`}</strong>
570 <ul>
571 <li>
572 <pre><code class="language-stscript">/instruct creative</code></pre>
573 </li>
574 </ul>
575 </div>
576 `,
577 }));
578 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
579 name: 'instruct-on',
580 callback: enableInstructCallback,
581 helpString: t`Enables instruct mode.`,
582 }));
583 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
584 name: 'instruct-off',
585 callback: disableInstructCallback,
586 helpString: t`Disables instruct mode`,
587 }));
588 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
589 name: 'instruct-state',
590 aliases: ['instruct-toggle'],
591 helpString: t`Gets the current instruct mode state. If an argument is provided, it will set the instruct mode state.`,
592 unnamedArgumentList: [
593 SlashCommandArgument.fromProps({
594 description: t`instruct mode state`,
595 typeList: [ARGUMENT_TYPE.BOOLEAN],
596 enumList: commonEnumProviders.boolean('trueFalse')(),
597 }),
598 ],
599 callback: async (_args, state) => {
600 if (!state || typeof state !== 'string') {
601 return String(power_user.instruct.enabled);
602 }
603
604 const newState = isTrueBoolean(state);
605 newState ? enableInstructCallback() : disableInstructCallback();
606 return String(power_user.instruct.enabled);
607 },
608 }));
609 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
610 name: 'context',
611 callback: async function (args, name) {
612 if (!name) {
613 return power_user.context.preset;
614 }
615
616 const quiet = isTrueBoolean(args?.quiet?.toString());
617 const contextNames = context_presets.map(preset => preset.name);
618 const fuse = new Fuse(contextNames);
619 const result = fuse.search(name?.toString() ?? '');
620
621 if (result.length === 0) {
622 !quiet && toastr.warning(t`Context template '${name}' not found`);
623 return '';
624 }
625
626 const foundName = result[0].item;
627 selectContextPreset(foundName, { quiet: quiet });
628 return foundName;
629 },
630 returns: t`template name`,
631 namedArgumentList: [
632 SlashCommandNamedArgument.fromProps({
633 name: 'quiet',
634 description: t`Suppress the toast message on template change`,
635 typeList: [ARGUMENT_TYPE.BOOLEAN],
636 defaultValue: 'false',
637 enumList: commonEnumProviders.boolean('trueFalse')(),
638 }),
639 ],
640 unnamedArgumentList: [
641 SlashCommandArgument.fromProps({
642 description: t`context template name`,
643 typeList: [ARGUMENT_TYPE.STRING],
644 enumProvider: () => context_presets.map(preset => new SlashCommandEnumValue(preset.name, null, enumTypes.enum, enumIcons.preset)),
645 }),
646 ],
647 helpString: t`Selects context template by name. Gets the current template if no name is provided`,
648 }));
649 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
650 name: 'chat-manager',
651 callback: () => {
652 $('#option_select_chat').trigger('click');
653 return '';
654 },
655 aliases: ['chat-history', 'manage-chats'],
656 helpString: t`Opens the chat manager for the current character/group.`,
657 }));
658 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
659 name: '?',
660 callback: helpCommandCallback,
661 aliases: ['help'],
662 unnamedArgumentList: [SlashCommandArgument.fromProps({
663 description: t`help topic`,
664 typeList: [ARGUMENT_TYPE.STRING],
665 enumList: [
666 new SlashCommandEnumValue('slash', t`slash commands (STscript)`, enumTypes.command, '/'),
667 new SlashCommandEnumValue('macros', t`{{macros}} (text replacement)`, enumTypes.macro, enumIcons.macro),
668 new SlashCommandEnumValue('format', t`chat/text formatting`, enumTypes.name, '★'),
669 new SlashCommandEnumValue('hotkeys', t`keyboard shortcuts`, enumTypes.enum, '⏎'),
670 ],
671 })],
672 helpString: t`Get help on macros, chat formatting and commands.`,
673 }));
674 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
675 name: 'bg',
676 callback: setBackgroundCallback,
677 aliases: ['background'],
678 returns: t`the current background`,
679 unnamedArgumentList: [
680 SlashCommandArgument.fromProps({
681 description: t`background filename`,
682 typeList: [ARGUMENT_TYPE.STRING],
683 enumProvider: commonEnumProviders.backgrounds,
684 }),
685 ],
686 helpString: `
687 <div>
688 ${t`Sets a background according to the provided filename. Partial names allowed.`}
689 </div>
690 <div>
691 ${t`If no background is provided, this will return the currently selected background.`}
692 </div>
693 <div>
694 <strong>${t`Example:`}</strong>
695 <ul>
696 <li>
697 <pre><code>/bg beach.jpg</code></pre>
698 </li>
699 <li>
700 <pre><code>/bg</code></pre>
701 </li>
702 </ul>
703 </div>
704 `,
705 }));
706 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
707 name: 'char-find',
708 aliases: ['findchar'],
709 callback: (args, name) => {
710 if (typeof name !== 'string') throw new Error(t`name must be a string`);
711 if (args.preferCurrent instanceof SlashCommandClosure || Array.isArray(args.preferCurrent)) throw new Error(t`preferCurrent cannot be a closure or array`);
712 if (args.quiet instanceof SlashCommandClosure || Array.isArray(args.quiet)) throw new Error(t`quiet cannot be a closure or array`);
713
714 const char = findChar({ name: name, filteredByTags: validateArrayArgString(args.tag, 'tag'), preferCurrentChar: !isFalseBoolean(args.preferCurrent), quiet: isTrueBoolean(args.quiet) });
715 return char?.avatar ?? '';
716 },
717 returns: t`the avatar key (unique identifier) of the character`,
718 namedArgumentList: [
719 SlashCommandNamedArgument.fromProps({
720 name: 'tag',
721 description: t`Supply one or more tags to filter down to the correct character for the provided name, if multiple characters have the same name.`,
722 typeList: [ARGUMENT_TYPE.STRING],
723 enumProvider: commonEnumProviders.tags('assigned'),
724 acceptsMultiple: true,
725 }),
726 SlashCommandNamedArgument.fromProps({
727 name: 'preferCurrent',
728 description: t`Prefer current character or characters in a group, if multiple characters match`,
729 typeList: [ARGUMENT_TYPE.BOOLEAN],
730 defaultValue: 'true',
731 }),
732 SlashCommandNamedArgument.fromProps({
733 name: 'quiet',
734 description: t`Do not show warning if multiple charactrers are found`,
735 typeList: [ARGUMENT_TYPE.BOOLEAN],
736 defaultValue: 'false',
737 enumProvider: commonEnumProviders.boolean('trueFalse'),
738 }),
739 ],
740 unnamedArgumentList: [
741 SlashCommandArgument.fromProps({
742 description: t`Character name - or unique character identifier (avatar key)`,
743 typeList: [ARGUMENT_TYPE.STRING],
744 enumProvider: commonEnumProviders.characters('character'),
745 }),
746 ],
747 helpString: `
748 <div>
749 ${t`Searches for a character and returns its avatar key.`}
750 </div>
751 <div>
752 ${t`This can be used to choose the correct character for something like <code>/sendas</code> or other commands in need of a character name if you have multiple characters with the same name.`}
753 </div>
754 <div>
755 <strong>${t`Example:`}</strong>
756 <ul>
757 <li>
758 <pre><code>/char-find name="Chloe"</code></pre>
759 ${t`Returns the avatar key for "Chloe".`}
760 </li>
761 <li>
762 <pre><code>/search name="Chloe" tag="friend"</code></pre>
763 ${t`Returns the avatar key for the character "Chloe" that is tagged with "friend".`}
764 ${t`This is useful if you for example have multiple characters named "Chloe", and the others are "foe", "goddess", or anything else, so you can actually select the character you are looking for.`}
765 </li>
766 </ul>
767 </div>
768 `,
769 }));
770
771 // Shared character field definitions for char CRUD commands
772 const getCharacterFieldArgs = ({ requiredFields = [] } = {}) => [
773 SlashCommandNamedArgument.fromProps({
774 name: 'name',
775 description: t`The name of the character`,
776 typeList: [ARGUMENT_TYPE.STRING],
777 isRequired: requiredFields.includes('name'),
778 }),
779 SlashCommandNamedArgument.fromProps({
780 name: 'description',
781 description: t`The character's description/personality definition`,
782 typeList: [ARGUMENT_TYPE.STRING],
783 isRequired: requiredFields.includes('description'),
784 }),
785 SlashCommandNamedArgument.fromProps({
786 name: 'firstMessage',
787 description: t`The character's first message/greeting`,
788 typeList: [ARGUMENT_TYPE.STRING],
789 isRequired: requiredFields.includes('firstMessage'),
790 }),
791 SlashCommandNamedArgument.fromProps({
792 name: 'personality',
793 description: t`A brief description of the personality`,
794 typeList: [ARGUMENT_TYPE.STRING],
795 isRequired: requiredFields.includes('personality'),
796 }),
797 SlashCommandNamedArgument.fromProps({
798 name: 'scenario',
799 description: t`The scenario or circumstances for the conversation`,
800 typeList: [ARGUMENT_TYPE.STRING],
801 isRequired: requiredFields.includes('scenario'),
802 }),
803 SlashCommandNamedArgument.fromProps({
804 name: 'messageExamples',
805 description: t`Example messages for the character`,
806 typeList: [ARGUMENT_TYPE.STRING],
807 isRequired: requiredFields.includes('messageExamples'),
808 }),
809 SlashCommandNamedArgument.fromProps({
810 name: 'creatorNotes',
811 description: t`Notes from the character creator`,
812 typeList: [ARGUMENT_TYPE.STRING],
813 isRequired: requiredFields.includes('creatorNotes'),
814 }),
815 SlashCommandNamedArgument.fromProps({
816 name: 'systemPrompt',
817 description: t`The character's system prompt`,
818 typeList: [ARGUMENT_TYPE.STRING],
819 isRequired: requiredFields.includes('systemPrompt'),
820 }),
821 SlashCommandNamedArgument.fromProps({
822 name: 'postHistoryInstructions',
823 description: t`Post-history instructions (jailbreak)`,
824 typeList: [ARGUMENT_TYPE.STRING],
825 isRequired: requiredFields.includes('postHistoryInstructions'),
826 }),
827 SlashCommandNamedArgument.fromProps({
828 name: 'creator',
829 description: t`The creator of the character`,
830 typeList: [ARGUMENT_TYPE.STRING],
831 isRequired: requiredFields.includes('creator'),
832 }),
833 SlashCommandNamedArgument.fromProps({
834 name: 'characterVersion',
835 description: t`The version of the character`,
836 typeList: [ARGUMENT_TYPE.STRING],
837 isRequired: requiredFields.includes('characterVersion'),
838 }),
839 SlashCommandNamedArgument.fromProps({
840 name: 'tags',
841 description: t`Comma-separated list of character card tags (embedded in the card, not ST's folder/filter tags). Use /tag-add for ST tags or /tag-import to import card tags as ST tags.`,
842 typeList: [ARGUMENT_TYPE.STRING],
843 isRequired: requiredFields.includes('tags'),
844 }),
845 SlashCommandNamedArgument.fromProps({
846 name: 'favorite',
847 description: t`Whether this character is a favorite`,
848 typeList: [ARGUMENT_TYPE.BOOLEAN],
849 enumProvider: commonEnumProviders.boolean('trueFalse'),
850 isRequired: requiredFields.includes('favorite'),
851 }),
852 SlashCommandNamedArgument.fromProps({
853 name: 'avatar',
854 description: t`Avatar image. Use "prompt" to open file picker, or provide a local ST file path (e.g., characters/Name.png, backgrounds/image.png). This can also be the return value from the /imagine command. External URLs are not supported.`,
855 typeList: [ARGUMENT_TYPE.STRING],
856 isRequired: requiredFields.includes('avatar'),
857 enumList: [
858 new SlashCommandEnumValue('prompt', 'Open file picker to select an image', 'enum', '📁'),
859 new SlashCommandEnumValue('characters/...', 'Character avatars path (e.g., characters/Name.png)', 'enum', '📄', (input) => commonEnumMatchProviders.folderEnum(input, 'characters/'), () => 'characters/'),
860 new SlashCommandEnumValue('backgrounds/...', 'Background image path', 'enum', '📄', (input) => commonEnumMatchProviders.folderEnum(input, 'backgrounds/'), () => 'backgrounds/'),
861 new SlashCommandEnumValue('User Avatars/...', 'User avatar path', 'enum', '📄', (input) => commonEnumMatchProviders.folderEnum(input, 'User Avatars/'), () => 'User Avatars/'),
862 new SlashCommandEnumValue('assets/...', 'Asset file path', 'enum', '📄', (input) => commonEnumMatchProviders.folderEnum(input, 'assets/'), () => 'assets/'),
863 new SlashCommandEnumValue('user/images/...', 'User image path', 'enum', '📄', (input) => commonEnumMatchProviders.folderEnum(input, 'user/images/'), () => 'user/images/'),
864 ],
865 }),
866 SlashCommandNamedArgument.fromProps({
867 name: 'avatarPromptResize',
868 description: t`Whether to show the avatar resize/crop dialog when uploading (default: true). Ignored if "Never resize avatars" is enabled in settings.`,
869 typeList: [ARGUMENT_TYPE.BOOLEAN],
870 defaultValue: 'true',
871 enumProvider: commonEnumProviders.boolean('trueFalse'),
872 }),
873 SlashCommandNamedArgument.fromProps({
874 name: 'talkativeness',
875 description: t`How often the character speaks in group chats (0.0 to 1.0)`,
876 typeList: [ARGUMENT_TYPE.NUMBER],
877 isRequired: requiredFields.includes('talkativeness'),
878 }),
879 SlashCommandNamedArgument.fromProps({
880 name: 'world',
881 description: t`The name of the lorebook to attach`,
882 typeList: [ARGUMENT_TYPE.STRING],
883 enumProvider: commonEnumProviders.worlds,
884 isRequired: requiredFields.includes('world'),
885 }),
886 SlashCommandNamedArgument.fromProps({
887 name: 'depthPrompt',
888 description: t`Character-specific depth prompt content`,
889 typeList: [ARGUMENT_TYPE.STRING],
890 isRequired: requiredFields.includes('depthPrompt'),
891 }),
892 SlashCommandNamedArgument.fromProps({
893 name: 'depthPromptDepth',
894 description: t`Depth for the character-specific depth prompt`,
895 typeList: [ARGUMENT_TYPE.NUMBER],
896 isRequired: requiredFields.includes('depthPromptDepth'),
897 }),
898 SlashCommandNamedArgument.fromProps({
899 name: 'depthPromptRole',
900 description: t`Role for the depth prompt`,
901 typeList: [ARGUMENT_TYPE.STRING],
902 enumList: commonEnumProviders.messageRoles(),
903 isRequired: requiredFields.includes('depthPromptRole'),
904 }),
905 ];
906
907 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
908 name: 'char-create',
909 callback: createCharacterCallback,
910 returns: t`the avatar key (unique identifier) of the created character`,
911 namedArgumentList: [
912 ...getCharacterFieldArgs({ requiredFields: ['name'] }),
913 SlashCommandNamedArgument.fromProps({
914 name: 'select',
915 description: t`Whether to select/open the character after creation (default: true)`,
916 typeList: [ARGUMENT_TYPE.BOOLEAN],
917 defaultValue: 'true',
918 enumProvider: commonEnumProviders.boolean('trueFalse'),
919 }),
920 ],
921 helpString: `
922 <div>
923 ${t`Creates a new character with the specified attributes. Returns the avatar key of the created character.`}
924 </div>
925 <div>
926 <strong>${t`Required arguments:`}</strong>
927 <ul>
928 <li><code>name</code> - ${t`The character's name`}</li>
929 </ul>
930 </div>
931 <div>
932 <strong>${t`Note on tags:`}</strong> ${t`The <code>tags</code> argument sets character card tags (embedded in the character file), not SillyTavern's folder/filter tags. To add ST tags after creation, use <code>/tag-add</code>. To import card tags as ST tags, use <code>/tag-import</code>.`}
933 </div>
934 <div>
935 <strong>${t`Note on avatar:`}</strong> ${t`The <code>avatar</code> argument accepts <code>prompt</code> to open a file picker, or a local ST file path. Supported paths include: <code>characters/Name.png</code>, <code>backgrounds/image.png</code>, <code>User Avatars/avatar.png</code>, <code>assets/category/file.png</code>. This can also be the return value from the /imagine command. External URLs are not supported.`}
936 </div>
937 <div>
938 <strong>${t`Example:`}</strong>
939 <ul>
940 <li>
941 <pre><code>/char-create name="Alice" description="A friendly AI assistant" firstMessage="Hello! How can I help you today?"</code></pre>
942 </li>
943 <li>
944 <pre><code>/char-create name="Bob" description="A wise wizard" firstMessage="Greetings, traveler." personality="Wise, patient" scenario="A magical library" favorite=true</code></pre>
945 </li>
946 <li>
947 <pre><code>/char-create name="Clone" description="A clone" firstMessage="Hi!" avatar=prompt</code></pre>
948 <span>${t`(opens file picker for avatar)`}</span>
949 </li>
950 </ul>
951 </div>
952 `,
953 }));
954 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
955 name: 'char-update',
956 callback: updateCharacterCallback,
957 returns: t`the avatar key of the updated character`,
958 namedArgumentList: [
959 SlashCommandNamedArgument.fromProps({
960 name: 'char',
961 description: t`Character name or avatar key. If not provided, uses the currently selected character.`,
962 typeList: [ARGUMENT_TYPE.STRING],
963 enumProvider: commonEnumProviders.characters('character'),
964 }),
965 ...getCharacterFieldArgs(),
966 ],
967 helpString: `
968 <div>
969 ${t`Updates an existing character's attributes. The character does not need to be currently selected.`}
970 </div>
971 <div>
972 ${t`If no <code>char</code> argument is provided, updates the currently selected character.`}
973 </div>
974 <div>
975 <strong>${t`Note on tags:`}</strong> ${t`The <code>tags</code> argument sets character card tags (embedded in the PNG), not SillyTavern's folder/filter tags. To add ST tags, use <code>/tag-add</code>. To import card tags as ST tags, use <code>/tag-import</code>.`}
976 </div>
977 <div>
978 <strong>${t`Note on avatar:`}</strong> ${t`The <code>avatar</code> argument accepts <code>prompt</code> to open a file picker, or a local ST file path. Supported paths: <code>characters/Name.png</code>, <code>backgrounds/image.png</code>, <code>User Avatars/avatar.png</code>, <code>assets/category/file.png</code>. This can also be the return value from the /imagine command. External URLs are not supported.`}
979 </div>
980 <div>
981 <strong>${t`Example:`}</strong>
982 <ul>
983 <li>
984 <pre><code>/char-update description="An updated description for this character"</code></pre>
985 ${t`Updates the currently selected character's description.`}
986 </li>
987 <li>
988 <pre><code>/char-update char="Alice" personality="Cheerful and energetic" favorite=true</code></pre>
989 ${t`Updates Alice's personality and marks her as a favorite.`}
990 </li>
991 <li>
992 <pre><code>/imagine you | /char-update avatar="{{pipe}}"</code></pre>
993 ${t`Generates an image and sets it as the current character's avatar.`}
994 </li>
995 </ul>
996 </div>
997 `,
998 }));
999 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1000 name: 'char-duplicate',
1001 aliases: ['dupe'],
1002 callback: duplicateCharacterCallback,
1003 returns: t`the avatar key (unique identifier) of the duplicated character`,
1004 namedArgumentList: [
1005 SlashCommandNamedArgument.fromProps({
1006 name: 'char',
1007 description: t`Character name or avatar key to duplicate. If not provided, uses the currently selected character.`,
1008 typeList: [ARGUMENT_TYPE.STRING],
1009 enumProvider: commonEnumProviders.characters('character'),
1010 }),
1011 SlashCommandNamedArgument.fromProps({
1012 name: 'select',
1013 description: t`Whether to select/open the duplicated character after creation (default: false)`,
1014 typeList: [ARGUMENT_TYPE.BOOLEAN],
1015 defaultValue: 'false',
1016 enumProvider: commonEnumProviders.boolean('trueFalse'),
1017 }),
1018 ],
1019 helpString: `
1020 <div>
1021 ${t`Duplicates a character. Returns the avatar key of the duplicated character.`}
1022 </div>
1023 <div>
1024 ${t`Use <code>/char-update</code> afterwards to modify the duplicated character's fields.`}
1025 </div>
1026 <div>
1027 <strong>${t`Example:`}</strong>
1028 <ul>
1029 <li>
1030 <pre><code>/char-duplicate</code></pre>
1031 ${t`Duplicates the currently selected character.`}
1032 </li>
1033 <li>
1034 <pre><code>/char-duplicate char="Alice" select=true</code></pre>
1035 ${t`Duplicates Alice and selects the new character.`}
1036 </li>
1037 <li>
1038 <pre><code>/char-duplicate | /setvar key=newChar | /char-update char="{{getvar::newChar}}" name="Clone"</code></pre>
1039 ${t`Duplicates the current character and renames the clone.`}
1040 </li>
1041 </ul>
1042 </div>
1043 `,
1044 }));
1045 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1046 name: 'char-get',
1047 aliases: ['char-data'],
1048 callback: getCharacterDataCallback,
1049 returns: t`character data as JSON or a specific field value`,
1050 namedArgumentList: [
1051 SlashCommandNamedArgument.fromProps({
1052 name: 'char',
1053 description: t`Character name or avatar key. If not provided, uses the currently selected character.`,
1054 typeList: [ARGUMENT_TYPE.STRING],
1055 enumProvider: commonEnumProviders.characters('character'),
1056 }),
1057 SlashCommandNamedArgument.fromProps({
1058 name: 'field',
1059 description: t`Specific field to retrieve. If not provided, returns the entire character data.`,
1060 typeList: [ARGUMENT_TYPE.STRING],
1061 enumList: [
1062 new SlashCommandEnumValue('name', t`Character name`, enumTypes.enum),
1063 new SlashCommandEnumValue('description', t`Character description`, enumTypes.enum),
1064 new SlashCommandEnumValue('personality', t`Character personality`, enumTypes.enum),
1065 new SlashCommandEnumValue('scenario', t`Character scenario`, enumTypes.enum),
1066 new SlashCommandEnumValue('first_mes', t`First message`, enumTypes.enum),
1067 new SlashCommandEnumValue('mes_example', t`Message examples`, enumTypes.enum),
1068 new SlashCommandEnumValue('creator_notes', t`Creator notes`, enumTypes.enum),
1069 new SlashCommandEnumValue('system_prompt', t`System prompt`, enumTypes.enum),
1070 new SlashCommandEnumValue('post_history_instructions', t`Post-history instructions`, enumTypes.enum),
1071 new SlashCommandEnumValue('creator', t`Creator name`, enumTypes.enum),
1072 new SlashCommandEnumValue('character_version', t`Character version`, enumTypes.enum),
1073 new SlashCommandEnumValue('tags', t`Character tags`, enumTypes.enum),
1074 new SlashCommandEnumValue('talkativeness', t`Talkativeness`, enumTypes.enum),
1075 new SlashCommandEnumValue('avatar', t`Avatar filename`, enumTypes.enum),
1076 new SlashCommandEnumValue('fav', t`Favorite status`, enumTypes.enum),
1077 ],
1078 }),
1079 SlashCommandNamedArgument.fromProps({
1080 name: 'return',
1081 description: t`The way to return the result`,
1082 typeList: [ARGUMENT_TYPE.STRING],
1083 defaultValue: 'pipe',
1084 enumList: slashCommandReturnHelper.enumList({ allowPipe: true, allowObject: true, allowChat: false, allowPopup: true, allowTextVersion: false }),
1085 }),
1086 ],
1087 helpString: `
1088 <div>
1089 ${t`Retrieves character data. Can get all data or a specific field.`}
1090 </div>
1091 <div>
1092 <strong>${t`Example:`}</strong>
1093 <ul>
1094 <li>
1095 <pre><code>/char-get field=description | /echo</code></pre>
1096 ${t`Outputs the current character's description.`}
1097 </li>
1098 <li>
1099 <pre><code>/char-get char="Alice" field=personality</code></pre>
1100 ${t`Returns Alice's personality field.`}
1101 </li>
1102 <li>
1103 <pre><code>/char-get char="Bob" return=object</code></pre>
1104 ${t`Returns Bob's entire character data as an object.`}
1105 </li>
1106 </ul>
1107 </div>
1108 `,
1109 }));
1110 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1111 name: 'char-delete',
1112 callback: deleteCharacterCallback,
1113 returns: t`true if the character was deleted, false otherwise`,
1114 namedArgumentList: [
1115 SlashCommandNamedArgument.fromProps({
1116 name: 'char',
1117 description: t`Character name or avatar key. If not provided, uses the currently selected character.`,
1118 typeList: [ARGUMENT_TYPE.STRING],
1119 enumProvider: commonEnumProviders.characters('character'),
1120 }),
1121 SlashCommandNamedArgument.fromProps({
1122 name: 'deleteChats',
1123 description: t`Whether to also delete all chats with this character`,
1124 typeList: [ARGUMENT_TYPE.BOOLEAN],
1125 defaultValue: 'false',
1126 enumProvider: commonEnumProviders.boolean('trueFalse'),
1127 }),
1128 SlashCommandNamedArgument.fromProps({
1129 name: 'silent',
1130 description: t`Skip the confirmation popup`,
1131 typeList: [ARGUMENT_TYPE.BOOLEAN],
1132 defaultValue: 'false',
1133 enumProvider: commonEnumProviders.boolean('trueFalse'),
1134 }),
1135 ],
1136 helpString: `
1137 <div>
1138 ${t`Deletes a character from the system.`}
1139 </div>
1140 <div>
1141 ${t`If no <code>char</code> argument is provided, deletes the currently selected character.`}
1142 </div>
1143 <div>
1144 <strong>${t`Warning:`}</strong> ${t`This action is irreversible!`}
1145 </div>
1146 <div>
1147 <strong>${t`Example:`}</strong>
1148 <ul>
1149 <li>
1150 <pre><code>/char-delete</code></pre>
1151 ${t`Deletes the currently selected character (will show confirmation popup).`}
1152 </li>
1153 <li>
1154 <pre><code>/char-delete char="Bob" deleteChats=true silent=true</code></pre>
1155 ${t`Deletes Bob and all associated chats without confirmation.`}
1156 </li>
1157 </ul>
1158 </div>
1159 `,
1160 }));
1161 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1162 name: 'message-role',
1163 callback: messageRoleCallback,
1164 returns: 'The role of the message sender',
1165 namedArgumentList: [
1166 SlashCommandNamedArgument.fromProps({
1167 name: 'at',
1168 description: 'the ID of the message to modify (index-based, corresponding to message id). If omitted, the last message is chosen.\nNegative values are accepted and will work similarly to how \'depth\' usually works. For example, -1 will modify the message right before the last message in chat. At must be nonzero.',
1169 typeList: [ARGUMENT_TYPE.NUMBER],
1170 defaultValue: '',
1171 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
1172 }),
1173 ],
1174 unnamedArgumentList: [
1175 SlashCommandArgument.fromProps({
1176 description: 'Role to set for the message sender (user, assistant, system)',
1177 typeList: [ARGUMENT_TYPE.STRING],
1178 isRequired: false,
1179 enumProvider: commonEnumProviders.messageRoles,
1180 }),
1181 ],
1182 helpString: `
1183 <div>
1184 Changes the role of a message sender to one of your choice.
1185 If no role is provided, just gets the current role of the message sender.
1186 If no index is provided, the last message is chosen.
1187 </div>
1188 <div>
1189 <strong>Example:</strong>
1190 <ul>
1191 <li>
1192 <pre><code>/message-role | /echo</code></pre>
1193 Will output the role of the sender of the last message.
1194 </li>
1195 <li>
1196 <pre><code>/message-role at=-2 assistant</code></pre>
1197 Will change the third message from the bottom to be sent by the assistant.
1198 </li>
1199 </ul>
1200 </div>
1201 `,
1202 }));
1203 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1204 name: 'message-name',
1205 callback: messageNameCallback,
1206 returns: 'The name of the message sender',
1207 namedArgumentList: [
1208 SlashCommandNamedArgument.fromProps({
1209 name: 'at',
1210 description: 'the ID of the message to modify (index-based, corresponding to message id). If omitted, the last message is chosen.\nNegative values are accepted and will work similarly to how \'depth\' usually works. For example, -1 will modify the message right before the last message in chat. At must be nonzero.',
1211 typeList: [ARGUMENT_TYPE.NUMBER],
1212 defaultValue: '',
1213 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
1214 }),
1215 ],
1216 unnamedArgumentList: [
1217 SlashCommandArgument.fromProps({
1218 description: 'Persona name, character name, or unique character identifier (avatar key)',
1219 typeList: [ARGUMENT_TYPE.STRING],
1220 isRequired: false,
1221 enumProvider: (executor) => {
1222 let modifyAt = Number(executor.namedArgumentList.find(arg => arg.name === 'at')?.value ?? (chat.length - 1));
1223 if (!isNaN(modifyAt) && (modifyAt < 0 || Object.is(modifyAt, -0))) {
1224 modifyAt = chat.length + modifyAt;
1225 }
1226 return chat[modifyAt]?.is_user
1227 ? commonEnumProviders.personas()()
1228 : commonEnumProviders.characters('character')();
1229 },
1230 }),
1231 ],
1232 helpString: `
1233 <div>
1234 Changes the name of a message sender to one of your choice.
1235 If no name is provided, just gets the current name of the message sender.
1236 If no index is provided, the last message is chosen.
1237 </div>
1238 <div>
1239 <strong>Example:</strong>
1240 <ul>
1241 <li>
1242 <pre><code>/message-name | /echo</code></pre>
1243 Will output the name of the sender of the last message.
1244 </li>
1245 <li>
1246 <pre><code>/message-name at=-2 "Chloe"</code></pre>
1247 Will change the third message from the bottom to be sent by "Chloe".
1248 </li>
1249 </ul>
1250 </div>
1251 `,
1252 }));
1253 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1254 name: 'sendas',
1255 rawQuotes: true,
1256 callback: sendMessageAs,
1257 returns: t`Optionally the text of the sent message, if specified in the "return" argument`,
1258 namedArgumentList: [
1259 SlashCommandNamedArgument.fromProps({
1260 name: 'name',
1261 description: t`Character name - or unique character identifier (avatar key)`,
1262 typeList: [ARGUMENT_TYPE.STRING],
1263 isRequired: true,
1264 enumProvider: commonEnumProviders.characters('character'),
1265 }),
1266 SlashCommandNamedArgument.fromProps({
1267 name: 'avatar',
1268 description: t`Character avatar override (Can be either avatar key or just the character name to pull the avatar from)`,
1269 typeList: [ARGUMENT_TYPE.STRING],
1270 enumProvider: commonEnumProviders.characters('character'),
1271 }),
1272 SlashCommandNamedArgument.fromProps({
1273 name: 'compact',
1274 description: t`Use compact layout`,
1275 typeList: [ARGUMENT_TYPE.BOOLEAN],
1276 defaultValue: 'false',
1277 }),
1278 SlashCommandNamedArgument.fromProps({
1279 name: 'at',
1280 description: t`position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values (including -0) are accepted and will work similarly to how 'depth' usually works. For example, -1 will insert the message right before the last message in chat.`,
1281 typeList: [ARGUMENT_TYPE.NUMBER],
1282 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
1283 }),
1284 SlashCommandNamedArgument.fromProps({
1285 name: 'return',
1286 description: t`The way how you want the return value to be provided`,
1287 typeList: [ARGUMENT_TYPE.STRING],
1288 defaultValue: 'none',
1289 enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
1290 forceEnum: true,
1291 }),
1292 SlashCommandNamedArgument.fromProps({
1293 name: 'raw',
1294 description: t`If true, does not alter quoted literal unnamed arguments`,
1295 typeList: [ARGUMENT_TYPE.BOOLEAN],
1296 defaultValue: 'true',
1297 enumProvider: commonEnumProviders.boolean('trueFalse'),
1298 isRequired: false,
1299 }),
1300 ],
1301 unnamedArgumentList: [
1302 new SlashCommandArgument(
1303 'text', [ARGUMENT_TYPE.STRING], true,
1304 ),
1305 ],
1306 helpString: `
1307 <div>
1308 ${t`Sends a message as a specific character. Uses the character avatar if it exists in the characters list.`}
1309 </div>
1310 <div>
1311 <strong>${t`Example:`}</strong>
1312 <ul>
1313 <li>
1314 <pre><code>/sendas name="Chloe" Hello, guys!</code></pre>
1315 ${t`will send "Hello, guys!" from "Chloe".`}
1316 </li>
1317 <li>
1318 <pre><code>/sendas name="Chloe" avatar="BigBadBoss" Hehehe, I am the big bad evil, fear me.</code></pre>
1319 ${t`will send a message as the character "Chloe", but utilizing the avatar from a character named "BigBadBoss".`}
1320 </li>
1321 </ul>
1322 </div>
1323 <div>
1324 ${t`If "compact" is set to true, the message is sent using a compact layout.`}
1325 </div>
1326 `,
1327 }));
1328 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1329 name: 'sys',
1330 rawQuotes: true,
1331 callback: sendNarratorMessage,
1332 aliases: ['nar'],
1333 returns: t`Optionally the text of the sent message, if specified in the "return" argument`,
1334 namedArgumentList: [
1335 new SlashCommandNamedArgument(
1336 'compact',
1337 t`compact layout`,
1338 [ARGUMENT_TYPE.BOOLEAN],
1339 false,
1340 false,
1341 'false',
1342 ),
1343 SlashCommandNamedArgument.fromProps({
1344 name: 'at',
1345 description: t`position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values (including -0) are accepted and will work similarly to how 'depth' usually works. For example, -1 will insert the message right before the last message in chat.`,
1346 typeList: [ARGUMENT_TYPE.NUMBER],
1347 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
1348 }),
1349 SlashCommandNamedArgument.fromProps({
1350 name: 'name',
1351 description: t`Optional custom display name to use for this system narrator message.`,
1352 typeList: [ARGUMENT_TYPE.STRING],
1353 }),
1354 SlashCommandNamedArgument.fromProps({
1355 name: 'return',
1356 description: t`The way how you want the return value to be provided`,
1357 typeList: [ARGUMENT_TYPE.STRING],
1358 defaultValue: 'none',
1359 enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
1360 forceEnum: true,
1361 }),
1362 SlashCommandNamedArgument.fromProps({
1363 name: 'raw',
1364 description: t`If true, does not alter quoted literal unnamed arguments`,
1365 typeList: [ARGUMENT_TYPE.BOOLEAN],
1366 defaultValue: 'true',
1367 enumProvider: commonEnumProviders.boolean('trueFalse'),
1368 isRequired: false,
1369 }),
1370 ],
1371 unnamedArgumentList: [
1372 new SlashCommandArgument(
1373 'text', [ARGUMENT_TYPE.STRING], true,
1374 ),
1375 ],
1376 helpString: `
1377 <div>
1378 ${t`Sends a message as a system narrator.`}
1379 </div>
1380 <div>
1381 ${t`If <code>compact</code> is set to <code>true</code>, the message is sent using a compact layout.`}
1382 </div>
1383 <div>
1384 <strong>${t`Example:`}</strong>
1385 <ul>
1386 <li>
1387 <pre><code>/sys The sun sets in the west.</code></pre>
1388 </li>
1389 <li>
1390 <pre><code>/sys compact=true A brief note.</code></pre>
1391 </li>
1392 </ul>
1393 </div>
1394 `,
1395 }));
1396 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1397 name: 'sysname',
1398 callback: setNarratorName,
1399 unnamedArgumentList: [
1400 new SlashCommandArgument(
1401 t`name`, [ARGUMENT_TYPE.STRING], false,
1402 ),
1403 ],
1404 helpString: t`Sets a name for future system narrator messages in this chat (display only). Default: System. Leave empty to reset.`,
1405 }));
1406 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1407 name: 'comment',
1408 rawQuotes: true,
1409 callback: sendCommentMessage,
1410 returns: t`Optionally the text of the sent message, if specified in the "return" argument`,
1411 namedArgumentList: [
1412 new SlashCommandNamedArgument(
1413 'compact',
1414 t`Whether to use a compact layout`,
1415 [ARGUMENT_TYPE.BOOLEAN],
1416 false,
1417 false,
1418 'false',
1419 ),
1420 SlashCommandNamedArgument.fromProps({
1421 name: 'at',
1422 description: t`position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values (including -0) are accepted and will work similarly to how 'depth' usually works. For example, -1 will insert the message right before the last message in chat.`,
1423 typeList: [ARGUMENT_TYPE.NUMBER],
1424 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
1425 }),
1426 SlashCommandNamedArgument.fromProps({
1427 name: 'return',
1428 description: t`The way how you want the return value to be provided`,
1429 typeList: [ARGUMENT_TYPE.STRING],
1430 defaultValue: 'none',
1431 enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
1432 forceEnum: true,
1433 }),
1434 SlashCommandNamedArgument.fromProps({
1435 name: 'raw',
1436 description: t`If true, does not alter quoted literal unnamed arguments`,
1437 typeList: [ARGUMENT_TYPE.BOOLEAN],
1438 defaultValue: 'true',
1439 enumProvider: commonEnumProviders.boolean('trueFalse'),
1440 isRequired: false,
1441 }),
1442 ],
1443 unnamedArgumentList: [
1444 new SlashCommandArgument(
1445 'text',
1446 [ARGUMENT_TYPE.STRING],
1447 true,
1448 ),
1449 ],
1450 helpString: `
1451 <div>
1452 ${t`Adds a note/comment message not part of the chat.`}
1453 </div>
1454 <div>
1455 ${t`If <code>compact</code> is set to <code>true</code>, the message is sent using a compact layout.`}
1456 </div>
1457 <div>
1458 <strong>${t`Example:`}</strong>
1459 <ul>
1460 <li>
1461 <pre><code>/comment This is a comment</code></pre>
1462 </li>
1463 <li>
1464 <pre><code>/comment compact=true This is a compact comment</code></pre>
1465 </li>
1466 </ul>
1467 </div>
1468 `,
1469 }));
1470 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1471 name: 'single',
1472 callback: setStoryModeCallback,
1473 aliases: ['story'],
1474 helpString: t`Sets the message style to single document mode without names or avatars visible.`,
1475 }));
1476 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1477 name: 'bubble',
1478 callback: setBubbleModeCallback,
1479 aliases: ['bubbles'],
1480 helpString: t`Sets the message style to bubble chat mode.`,
1481 }));
1482 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1483 name: 'flat',
1484 callback: setFlatModeCallback,
1485 aliases: ['default'],
1486 helpString: t`Sets the message style to flat chat mode.`,
1487 }));
1488 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1489 name: 'continue',
1490 callback: continueChatCallback,
1491 aliases: ['cont'],
1492 namedArgumentList: [
1493 new SlashCommandNamedArgument(
1494 'await',
1495 t`Whether to await for the continued generation before proceeding`,
1496 [ARGUMENT_TYPE.BOOLEAN],
1497 false,
1498 false,
1499 'false',
1500 ),
1501 ],
1502 unnamedArgumentList: [
1503 new SlashCommandArgument(
1504 'prompt', [ARGUMENT_TYPE.STRING], false,
1505 ),
1506 ],
1507 helpString: `
1508 <div>
1509 ${t`Continues the last message in the chat, with an optional additional prompt.`}
1510 </div>
1511 <div>
1512 ${t`If <code>await=true</code> named argument is passed, the command will await for the continued generation before proceeding.`}
1513 </div>
1514 <div>
1515 <strong>${t`Example:`}</strong>
1516 <ul>
1517 <li>
1518 <pre><code>/continue</code></pre>
1519 ${t`Continues the chat with no additional prompt and immediately proceeds to the next command.`}
1520 </li>
1521 <li>
1522 <pre><code>/continue await=true Let's explore this further...</code></pre>
1523 ${t`Continues the chat with the provided prompt and waits for the generation to finish.`}
1524 </li>
1525 </ul>
1526 </div>
1527 `,
1528 }));
1529 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1530 name: 'regenerate',
1531 callback: regenerateChatCallback,
1532 aliases: ['regen'],
1533 namedArgumentList: [
1534 new SlashCommandNamedArgument(
1535 'await',
1536 t`Whether to await for the regeneration before proceeding`,
1537 [ARGUMENT_TYPE.BOOLEAN],
1538 false,
1539 false,
1540 'false',
1541 ),
1542 ],
1543 helpString: `
1544 <div>
1545 ${t`Regenerates the latest reply in the chat.`}
1546 </div>
1547 <div>
1548 ${t`If <code>await=true</code> named argument is passed, the command will await for the regeneration before proceeding.`}
1549 </div>
1550 `,
1551 }));
1552 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1553 name: 'swipe',
1554 callback: swipeChatCallback,
1555 namedArgumentList: [
1556 new SlashCommandNamedArgument(
1557 'direction',
1558 t`Swipe direction`,
1559 [ARGUMENT_TYPE.STRING],
1560 false,
1561 false,
1562 SWIPE_DIRECTION.RIGHT,
1563 [
1564 new SlashCommandEnumValue(SWIPE_DIRECTION.RIGHT, t`Swipe to the next reply`, enumTypes.enum, enumIcons.default),
1565 new SlashCommandEnumValue(SWIPE_DIRECTION.LEFT, t`Swipe to the previous reply`, enumTypes.enum, enumIcons.default),
1566 ],
1567 [],
1568 null,
1569 true,
1570 ),
1571 new SlashCommandNamedArgument(
1572 'await',
1573 t`Whether to await for the swipe action before proceeding`,
1574 [ARGUMENT_TYPE.BOOLEAN],
1575 false,
1576 false,
1577 'false',
1578 ),
1579 ],
1580 helpString: `
1581 <div>
1582 ${t`Swipes the latest reply. Defaults to <code>direction=right</code>; use <code>direction=left</code> to go to the previous reply. If no next swipe exists, behavior depends on message context.`}
1583 </div>
1584 <div>
1585 ${t`If <code>await=true</code> named argument is passed, the command will await for the swipe action before proceeding.`}
1586 </div>
1587 `,
1588 }));
1589 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1590 name: 'go',
1591 callback: goToCharacterCallback,
1592 returns: t`The character/group name`,
1593 unnamedArgumentList: [
1594 SlashCommandArgument.fromProps({
1595 description: t`Character name - or unique character identifier (avatar key)`,
1596 typeList: [ARGUMENT_TYPE.STRING],
1597 isRequired: true,
1598 enumProvider: commonEnumProviders.characters('all'),
1599 }),
1600 ],
1601 helpString: t`Opens up a chat with the character or group by its name`,
1602 aliases: ['char'],
1603 }));
1604 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1605 name: 'rename-char',
1606 /** @param {{silent: string, chats: string}} options @param {string} name */
1607 callback: async ({ silent = 'true', chats = null }, name) => {
1608 const renamed = await renameCharacter(name, { silent: isTrueBoolean(silent), renameChats: chats !== null ? isTrueBoolean(chats) : null });
1609 return String(renamed);
1610 },
1611 returns: t`true/false - Whether the rename was successful`,
1612 namedArgumentList: [
1613 new SlashCommandNamedArgument(
1614 'silent', t`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',
1615 ),
1616 new SlashCommandNamedArgument(
1617 'chats', t`Rename char in all previous chats`, [ARGUMENT_TYPE.BOOLEAN], false, false, '<null>',
1618 ),
1619 ],
1620 unnamedArgumentList: [
1621 new SlashCommandArgument(
1622 t`new char name`, [ARGUMENT_TYPE.STRING], true,
1623 ),
1624 ],
1625 helpString: t`Renames the current character.`,
1626 }));
1627 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1628 name: 'sysgen',
1629 callback: generateSystemMessage,
1630 namedArgumentList: [
1631 SlashCommandNamedArgument.fromProps({
1632 name: 'trim',
1633 description: t`Trim the output by the last sentence boundary`,
1634 typeList: [ARGUMENT_TYPE.BOOLEAN],
1635 defaultValue: 'false',
1636 isRequired: false,
1637 enumProvider: commonEnumProviders.boolean('trueFalse'),
1638 }),
1639 SlashCommandNamedArgument.fromProps({
1640 name: 'compact',
1641 description: t`Use a compact layout for the message`,
1642 typeList: [ARGUMENT_TYPE.BOOLEAN],
1643 defaultValue: 'false',
1644 isRequired: false,
1645 acceptsMultiple: false,
1646 enumProvider: commonEnumProviders.boolean('trueFalse'),
1647 }),
1648 SlashCommandNamedArgument.fromProps({
1649 name: 'at',
1650 description: t`Position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values (including -0) are accepted and will work similarly to how 'depth' usually works. For example, -1 will insert the message right before the last message in chat.`,
1651 typeList: [ARGUMENT_TYPE.NUMBER],
1652 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
1653 }),
1654 SlashCommandNamedArgument.fromProps({
1655 name: 'name',
1656 description: t`Optional custom display name to use for this system narrator message.`,
1657 typeList: [ARGUMENT_TYPE.STRING],
1658 }),
1659 SlashCommandNamedArgument.fromProps({
1660 name: 'return',
1661 description: t`The way how you want the return value to be provided`,
1662 typeList: [ARGUMENT_TYPE.STRING],
1663 defaultValue: 'none',
1664 enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
1665 forceEnum: true,
1666 }),
1667 ],
1668 unnamedArgumentList: [
1669 new SlashCommandArgument(
1670 'prompt', [ARGUMENT_TYPE.STRING], true,
1671 ),
1672 ],
1673 helpString: t`Generates a system message using a specified prompt.`,
1674 }));
1675 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1676 name: 'ask',
1677 callback: askCharacter,
1678 returns: t`Optionally the text of the sent message, if specified in the "return" argument`,
1679 namedArgumentList: [
1680 SlashCommandNamedArgument.fromProps({
1681 name: 'name',
1682 description: t`Character name - or unique character identifier (avatar key)`,
1683 typeList: [ARGUMENT_TYPE.STRING],
1684 isRequired: true,
1685 enumProvider: commonEnumProviders.characters('character'),
1686 }),
1687 SlashCommandNamedArgument.fromProps({
1688 name: 'return',
1689 description: t`The way how you want the return value to be provided`,
1690 typeList: [ARGUMENT_TYPE.STRING],
1691 defaultValue: 'pipe',
1692 enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
1693 forceEnum: true,
1694 }),
1695 ],
1696 unnamedArgumentList: [
1697 new SlashCommandArgument(
1698 'prompt', [ARGUMENT_TYPE.STRING], false, false,
1699 ),
1700 ],
1701 helpString: t`Asks a specified character card a prompt. Character name must be provided in a named argument.`,
1702 }));
1703 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1704 name: 'delname',
1705 callback: deleteMessagesByNameCallback,
1706 namedArgumentList: [],
1707 unnamedArgumentList: [
1708 SlashCommandArgument.fromProps({
1709 description: t`Character name - or unique character identifier (avatar key)`,
1710 typeList: [ARGUMENT_TYPE.STRING],
1711 isRequired: true,
1712 enumProvider: commonEnumProviders.characters('character'),
1713 }),
1714 ],
1715 aliases: ['cancel'],
1716 helpString: `
1717 <div>
1718 ${t`Deletes all messages attributed to a specified name.`}
1719 </div>
1720 <div>
1721 <strong>${t`Example:`}</strong>
1722 <ul>
1723 <li>
1724 <pre><code>/delname John</code></pre>
1725 </li>
1726 </ul>
1727 </div>
1728 `,
1729 }));
1730 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1731 name: 'send',
1732 rawQuotes: true,
1733 callback: sendUserMessageCallback,
1734 returns: t`Optionally the text of the sent message, if specified in the "return" argument`,
1735 namedArgumentList: [
1736 new SlashCommandNamedArgument(
1737 'compact',
1738 t`whether to use a compact layout`,
1739 [ARGUMENT_TYPE.BOOLEAN],
1740 false,
1741 false,
1742 'false',
1743 ),
1744 SlashCommandNamedArgument.fromProps({
1745 name: 'at',
1746 description: t`position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values (including -0) are accepted and will work similarly to how 'depth' usually works. For example, -1 will insert the message right before the last message in chat.`,
1747 typeList: [ARGUMENT_TYPE.NUMBER],
1748 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
1749 }),
1750 SlashCommandNamedArgument.fromProps({
1751 name: 'name',
1752 description: t`display name`,
1753 typeList: [ARGUMENT_TYPE.STRING],
1754 defaultValue: '{{user}}',
1755 enumProvider: commonEnumProviders.personas({ allowPersonaKey: true }),
1756 }),
1757 SlashCommandNamedArgument.fromProps({
1758 name: 'return',
1759 description: t`The way how you want the return value to be provided`,
1760 typeList: [ARGUMENT_TYPE.STRING],
1761 defaultValue: 'none',
1762 enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
1763 forceEnum: true,
1764 }),
1765 SlashCommandNamedArgument.fromProps({
1766 name: 'raw',
1767 description: t`If true, does not alter quoted literal unnamed arguments`,
1768 typeList: [ARGUMENT_TYPE.BOOLEAN],
1769 defaultValue: 'true',
1770 enumProvider: commonEnumProviders.boolean('trueFalse'),
1771 isRequired: false,
1772 }),
1773 ],
1774 unnamedArgumentList: [
1775 new SlashCommandArgument(
1776 'text',
1777 [ARGUMENT_TYPE.STRING],
1778 true,
1779 ),
1780 ],
1781 helpString: `
1782 <div>
1783 ${t`Adds a user message to the chat log without triggering a generation.`}
1784 </div>
1785 <div>
1786 ${t`If <code>compact</code> is set to <code>true</code>, the message is sent using a compact layout.`}
1787 </div>
1788 <div>
1789 ${t`If <code>name</code> is set, it will be displayed as the message sender. Can be an empty for no name.`}
1790 </div>
1791 <div>
1792 <strong>${t`Example:`}</strong>
1793 <ul>
1794 <li>
1795 <pre><code>/send Hello there!</code></pre>
1796 </li>
1797 <li>
1798 <pre><code>/send compact=true Hi</code></pre>
1799 </li>
1800 </ul>
1801 </div>
1802 `,
1803 }));
1804 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1805 name: 'trigger',
1806 callback: triggerGenerationCallback,
1807 namedArgumentList: [
1808 new SlashCommandNamedArgument(
1809 'await',
1810 t`Whether to await for the triggered generation before continuing`,
1811 [ARGUMENT_TYPE.BOOLEAN],
1812 false,
1813 false,
1814 'false',
1815 ),
1816 ],
1817 unnamedArgumentList: [
1818 SlashCommandArgument.fromProps({
1819 description: t`group member index (starts with 0) or name`,
1820 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING],
1821 isRequired: false,
1822 enumProvider: commonEnumProviders.groupMembers(),
1823 }),
1824 ],
1825 helpString: `
1826 <div>
1827 ${t`Triggers a message generation. If in group, can trigger a message for the specified group member index or name.`}
1828 </div>
1829 <div>
1830 ${t`If <code>await=true</code> named argument is passed, the command will await for the triggered generation before continuing.`}
1831 </div>
1832 `,
1833 }));
1834 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1835 name: 'hide',
1836 callback: hideMessageCallback,
1837 namedArgumentList: [
1838 SlashCommandNamedArgument.fromProps({
1839 name: 'name',
1840 description: t`only hide messages from a certain character or persona`,
1841 typeList: [ARGUMENT_TYPE.STRING],
1842 enumProvider: commonEnumProviders.messageNames,
1843 isRequired: false,
1844 acceptsMultiple: false,
1845 }),
1846 ],
1847 unnamedArgumentList: [
1848 SlashCommandArgument.fromProps({
1849 description: t`message index (starts with 0) or range, defaults to the last message index if not provided`,
1850 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE],
1851 isRequired: false,
1852 enumProvider: commonEnumProviders.messages(),
1853 }),
1854 ],
1855 helpString: t`Hides a chat message from the prompt.`,
1856 }));
1857 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1858 name: 'unhide',
1859 callback: unhideMessageCallback,
1860 namedArgumentList: [
1861 SlashCommandNamedArgument.fromProps({
1862 name: 'name',
1863 description: t`only unhide messages from a certain character or persona`,
1864 typeList: [ARGUMENT_TYPE.STRING],
1865 enumProvider: commonEnumProviders.messageNames,
1866 isRequired: false,
1867 acceptsMultiple: false,
1868 }),
1869 ],
1870 unnamedArgumentList: [
1871 SlashCommandArgument.fromProps({
1872 description: t`message index (starts with 0) or range, defaults to the last message index if not provided`,
1873 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE],
1874 isRequired: false,
1875 enumProvider: commonEnumProviders.messages(),
1876 }),
1877 ],
1878 helpString: t`Unhides a message from the prompt.`,
1879 }));
1880 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1881 name: 'member-get',
1882 aliases: ['getmember', 'memberget'],
1883 callback: (async ({ field = 'name' }, arg) => {
1884 if (!selected_group) {
1885 toastr.warning(t`Cannot run /member-get command outside of a group chat.`);
1886 return '';
1887 }
1888 if (field === '') {
1889 toastr.warning(t`'/member-get field=' argument required!`);
1890 return '';
1891 }
1892 field = field.toString();
1893 arg = arg.toString();
1894 if (!['name', 'index', 'id', 'avatar'].includes(field)) {
1895 toastr.warning(t`'/member-get field=' argument required!`);
1896 return '';
1897 }
1898 const isId = !isNaN(parseInt(arg));
1899 const groupMember = findGroupMemberId(arg, true);
1900 if (!groupMember) {
1901 toastr.warning(t`No group member found using ${isId ? 'id' : 'string'} ${arg}`);
1902 return '';
1903 }
1904 return groupMember[field];
1905 }),
1906 namedArgumentList: [
1907 SlashCommandNamedArgument.fromProps({
1908 name: 'field',
1909 description: t`Whether to retrieve the name, index, id, or avatar.`,
1910 typeList: [ARGUMENT_TYPE.STRING],
1911 isRequired: true,
1912 defaultValue: 'name',
1913 enumList: [
1914 new SlashCommandEnumValue('name', t`Character name`),
1915 new SlashCommandEnumValue('index', t`Group member index`),
1916 new SlashCommandEnumValue('avatar', t`Character avatar`),
1917 new SlashCommandEnumValue('id', t`Character index`),
1918 ],
1919 }),
1920 ],
1921 unnamedArgumentList: [
1922 SlashCommandArgument.fromProps({
1923 description: t`member index (starts with 0), name, or avatar`,
1924 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING],
1925 isRequired: true,
1926 enumProvider: commonEnumProviders.groupMembers(),
1927 }),
1928 ],
1929 helpString: t`Retrieves a group member's name, index, id, or avatar.`,
1930 }));
1931 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1932 name: 'member-disable',
1933 callback: disableGroupMemberCallback,
1934 aliases: ['disable', 'disablemember', 'memberdisable'],
1935 unnamedArgumentList: [
1936 SlashCommandArgument.fromProps({
1937 description: t`member index (starts with 0) or name`,
1938 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING],
1939 isRequired: true,
1940 enumProvider: commonEnumProviders.groupMembers(),
1941 }),
1942 ],
1943 helpString: t`Disables a group member from being drafted for replies.`,
1944 }));
1945 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1946 name: 'member-enable',
1947 aliases: ['enable', 'enablemember', 'memberenable'],
1948 callback: enableGroupMemberCallback,
1949 unnamedArgumentList: [
1950 SlashCommandArgument.fromProps({
1951 description: t`member index (starts with 0) or name`,
1952 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING],
1953 isRequired: true,
1954 enumProvider: commonEnumProviders.groupMembers(),
1955 }),
1956 ],
1957 helpString: t`Enables a group member to be drafted for replies.`,
1958 }));
1959 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1960 name: 'member-add',
1961 callback: addGroupMemberCallback,
1962 aliases: ['addmember', 'memberadd'],
1963 unnamedArgumentList: [
1964 SlashCommandArgument.fromProps({
1965 description: t`Character name - or unique character identifier (avatar key)`,
1966 typeList: [ARGUMENT_TYPE.STRING],
1967 isRequired: true,
1968 enumProvider: () => selected_group ? commonEnumProviders.characters('character')() : [],
1969 }),
1970 ],
1971 helpString: `
1972 <div>
1973 ${t`Adds a new group member to the group chat.`}
1974 </div>
1975 <div>
1976 <strong>${t`Example:`}</strong>
1977 <ul>
1978 <li>
1979 <pre><code>/member-add John Doe</code></pre>
1980 </li>
1981 </ul>
1982 </div>
1983 `,
1984 }));
1985 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1986 name: 'member-remove',
1987 callback: removeGroupMemberCallback,
1988 aliases: ['removemember', 'memberremove'],
1989 unnamedArgumentList: [
1990 SlashCommandArgument.fromProps({
1991 description: t`member index (starts with 0) or name`,
1992 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING],
1993 isRequired: true,
1994 enumProvider: commonEnumProviders.groupMembers(),
1995 }),
1996 ],
1997 helpString: `
1998 <div>
1999 ${t`Removes a group member from the group chat.`}
2000 </div>
2001 <div>
2002 <strong>${t`Example:`}</strong>
2003 <ul>
2004 <li>
2005 <pre><code>/member-remove 2</code></pre>
2006 <pre><code>/member-remove John Doe</code></pre>
2007 </li>
2008 </ul>
2009 </div>
2010 `,
2011 }));
2012 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2013 name: 'member-up',
2014 callback: moveGroupMemberUpCallback,
2015 aliases: ['upmember', 'memberup'],
2016 unnamedArgumentList: [
2017 SlashCommandArgument.fromProps({
2018 description: t`member index (starts with 0) or name`,
2019 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING],
2020 isRequired: true,
2021 enumProvider: commonEnumProviders.groupMembers(),
2022 }),
2023 ],
2024 helpString: t`Moves a group member up in the group chat list.`,
2025 }));
2026 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2027 name: 'member-down',
2028 callback: moveGroupMemberDownCallback,
2029 aliases: ['downmember', 'memberdown'],
2030 unnamedArgumentList: [
2031 SlashCommandArgument.fromProps({
2032 description: t`member index (starts with 0) or name`,
2033 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING],
2034 isRequired: true,
2035 enumProvider: commonEnumProviders.groupMembers(),
2036 }),
2037 ],
2038 helpString: t`Moves a group member down in the group chat list.`,
2039 }));
2040 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2041 name: 'member-peek',
2042 aliases: ['peek', 'memberpeek', 'peekmember'],
2043 callback: peekCallback,
2044 unnamedArgumentList: [
2045 SlashCommandArgument.fromProps({
2046 description: t`member index (starts with 0) or name`,
2047 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING],
2048 isRequired: true,
2049 enumProvider: commonEnumProviders.groupMembers(),
2050 }),
2051 ],
2052 helpString: `
2053 <div>
2054 ${t`Shows a group member character card without switching chats.`}
2055 </div>
2056 <div>
2057 <strong>${t`Examples:`}</strong>
2058 <ul>
2059 <li>
2060 <pre><code>/peek Gloria</code></pre>
2061 ${t`Shows the character card for the character named "Gloria".`}
2062 </li>
2063 </ul>
2064 </div>
2065 `,
2066 }));
2067 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2068 name: 'member-count',
2069 callback: countGroupMemberCallback,
2070 aliases: ['countmember', 'membercount'],
2071 helpString: t`Returns the total number of group members in the group chat list.`,
2072 }));
2073 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2074 name: 'delswipe',
2075 callback: deleteSwipeCallback,
2076 returns: t`the new, currently selected swipe id`,
2077 aliases: ['swipedel'],
2078 unnamedArgumentList: [
2079 SlashCommandArgument.fromProps({
2080 description: t`1-based swipe id`,
2081 typeList: [ARGUMENT_TYPE.NUMBER],
2082 isRequired: true,
2083 enumProvider: () => Array.isArray(chat[chat.length - 1]?.swipes) ?
2084 chat[chat.length - 1].swipes.map((/** @type {string} */ swipe, /** @type {number} */ i) => new SlashCommandEnumValue(String(i + 1), swipe, enumTypes.enum, enumIcons.message))
2085 : [],
2086 }),
2087 ],
2088 helpString: `
2089 <div>
2090 ${t`Deletes a swipe from the last chat message. If swipe id is not provided, it deletes the current swipe.`}
2091 </div>
2092 <div>
2093 <strong>${t`Example:`}</strong>
2094 <ul>
2095 <li>
2096 <pre><code>/delswipe</code></pre>
2097 ${t`Deletes the current swipe.`}
2098 </li>
2099 <li>
2100 <pre><code>/delswipe 2</code></pre>
2101 ${t`Deletes the second swipe from the last chat message.`}
2102 </li>
2103 </ul>
2104 </div>
2105 `,
2106 }));
2107 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2108 name: 'echo',
2109 rawQuotes: true,
2110 callback: echoCallback,
2111 returns: t`the text`,
2112 namedArgumentList: [
2113 new SlashCommandNamedArgument(
2114 'title', t`title of the toast message`, [ARGUMENT_TYPE.STRING], false,
2115 ),
2116 SlashCommandNamedArgument.fromProps({
2117 name: 'severity',
2118 description: t`severity level of the toast message`,
2119 typeList: [ARGUMENT_TYPE.STRING],
2120 defaultValue: 'info',
2121 enumProvider: () => [
2122 new SlashCommandEnumValue('info', 'info', enumTypes.macro, 'ℹ️'),
2123 new SlashCommandEnumValue('warning', 'warning', enumTypes.enum, '⚠️'),
2124 new SlashCommandEnumValue('error', 'error', enumTypes.enum, '❗'),
2125 new SlashCommandEnumValue('success', 'success', enumTypes.enum, '✅'),
2126 ],
2127 }),
2128 SlashCommandNamedArgument.fromProps({
2129 name: 'timeout',
2130 description: t`time in milliseconds to display the toast message. Set this and 'extendedTimeout' to 0 to show indefinitely until dismissed.`,
2131 typeList: [ARGUMENT_TYPE.NUMBER],
2132 defaultValue: `${toastr.options.timeOut}`,
2133 }),
2134 SlashCommandNamedArgument.fromProps({
2135 name: 'extendedTimeout',
2136 description: t`time in milliseconds to display the toast message. Set this and 'timeout' to 0 to show indefinitely until dismissed.`,
2137 typeList: [ARGUMENT_TYPE.NUMBER],
2138 defaultValue: `${toastr.options.extendedTimeOut}`,
2139 }),
2140 SlashCommandNamedArgument.fromProps({
2141 name: 'preventDuplicates',
2142 description: t`prevent duplicate toasts with the same message from being displayed.`,
2143 typeList: [ARGUMENT_TYPE.BOOLEAN],
2144 defaultValue: 'false',
2145 enumList: commonEnumProviders.boolean('trueFalse')(),
2146 }),
2147 SlashCommandNamedArgument.fromProps({
2148 name: 'awaitDismissal',
2149 description: t`wait for the toast to be dismissed before continuing.`,
2150 typeList: [ARGUMENT_TYPE.BOOLEAN],
2151 defaultValue: 'false',
2152 enumList: commonEnumProviders.boolean('trueFalse')(),
2153 }),
2154 SlashCommandNamedArgument.fromProps({
2155 name: 'cssClass',
2156 description: t`additional CSS class to add to the toast message (e.g. for custom styling)`,
2157 typeList: [ARGUMENT_TYPE.STRING],
2158 }),
2159 SlashCommandNamedArgument.fromProps({
2160 name: 'color',
2161 description: t`custom CSS color of the toast message. Accepts all valid CSS color values (e.g. 'red', '#FF0000', 'rgb(255, 0, 0)').<br />>Can be more customizable with the 'cssClass' argument and custom classes.`,
2162 }),
2163 SlashCommandNamedArgument.fromProps({
2164 name: 'escapeHtml',
2165 description: t`whether to escape HTML in the toast message.`,
2166 typeList: [ARGUMENT_TYPE.BOOLEAN],
2167 defaultValue: 'true',
2168 enumList: commonEnumProviders.boolean('trueFalse')(),
2169 }),
2170 SlashCommandNamedArgument.fromProps({
2171 name: 'onClick',
2172 description: t`a closure to call when the toast is clicked. This executed closure receives scope as provided in the script. Careful about possible side effects when manipulating variables and more.`,
2173 typeList: [ARGUMENT_TYPE.CLOSURE],
2174 }),
2175 SlashCommandNamedArgument.fromProps({
2176 name: 'raw',
2177 description: t`If true, does not alter quoted literal unnamed arguments`,
2178 typeList: [ARGUMENT_TYPE.BOOLEAN],
2179 defaultValue: 'true',
2180 enumProvider: commonEnumProviders.boolean('trueFalse'),
2181 isRequired: false,
2182 }),
2183 ],
2184 unnamedArgumentList: [
2185 new SlashCommandArgument(
2186 'text', [ARGUMENT_TYPE.STRING], true,
2187 ),
2188 ],
2189 helpString: `
2190 <div>
2191 Echoes the provided text to a toast message. Can be used to display informational messages or for pipes debugging.
2192 </div>
2193 <div>
2194 <strong>Example:</strong>
2195 <ul>
2196 <li>
2197 <pre><code>/echo title="My Message" severity=warning This is a warning message</code></pre>
2198 </li>
2199 <li>
2200 <pre><code>/echo color=purple This message is purple</code></pre>
2201 </li>
2202 <li>
2203 <pre><code>/echo onClick={: /echo escapeHtml=false color=transparent cssClass=wider_dialogue_popup &lt;img src="/img/five.png" /&gt; :} timeout=5000 Clicking on this message within 5 seconds will open the image.</code></pre>
2204 </li>
2205 </ul>
2206 </div>
2207 `,
2208 }));
2209 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2210 name: 'gen',
2211 callback: generateCallback,
2212 returns: t`generated text`,
2213 namedArgumentList: [
2214 SlashCommandNamedArgument.fromProps({
2215 name: 'trim',
2216 description: t`Trim the output by the last sentence boundary`,
2217 typeList: [ARGUMENT_TYPE.BOOLEAN],
2218 defaultValue: 'false',
2219 isRequired: false,
2220 enumProvider: commonEnumProviders.boolean('trueFalse'),
2221 }),
2222 new SlashCommandNamedArgument(
2223 'lock', t`lock user input during generation`, [ARGUMENT_TYPE.BOOLEAN], false, false, null, commonEnumProviders.boolean('onOff')(),
2224 ),
2225 SlashCommandNamedArgument.fromProps({
2226 name: 'name',
2227 description: t`in-prompt character name for instruct mode (or unique character identifier (avatar key), which will be used as name)`,
2228 typeList: [ARGUMENT_TYPE.STRING],
2229 defaultValue: 'System',
2230 enumProvider: () => [...commonEnumProviders.characters('character')(), new SlashCommandEnumValue('System', null, enumTypes.enum, enumIcons.assistant)],
2231 }),
2232 new SlashCommandNamedArgument(
2233 'length', t`API response length in tokens`, [ARGUMENT_TYPE.NUMBER], false,
2234 ),
2235 SlashCommandNamedArgument.fromProps({
2236 name: 'as',
2237 description: t`role of the output prompt`,
2238 typeList: [ARGUMENT_TYPE.STRING],
2239 enumList: [
2240 new SlashCommandEnumValue('system', null, enumTypes.enum, enumIcons.assistant),
2241 new SlashCommandEnumValue('char', null, enumTypes.enum, enumIcons.character),
2242 ],
2243 }),
2244 ],
2245 unnamedArgumentList: [
2246 new SlashCommandArgument(
2247 'prompt', [ARGUMENT_TYPE.STRING], true,
2248 ),
2249 ],
2250 helpString: `
2251 <div>
2252 ${t`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").`}
2253 </div>
2254 <div>
2255 ${t`"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.`}
2256 </div>
2257 `,
2258 }));
2259 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2260 name: 'genraw',
2261 callback: generateRawCallback,
2262 returns: t`generated text`,
2263 namedArgumentList: [
2264 new SlashCommandNamedArgument(
2265 'lock', t`lock user input during generation`, [ARGUMENT_TYPE.BOOLEAN], false, false, 'off', commonEnumProviders.boolean('onOff')(),
2266 ),
2267 new SlashCommandNamedArgument(
2268 'instruct', t`use instruct mode`, [ARGUMENT_TYPE.BOOLEAN], false, false, 'on', commonEnumProviders.boolean('onOff')(),
2269 ),
2270 new SlashCommandNamedArgument(
2271 'stop', t`one-time custom stop strings`, [ARGUMENT_TYPE.LIST], false, false, '[]',
2272 ),
2273 SlashCommandNamedArgument.fromProps({
2274 name: 'as',
2275 description: t`role of the output prompt`,
2276 defaultValue: 'system',
2277 typeList: [ARGUMENT_TYPE.STRING],
2278 enumList: [
2279 new SlashCommandEnumValue('system', null, enumTypes.enum, enumIcons.assistant),
2280 new SlashCommandEnumValue('char', null, enumTypes.enum, enumIcons.character),
2281 ],
2282 }),
2283 new SlashCommandNamedArgument(
2284 'system', t`system prompt at the start`, [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.VARIABLE_NAME], false,
2285 ),
2286 new SlashCommandNamedArgument(
2287 'prefill', t`prefill prompt at the end`, [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.VARIABLE_NAME], false,
2288 ),
2289 new SlashCommandNamedArgument(
2290 'length', t`API response length in tokens`, [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], false,
2291 ),
2292 new SlashCommandNamedArgument(
2293 'trim', t`trim {{user}} and {{char}} prefixes from the output`, [ARGUMENT_TYPE.BOOLEAN], false, false, 'on', commonEnumProviders.boolean('onOff')(),
2294 ),
2295 ],
2296 unnamedArgumentList: [
2297 new SlashCommandArgument(
2298 'prompt', [ARGUMENT_TYPE.STRING], true,
2299 ),
2300 ],
2301 helpString: `
2302 <div>
2303 ${t`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.`}
2304 </div>
2305 <div>
2306 ${t`Use instruct=off to skip instruct formatting, e.g. <pre><code>/genraw instruct=off Why is the sky blue?</code></pre>`}
2307 </div>
2308 <div>
2309 ${t`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>`}
2310 </div>
2311 <div>
2312 ${t`"as" argument controls the role of the output prompt: system (default) or char. "system" argument adds an (optional) system prompt at the start.`}
2313 </div>
2314 <div>
2315 ${t`If "length" argument is provided as a number in tokens, allows to temporarily override an API response length.`}
2316 </div>
2317 `,
2318 }));
2319 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2320 name: 'addswipe',
2321 callback: addSwipeCallback,
2322 returns: t`the new swipe id`,
2323 aliases: ['swipeadd'],
2324 namedArgumentList: [
2325 SlashCommandNamedArgument.fromProps({
2326 name: 'switch',
2327 description: t`switch to the new swipe`,
2328 typeList: [ARGUMENT_TYPE.BOOLEAN],
2329 enumList: commonEnumProviders.boolean()(),
2330 }),
2331 ],
2332 unnamedArgumentList: [
2333 new SlashCommandArgument(
2334 'text', [ARGUMENT_TYPE.STRING], true,
2335 ),
2336 ],
2337 helpString: `
2338 <div>
2339 ${t`Adds a swipe to the last chat message.`}
2340 </div>
2341 <div>
2342 ${t`Use switch=true to switch to directly switch to the new swipe.`}
2343 </div>`,
2344 }));
2345 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2346 name: 'stop',
2347 callback: () => {
2348 const stopped = stopGeneration();
2349 return String(stopped);
2350 },
2351 returns: t`true/false, whether the generation was running and got stopped`,
2352 helpString: `
2353 <div>
2354 ${t`Stops the generation and any streaming if it is currently running.`}
2355 </div>
2356 <div>
2357 ${t`Note: This command cannot be executed from the chat input, as sending any message or script from there is blocked during generation. But it can be executed via automations or QR scripts/buttons.`}
2358 </div>
2359 `,
2360 aliases: ['generate-stop'],
2361 }));
2362 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2363 name: 'abort',
2364 callback: abortCallback,
2365 namedArgumentList: [
2366 SlashCommandNamedArgument.fromProps({
2367 name: 'quiet',
2368 description: t`Whether to suppress the toast message notifying about the /abort call.`,
2369 typeList: [ARGUMENT_TYPE.BOOLEAN],
2370 defaultValue: 'true',
2371 }),
2372 ],
2373 unnamedArgumentList: [
2374 SlashCommandArgument.fromProps({
2375 description: t`The reason for aborting command execution. Shown when quiet=false`,
2376 typeList: [ARGUMENT_TYPE.STRING],
2377 }),
2378 ],
2379 helpString: t`Aborts the slash command batch execution.`,
2380 }));
2381 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2382 name: 'fuzzy',
2383 callback: fuzzyCallback,
2384 returns: t`matching item`,
2385 namedArgumentList: [
2386 SlashCommandNamedArgument.fromProps({
2387 name: 'list',
2388 description: t`list of items to match against`,
2389 acceptsMultiple: false,
2390 isRequired: true,
2391 typeList: [ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.VARIABLE_NAME],
2392 enumProvider: commonEnumProviders.variables('all'),
2393 }),
2394 SlashCommandNamedArgument.fromProps({
2395 name: 'threshold',
2396 description: t`fuzzy match threshold (0.0 to 1.0)`,
2397 typeList: [ARGUMENT_TYPE.NUMBER],
2398 isRequired: false,
2399 defaultValue: '0.4',
2400 acceptsMultiple: false,
2401 }),
2402 SlashCommandNamedArgument.fromProps({
2403 name: 'mode',
2404 description: t`fuzzy match mode`,
2405 typeList: [ARGUMENT_TYPE.STRING],
2406 isRequired: false,
2407 defaultValue: 'first',
2408 acceptsMultiple: false,
2409 enumList: [
2410 new SlashCommandEnumValue('first', t`first match below the threshold`, enumTypes.enum, enumIcons.default),
2411 new SlashCommandEnumValue('best', t`best match below the threshold`, enumTypes.enum, enumIcons.default),
2412 ],
2413 }),
2414 ],
2415 unnamedArgumentList: [
2416 new SlashCommandArgument(
2417 t`text to search`, [ARGUMENT_TYPE.STRING], true,
2418 ),
2419 ],
2420 helpString: `
2421 <div>
2422 ${t`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.`}
2423 </div>
2424 <div>
2425 ${t`The optional <code>threshold</code> (default is 0.4) allows control over the match strictness.`}
2426 ${t`A low value (min 0.0) means the match is very strict.`}
2427 ${t`At 1.0 (max) the match is very loose and will match anything.`}
2428 </div>
2429 <div>
2430 ${t`The optional <code>mode</code> argument allows to control the behavior when multiple items match the text.`}
2431 <ul>
2432 <li>${t`<code>first</code> (default) returns the first match below the threshold.`}</li>
2433 <li>${t`<code>best</code> returns the best match below the threshold.`}</li>
2434 </ul>
2435 </div>
2436 <div>
2437 ${t`The returned value passes to the next command through the pipe.`}
2438 </div>
2439 <div>
2440 <strong>${t`Example:`}</strong>
2441 <ul>
2442 <li>
2443 <pre><code>/fuzzy list=["a","b","c"] threshold=0.4 abc</code></pre>
2444 </li>
2445 </ul>
2446 </div>
2447 `,
2448 }));
2449 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2450 name: 'pass',
2451 callback: (_, arg) => {
2452 // We do not support arrays of closures. Arrays of strings will be send as JSON
2453 if (Array.isArray(arg) && arg.some(x => x instanceof SlashCommandClosure)) throw new Error(t`Command /pass does not support multiple closures`);
2454 if (Array.isArray(arg)) return JSON.stringify(arg);
2455 return arg;
2456 },
2457 returns: t`the provided value`,
2458 unnamedArgumentList: [
2459 new SlashCommandArgument(
2460 t`text`, [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.BOOLEAN, ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.DICTIONARY, ARGUMENT_TYPE.CLOSURE], true,
2461 ),
2462 ],
2463 aliases: ['return'],
2464 helpString: `
2465 <div>
2466 <pre><span class="monospace">/pass (text)</span> – ${t`passes the text to the next command through the pipe.`}</pre>
2467 </div>
2468 <div>
2469 <strong>${t`Example:`}</strong>
2470 <ul>
2471 <li><pre><code>/pass Hello world</code></pre></li>
2472 </ul>
2473 </div>
2474 `,
2475 }));
2476 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2477 name: 'delay',
2478 callback: delayCallback,
2479 aliases: ['wait', 'sleep'],
2480 unnamedArgumentList: [
2481 new SlashCommandArgument(
2482 t`milliseconds`, [ARGUMENT_TYPE.NUMBER], true,
2483 ),
2484 ],
2485 helpString: `
2486 <div>
2487 ${t`Delays the next command in the pipe by the specified number of milliseconds.`}
2488 </div>
2489 <div>
2490 <strong>${t`Example:`}</strong>
2491 <ul>
2492 <li>
2493 <pre><code>/delay 1000</code></pre>
2494 </li>
2495 </ul>
2496 </div>
2497 `,
2498 }));
2499 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2500 name: 'input',
2501 aliases: ['prompt'],
2502 callback: inputCallback,
2503 returns: t`user input`,
2504 namedArgumentList: [
2505 SlashCommandNamedArgument.fromProps({
2506 name: 'default',
2507 description: t`default value of the input field`,
2508 typeList: [ARGUMENT_TYPE.STRING],
2509 }),
2510 SlashCommandNamedArgument.fromProps({
2511 name: 'large',
2512 description: t`popup window will be shown larger in height, with more space for content (input field needs to be sized via 'rows' argument)`,
2513 typeList: [ARGUMENT_TYPE.BOOLEAN],
2514 defaultValue: 'off',
2515 enumList: commonEnumProviders.boolean('onOff')(),
2516 }),
2517 SlashCommandNamedArgument.fromProps({
2518 name: 'wide',
2519 description: t`popup window will be shown wider, with a wider input field`,
2520 typeList: [ARGUMENT_TYPE.BOOLEAN],
2521 defaultValue: 'off',
2522 enumList: commonEnumProviders.boolean('onOff')(),
2523 }),
2524 SlashCommandNamedArgument.fromProps({
2525 name: 'okButton',
2526 description: t`text for the ok button`,
2527 typeList: [ARGUMENT_TYPE.STRING],
2528 defaultValue: 'Ok',
2529 }),
2530 SlashCommandNamedArgument.fromProps({
2531 name: 'rows',
2532 description: t`number of rows for the input field (lines being displayed)`,
2533 typeList: [ARGUMENT_TYPE.NUMBER],
2534 }),
2535 SlashCommandNamedArgument.fromProps({
2536 name: 'placeholder',
2537 description: t`placeholder text displayed in the input field when empty`,
2538 typeList: [ARGUMENT_TYPE.STRING],
2539 }),
2540 SlashCommandNamedArgument.fromProps({
2541 name: 'tooltip',
2542 description: t`tooltip text shown when hovering over the input field`,
2543 typeList: [ARGUMENT_TYPE.STRING],
2544 }),
2545 SlashCommandNamedArgument.fromProps({
2546 name: 'onSuccess',
2547 description: t`closure to execute when the ok button is clicked or the input is closed as successful (via Enter, etc)`,
2548 typeList: [ARGUMENT_TYPE.CLOSURE],
2549 }),
2550 SlashCommandNamedArgument.fromProps({
2551 name: 'onCancel',
2552 description: t`closure to execute when the cancel button is clicked or the input is closed as cancelled (via Escape, etc)`,
2553 typeList: [ARGUMENT_TYPE.CLOSURE],
2554 }),
2555 ],
2556 unnamedArgumentList: [
2557 SlashCommandArgument.fromProps({
2558 description: t`text to display`,
2559 typeList: [ARGUMENT_TYPE.STRING],
2560 }),
2561 ],
2562 helpString: `
2563 <div>
2564 ${t`Shows a popup with the provided text and an input field.`}
2565 ${t`The <code>default</code> argument is the default value of the input field, and the text argument is the text to display.`}
2566 </div>
2567 <div>
2568 <strong>${t`Example:`}</strong>
2569 <ul>
2570 <li>
2571 <pre><code>/input default="John" placeholder="Enter your name" tooltip="Your display name" What is your name?</code></pre>
2572 </li>
2573 </ul>
2574 </div>
2575 `,
2576 }));
2577 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2578 name: 'run',
2579 aliases: ['call', 'exec'],
2580 callback: runCallback,
2581 returns: t`result of the executed closure of QR`,
2582 namedArgumentList: [
2583 new SlashCommandNamedArgument(
2584 'args', t`named arguments`, [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.BOOLEAN, ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.DICTIONARY], false, true,
2585 ),
2586 ],
2587 unnamedArgumentList: [
2588 SlashCommandArgument.fromProps({
2589 description: t`scoped variable or qr label`,
2590 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.CLOSURE],
2591 isRequired: true,
2592 enumProvider: (executor, scope) => [
2593 ...commonEnumProviders.variables('scope')(executor, scope),
2594 ...(typeof globalThis.qrEnumProviderExecutables === 'function') ? globalThis.qrEnumProviderExecutables() : [],
2595 ],
2596 }),
2597 ],
2598 helpString: `
2599 <div>
2600 ${t`Runs a closure from a scoped variable, or a Quick Reply with the specified name from a currently active preset or from another preset.`}
2601 ${t`Named arguments can be referenced in a QR with <code>{{arg::key}}</code>.`}
2602 </div>
2603 `,
2604 }));
2605 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2606 name: 'messages',
2607 callback: getMessagesCallback,
2608 aliases: ['message'],
2609 namedArgumentList: [
2610 new SlashCommandNamedArgument(
2611 'names', t`show message author names`, [ARGUMENT_TYPE.BOOLEAN], false, false, 'off', commonEnumProviders.boolean('onOff')(),
2612 ),
2613 new SlashCommandNamedArgument(
2614 'hidden', t`include hidden messages`, [ARGUMENT_TYPE.BOOLEAN], false, false, 'on', commonEnumProviders.boolean('onOff')(),
2615 ),
2616 SlashCommandNamedArgument.fromProps({
2617 name: 'role',
2618 description: t`filter messages by role`,
2619 typeList: [ARGUMENT_TYPE.STRING],
2620 enumList: [
2621 new SlashCommandEnumValue('system', null, enumTypes.enum, enumIcons.system),
2622 new SlashCommandEnumValue('assistant', null, enumTypes.enum, enumIcons.assistant),
2623 new SlashCommandEnumValue('user', null, enumTypes.enum, enumIcons.user),
2624 ],
2625 }),
2626 ],
2627 unnamedArgumentList: [
2628 SlashCommandArgument.fromProps({
2629 description: t`message index (starts with 0) or range`,
2630 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE],
2631 isRequired: true,
2632 enumProvider: commonEnumProviders.messages(),
2633 }),
2634 ],
2635 returns: t`the specified message or range of messages as a string`,
2636 helpString: `
2637 <div>
2638 ${t`Returns the specified message or range of messages as a string.`}
2639 </div>
2640 <div>
2641 ${t`Use the <code>hidden=off</code> argument to exclude hidden messages.`}
2642 </div>
2643 <div>
2644 ${t`Use the <code>role</code> argument to filter messages by role. Possible values are: system, assistant, user.`}
2645 </div>
2646 <div>
2647 <strong>${t`Examples:`}</strong>
2648 <ul>
2649 <li>
2650 <pre><code>/messages 10</code></pre>
2651 ${t`Returns the 10th message.`}
2652 </li>
2653 <li>
2654 <pre><code>/messages names=on 5-10</code></pre>
2655 ${t`Returns messages 5 through 10 with author names.`}
2656 </li>
2657 </ul>
2658 </div>
2659 `,
2660 }));
2661 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2662 name: 'setinput',
2663 callback: setInputCallback,
2664 unnamedArgumentList: [
2665 new SlashCommandArgument(
2666 t`text`, [ARGUMENT_TYPE.STRING], true,
2667 ),
2668 ],
2669 helpString: `
2670 <div>
2671 ${t`Sets the user input to the specified text and passes it to the next command through the pipe.`}
2672 </div>
2673 <div>
2674 <strong>${t`Example:`}</strong>
2675 <ul>
2676 <li>
2677 <pre><code>/setinput Hello world</code></pre>
2678 </li>
2679 </ul>
2680 </div>
2681 `,
2682 }));
2683 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2684 name: 'popup',
2685 callback: popupCallback,
2686 returns: t`Popup text`,
2687 namedArgumentList: [
2688 SlashCommandNamedArgument.fromProps({
2689 name: 'scroll',
2690 description: t`allows vertical scrolling of the content`,
2691 typeList: [ARGUMENT_TYPE.BOOLEAN],
2692 enumList: commonEnumProviders.boolean('trueFalse')(),
2693 defaultValue: 'true',
2694 }),
2695 SlashCommandNamedArgument.fromProps({
2696 name: 'large',
2697 description: t`show large popup`,
2698 typeList: [ARGUMENT_TYPE.BOOLEAN],
2699 enumList: commonEnumProviders.boolean('trueFalse')(),
2700 defaultValue: 'false',
2701 }),
2702 SlashCommandNamedArgument.fromProps({
2703 name: 'wide',
2704 description: t`show wide popup`,
2705 typeList: [ARGUMENT_TYPE.BOOLEAN],
2706 enumList: commonEnumProviders.boolean('trueFalse')(),
2707 defaultValue: 'false',
2708 }),
2709 SlashCommandNamedArgument.fromProps({
2710 name: 'wider',
2711 description: t`show wider popup`,
2712 typeList: [ARGUMENT_TYPE.BOOLEAN],
2713 enumList: commonEnumProviders.boolean('trueFalse')(),
2714 defaultValue: 'false',
2715 }),
2716 SlashCommandNamedArgument.fromProps({
2717 name: 'transparent',
2718 description: t`show transparent popup`,
2719 typeList: [ARGUMENT_TYPE.BOOLEAN],
2720 enumList: commonEnumProviders.boolean('trueFalse')(),
2721 defaultValue: 'false',
2722 }),
2723 SlashCommandNamedArgument.fromProps({
2724 name: 'okButton',
2725 description: t`text for the OK button`,
2726 typeList: [ARGUMENT_TYPE.STRING],
2727 defaultValue: 'OK',
2728 }),
2729 SlashCommandNamedArgument.fromProps({
2730 name: 'cancelButton',
2731 description: t`text for the Cancel button`,
2732 typeList: [ARGUMENT_TYPE.STRING],
2733 }),
2734 SlashCommandNamedArgument.fromProps({
2735 name: 'result',
2736 description: t`if enabled, returns the popup result (as an integer) instead of the popup text. Resolves to 1 for OK and 0 cancel button, empty string for exiting out.`,
2737 typeList: [ARGUMENT_TYPE.BOOLEAN],
2738 enumList: commonEnumProviders.boolean('trueFalse')(),
2739 defaultValue: 'false',
2740 }),
2741 SlashCommandNamedArgument.fromProps({
2742 name: 'tooltip',
2743 description: t`tooltip text shown when hovering over the popup content area`,
2744 typeList: [ARGUMENT_TYPE.STRING],
2745 }),
2746 ],
2747 unnamedArgumentList: [
2748 SlashCommandArgument.fromProps({
2749 description: t`popup text`,
2750 typeList: [ARGUMENT_TYPE.STRING],
2751 isRequired: true,
2752 }),
2753 ],
2754 helpString: `
2755 <div>
2756 ${t`Shows a blocking popup with the specified text and buttons.`}
2757 ${t`Returns the popup text.`}
2758 </div>
2759 <div>
2760 <strong>${t`Example:`}</strong>
2761 <ul>
2762 <li>
2763 <pre><code>/popup large=on wide=on okButton="Confirm" Please confirm this action.</code></pre>
2764 </li>
2765 <li>
2766 <pre><code>/popup okButton="Left" cancelButton="Right" result=true Do you want to go left or right? | /echo 0 means right, 1 means left. Choice: {{pipe}}</code></pre>
2767 </li>
2768 </ul>
2769 </div>
2770 `,
2771 }));
2772 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2773 name: 'buttons',
2774 callback: buttonsCallback,
2775 returns: t`clicked button label (or array of labels if multiple is enabled)`,
2776 namedArgumentList: [
2777 SlashCommandNamedArgument.fromProps({
2778 name: 'labels',
2779 description: t`button labels - can be an array of strings or objects with text, tooltip, and icon properties`,
2780 typeList: [ARGUMENT_TYPE.LIST],
2781 isRequired: true,
2782 }),
2783 SlashCommandNamedArgument.fromProps({
2784 name: 'multiple',
2785 description: t`if enabled multiple buttons can be clicked/toggled, and all clicked buttons are returned as an array`,
2786 typeList: [ARGUMENT_TYPE.BOOLEAN],
2787 enumList: commonEnumProviders.boolean('trueFalse')(),
2788 defaultValue: 'false',
2789 }),
2790 ],
2791 unnamedArgumentList: [
2792 SlashCommandArgument.fromProps({
2793 description: t`text`,
2794 typeList: [ARGUMENT_TYPE.STRING],
2795 isRequired: true,
2796 }),
2797 ],
2798 helpString: `
2799 <div>
2800 ${t`Shows a blocking popup with the specified text and buttons.`}
2801 ${t`Returns the clicked button label into the pipe or empty string if canceled.`}
2802 </div>
2803 <div>
2804 ${t`Labels can be simple strings or objects with <code>text</code>, <code>tooltip</code>, and <code>icon</code> (Font Awesome class) properties.`}
2805 </div>
2806 <div>
2807 <strong>${t`Example:`}</strong>
2808 <ul>
2809 <li>
2810 <pre><code>/buttons labels=["Yes","No"] Do you want to continue?</code></pre>
2811 </li>
2812 <li>
2813 <pre><code>/buttons labels=[{"text":"Save","icon":"fa-floppy-disk","tooltip":"Save changes"},{"text":"Cancel"}] Choose an action</code></pre>
2814 </li>
2815 </ul>
2816 </div>
2817 `,
2818 }));
2819 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2820 name: 'trimtokens',
2821 callback: trimTokensCallback,
2822 returns: t`trimmed text`,
2823 namedArgumentList: [
2824 new SlashCommandNamedArgument(
2825 'limit', t`number of tokens to keep`, [ARGUMENT_TYPE.NUMBER], true,
2826 ),
2827 SlashCommandNamedArgument.fromProps({
2828 name: 'direction',
2829 description: t`trim direction`,
2830 typeList: [ARGUMENT_TYPE.STRING],
2831 isRequired: true,
2832 enumList: [
2833 new SlashCommandEnumValue('start', null, enumTypes.enum, '⏪'),
2834 new SlashCommandEnumValue('end', null, enumTypes.enum, '⏩'),
2835 ],
2836 }),
2837 ],
2838 unnamedArgumentList: [
2839 new SlashCommandArgument(
2840 t`text`, [ARGUMENT_TYPE.STRING], false,
2841 ),
2842 ],
2843 helpString: `
2844 <div>
2845 ${t`Trims the start or end of text to the specified number of tokens.`}
2846 </div>
2847 <div>
2848 <strong>${t`Example:`}</strong>
2849 <ul>
2850 <li>
2851 <pre><code>/trimtokens limit=5 direction=start This is a long sentence with many words</code></pre>
2852 </li>
2853 </ul>
2854 </div>
2855 `,
2856 }));
2857 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2858 name: 'trimstart',
2859 callback: trimStartCallback,
2860 returns: t`trimmed text`,
2861 unnamedArgumentList: [
2862 new SlashCommandArgument(
2863 t`text`, [ARGUMENT_TYPE.STRING], true,
2864 ),
2865 ],
2866 helpString: `
2867 <div>
2868 ${t`Trims the text to the start of the first full sentence.`}
2869 </div>
2870 <div>
2871 <strong>${t`Example:`}</strong>
2872 <ul>
2873 <li>
2874 <pre><code>/trimstart This is a sentence. And here is another sentence.</code></pre>
2875 </li>
2876 </ul>
2877 </div>
2878 `,
2879 }));
2880 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2881 name: 'trimend',
2882 callback: trimEndCallback,
2883 returns: t`trimmed text`,
2884 unnamedArgumentList: [
2885 new SlashCommandArgument(
2886 t`text`, [ARGUMENT_TYPE.STRING], true,
2887 ),
2888 ],
2889 helpString: t`Trims the text to the end of the last full sentence.`,
2890 }));
2891 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2892 name: 'inject',
2893 returns: t`injection ID`,
2894 callback: injectCallback,
2895 namedArgumentList: [
2896 SlashCommandNamedArgument.fromProps({
2897 name: 'id',
2898 description: t`injection ID`,
2899 typeList: [ARGUMENT_TYPE.STRING],
2900 isRequired: false,
2901 enumProvider: commonEnumProviders.injects,
2902 }),
2903 new SlashCommandNamedArgument(
2904 'position', t`injection position`, [ARGUMENT_TYPE.STRING], false, false, 'after', ['before', 'after', 'chat', 'none'],
2905 ),
2906 new SlashCommandNamedArgument(
2907 'depth', t`injection depth`, [ARGUMENT_TYPE.NUMBER], false, false, '4',
2908 ),
2909 new SlashCommandNamedArgument(
2910 'scan', t`include injection content into World Info scans`, [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',
2911 ),
2912 SlashCommandNamedArgument.fromProps({
2913 name: 'role',
2914 description: t`role for in-chat injections`,
2915 typeList: [ARGUMENT_TYPE.STRING],
2916 isRequired: false,
2917 enumList: [
2918 new SlashCommandEnumValue('system', null, enumTypes.enum, enumIcons.system),
2919 new SlashCommandEnumValue('assistant', null, enumTypes.enum, enumIcons.assistant),
2920 new SlashCommandEnumValue('user', null, enumTypes.enum, enumIcons.user),
2921 ],
2922 }),
2923 new SlashCommandNamedArgument(
2924 'ephemeral', t`remove injection after generation`, [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',
2925 ),
2926 SlashCommandNamedArgument.fromProps({
2927 name: 'filter',
2928 description: t`if a filter is defined, an injection will only be performed if the closure returns true`,
2929 typeList: [ARGUMENT_TYPE.CLOSURE],
2930 isRequired: false,
2931 acceptsMultiple: false,
2932 }),
2933 ],
2934 unnamedArgumentList: [
2935 new SlashCommandArgument(
2936 t`text`, [ARGUMENT_TYPE.STRING], false,
2937 ),
2938 ],
2939 helpString: t`Injects a text into the LLM prompt for the current chat. Requires a unique injection ID (will be auto-generated if not provided). Positions: "before" main prompt, "after" main prompt, in-"chat", hidden with "none" (default: after). Depth: injection depth for the prompt (default: 4). Role: role for in-chat injections (default: system). Scan: include injection content into World Info scans (default: false). Hidden injects in "none" position are not inserted into the prompt but can be used for triggering WI entries. Returns the injection ID.`,
2940 }));
2941 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2942 name: 'listinjects',
2943 callback: listInjectsCallback,
2944 helpString: t`Lists all script injections for the current chat. Displays injects in a popup by default. Use the <code>return</code> argument to change the return type.`,
2945 returns: t`Optionally the JSON object of script injections`,
2946 namedArgumentList: [
2947 SlashCommandNamedArgument.fromProps({
2948 name: 'return',
2949 description: t`The way how you want the return value to be provided`,
2950 typeList: [ARGUMENT_TYPE.STRING],
2951 defaultValue: 'popup-html',
2952 enumList: slashCommandReturnHelper.enumList({ allowPipe: false, allowObject: true, allowChat: true, allowPopup: true, allowTextVersion: false }),
2953 forceEnum: true,
2954 }),
2955 ],
2956 }));
2957 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2958 name: 'flushinject',
2959 aliases: ['flushinjects'],
2960 unnamedArgumentList: [
2961 SlashCommandArgument.fromProps({
2962 description: t`injection ID or a variable name pointing to ID`,
2963 typeList: [ARGUMENT_TYPE.STRING],
2964 defaultValue: '',
2965 enumProvider: commonEnumProviders.injects,
2966 }),
2967 ],
2968 callback: flushInjectsCallback,
2969 helpString: t`Removes a script injection for the current chat. If no ID is provided, removes all script injections.`,
2970 }));
2971 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2972 name: 'tokens',
2973 callback: (_, text) => {
2974 if (text instanceof SlashCommandClosure || Array.isArray(text)) throw new Error(t`Unnamed argument cannot be a closure for command /tokens`);
2975 return getTokenCountAsync(text).then(count => String(count));
2976 },
2977 returns: t`number of tokens`,
2978 unnamedArgumentList: [
2979 new SlashCommandArgument(
2980 t`text`, [ARGUMENT_TYPE.STRING], true,
2981 ),
2982 ],
2983 helpString: t`Counts the number of tokens in the provided text.`,
2984 }));
2985 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2986 name: 'model',
2987 callback: modelCallback,
2988 returns: t`current model`,
2989 namedArgumentList: [
2990 SlashCommandNamedArgument.fromProps({
2991 name: 'quiet',
2992 description: t`suppress the toast message on model change`,
2993 typeList: [ARGUMENT_TYPE.BOOLEAN],
2994 defaultValue: 'false',
2995 enumList: commonEnumProviders.boolean('trueFalse')(),
2996 }),
2997 ],
2998 unnamedArgumentList: [
2999 SlashCommandArgument.fromProps({
3000 description: t`model name`,
3001 typeList: [ARGUMENT_TYPE.STRING],
3002 enumProvider: () => getModelOptions(true)?.options?.map(option => new SlashCommandEnumValue(option.value, option.value !== option.text ? option.text : null)) ?? [],
3003 }),
3004 ],
3005 helpString: t`Sets the model for the current API. Gets the current model name if no argument is provided.`,
3006 }));
3007 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3008 name: 'getpromptentry',
3009 aliases: ['getpromptentries'],
3010 callback: getPromptEntryCallback,
3011 returns: t`true/false state of prompt(s)`,
3012 namedArgumentList: [
3013 SlashCommandNamedArgument.fromProps({
3014 name: 'identifier',
3015 description: t`Prompt entry identifier(s) to retrieve`,
3016 typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.LIST],
3017 acceptsMultiple: true,
3018 enumProvider: () =>
3019 promptManager.serviceSettings.prompts
3020 .map(prompt => prompt.identifier)
3021 .map(identifier => new SlashCommandEnumValue(identifier)),
3022 }),
3023 SlashCommandNamedArgument.fromProps({
3024 name: 'name',
3025 description: t`Prompt entry name(s) to retrieve`,
3026 typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.LIST],
3027 acceptsMultiple: true,
3028 enumProvider: () =>
3029 promptManager.serviceSettings.prompts
3030 .map(prompt => prompt.name)
3031 .map(name => new SlashCommandEnumValue(name)),
3032 }),
3033 SlashCommandNamedArgument.fromProps({
3034 name: 'return',
3035 description: t`Whether the return will be simple, a list, or a dict.`,
3036 typeList: [ARGUMENT_TYPE.STRING],
3037 defaultValue: 'simple',
3038 enumList: ['simple', 'list', 'dict'],
3039 }),
3040 ],
3041 helpString: `
3042 <div>
3043 ${t`Gets the state of the specified prompt entries.`}
3044 </div>
3045 <div>
3046 ${t`If <code>return</code> is <code>simple</code> (default) then the return will be a single value if only one value was retrieved; otherwise uses a dict (if the identifier parameter was used) or a list.`}
3047 </div>
3048 `,
3049 }));
3050 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3051 name: 'setpromptentry',
3052 aliases: ['setpromptentries'],
3053 callback: setPromptEntryCallback,
3054 namedArgumentList: [
3055 SlashCommandNamedArgument.fromProps({
3056 name: 'identifier',
3057 description: t`Prompt entry identifier(s) to target`,
3058 typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.LIST],
3059 acceptsMultiple: true,
3060 enumProvider: () => {
3061 const prompts = promptManager.serviceSettings.prompts;
3062 return prompts.map(prompt => new SlashCommandEnumValue(prompt.identifier, prompt.name, enumTypes.enum));
3063 },
3064 }),
3065 SlashCommandNamedArgument.fromProps({
3066 name: 'name',
3067 description: t`Prompt entry name(s) to target`,
3068 typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.LIST],
3069 acceptsMultiple: true,
3070 enumProvider: () => {
3071 const prompts = promptManager.serviceSettings.prompts;
3072 return prompts.map(prompt => new SlashCommandEnumValue(prompt.name, prompt.identifier, enumTypes.enum));
3073 },
3074 }),
3075 ],
3076 unnamedArgumentList: [
3077 SlashCommandArgument.fromProps({
3078 description: t`Set entry/entries on or off`,
3079 typeList: [ARGUMENT_TYPE.STRING],
3080 isRequired: true,
3081 acceptsMultiple: false,
3082 defaultValue: 'toggle', // unnamed arguments don't support default values yet
3083 enumList: commonEnumProviders.boolean('onOffToggle')(),
3084 }),
3085 ],
3086 helpString: t`Sets the specified prompt manager entry/entries on or off.`,
3087 }));
3088 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3089 name: 'pm-render',
3090 callback: (args, _) => {
3091 const dryRun = !isFalseBoolean(args?.refresh?.toString());
3092 promptManager.render(dryRun);
3093 return '';
3094 },
3095 namedArgumentList: [
3096 SlashCommandNamedArgument.fromProps({
3097 name: 'refresh',
3098 description: 'Perform a dry run of the generation to refresh token counters before rendering the prompt manager',
3099 typeList: [ARGUMENT_TYPE.BOOLEAN],
3100 defaultValue: 'true',
3101 enumList: commonEnumProviders.boolean('trueFalse')(),
3102 }),
3103 ],
3104 helpString: t`Rerenders the prompt manager content. Use this if you have made changes to the prompt entries through slash commands and want to see the changes reflected in the prompt manager UI.`,
3105 }));
3106 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3107 name: 'pick-icon',
3108 callback: async () => ((await showFontAwesomePicker()) ?? false).toString(),
3109 returns: t`The chosen icon name or false if cancelled.`,
3110 helpString: `
3111 <div>${t`Opens a popup with all the available Font Awesome icons and returns the selected icon's name.`}</div>
3112 <div>
3113 <strong>${t`Example:`}</strong>
3114 <ul>
3115 <li>
3116 <pre><code>/pick-icon |\n/if left={{pipe}} rule=eq right=false\n\telse={: /echo chosen icon: "{{pipe}}" :}\n\t{: /echo cancelled icon selection :}\n|</code></pre>
3117 </li>
3118 </ul>
3119 </div>
3120 `,
3121 }));
3122 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3123 name: 'api-url',
3124 callback: setApiUrlCallback,
3125 returns: t`the current API url`,
3126 aliases: ['server'],
3127 namedArgumentList: [
3128 SlashCommandNamedArgument.fromProps({
3129 name: 'api',
3130 description: t`API to set/get the URL for - if not provided, current API is used`,
3131 typeList: [ARGUMENT_TYPE.STRING],
3132 enumList: [
3133 new SlashCommandEnumValue('custom', 'custom OpenAI-compatible', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'openai')), 'O'),
3134 new SlashCommandEnumValue('zai', 'Z.AI', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'zai')), 'Z'),
3135 new SlashCommandEnumValue('vertexai', 'Google Vertex AI', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'vertexai')), 'V'),
3136 new SlashCommandEnumValue('siliconflow', 'SiliconFlow', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'siliconflow')), 'S'),
3137 new SlashCommandEnumValue('minimax', 'MiniMax', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'minimax')), 'M'),
3138 new SlashCommandEnumValue('kobold', 'KoboldAI Classic', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'kobold')), 'K'),
3139 ...Object.values(textgen_types).filter(api => Object.keys(SERVER_INPUTS).includes(api)).map(api => new SlashCommandEnumValue(api, null, enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'textgenerationwebui')), 'T')),
3140 ],
3141 }),
3142 SlashCommandNamedArgument.fromProps({
3143 name: 'connect',
3144 description: t`Whether to auto-connect to the API after setting the URL`,
3145 typeList: [ARGUMENT_TYPE.BOOLEAN],
3146 defaultValue: 'true',
3147 enumList: commonEnumProviders.boolean('trueFalse')(),
3148 }),
3149 SlashCommandNamedArgument.fromProps({
3150 name: 'quiet',
3151 description: t`suppress the toast message on API change`,
3152 typeList: [ARGUMENT_TYPE.BOOLEAN],
3153 defaultValue: 'false',
3154 enumList: commonEnumProviders.boolean('trueFalse')(),
3155 }),
3156 ],
3157 unnamedArgumentList: [
3158 SlashCommandArgument.fromProps({
3159 description: t`API url to connect to`,
3160 typeList: [ARGUMENT_TYPE.STRING],
3161 }),
3162 ],
3163 helpString: `
3164 <div>
3165 ${t`Set the API URL / server URL / endpoint for the currently selected API, including the port. If no argument is provided, it will return the current API url.`}
3166 </div>
3167 <div>
3168 ${t`If a manual API is provided to <b>set</b> the URL, make sure to set <code>connect=false</code>, as auto-connect only works for the currently selected API, or consider switching to it with <code>/api</code> first.`}
3169 </div>
3170 <div>
3171 ${t`This slash command works for most of the Text Completion sources, KoboldAI Classic, and also Custom OpenAI compatible, Z.AI, SiliconFlow, MiniMax, and Google Vertex AI for the Chat Completion sources. If unsure which APIs are supported, check the auto-completion of the optional <code>api</code> argument of this command.`}
3172 </div>
3173 `,
3174 }));
3175 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3176 name: 'tokenizer',
3177 callback: selectTokenizerCallback,
3178 returns: t`current tokenizer`,
3179 unnamedArgumentList: [
3180 SlashCommandArgument.fromProps({
3181 description: t`tokenizer name`,
3182 typeList: [ARGUMENT_TYPE.STRING],
3183 enumList: getAvailableTokenizers().map(tokenizer =>
3184 new SlashCommandEnumValue(tokenizer.tokenizerKey, tokenizer.tokenizerName, enumTypes.enum, enumIcons.default)),
3185 }),
3186 ],
3187 helpString: `
3188 <div>
3189 ${t`Selects tokenizer by name. Gets the current tokenizer if no name is provided.`}
3190 </div>
3191 <div>
3192 <strong>${t`Available tokenizers:`}</strong>
3193 <pre><code>${getAvailableTokenizers().map(t => t.tokenizerKey).join(', ')}</code></pre>
3194 </div>
3195 `,
3196 }));
3197 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3198 name: 'upper',
3199 aliases: ['uppercase', 'to-upper'],
3200 callback: (_, text) => typeof text === 'string' ? text.toUpperCase() : '',
3201 returns: t`uppercase string`,
3202 unnamedArgumentList: [
3203 new SlashCommandArgument(
3204 t`text to affect`, [ARGUMENT_TYPE.STRING], true, false,
3205 ),
3206 ],
3207 helpString: t`Converts the provided string to uppercase.`,
3208 }));
3209 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3210 name: 'lower',
3211 aliases: ['lowercase', 'to-lower'],
3212 callback: (_, text) => typeof text === 'string' ? text.toLowerCase() : '',
3213 returns: t`lowercase string`,
3214 unnamedArgumentList: [
3215 new SlashCommandArgument(
3216 t`text to affect`, [ARGUMENT_TYPE.STRING], true, false,
3217 ),
3218 ],
3219 helpString: t`Converts the provided string to lowercase.`,
3220 }));
3221 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3222 name: 'substr',
3223 aliases: ['substring'],
3224 callback: (arg, text) => typeof text === 'string' ? text.slice(...[Number(arg.start), arg.end && Number(arg.end)]) : '',
3225 returns: t`substring`,
3226 namedArgumentList: [
3227 new SlashCommandNamedArgument(
3228 'start', t`start index`, [ARGUMENT_TYPE.NUMBER], false, false,
3229 ),
3230 new SlashCommandNamedArgument(
3231 'end', t`end index`, [ARGUMENT_TYPE.NUMBER], false, false,
3232 ),
3233 ],
3234 unnamedArgumentList: [
3235 new SlashCommandArgument(
3236 t`text to affect`, [ARGUMENT_TYPE.STRING], true, false,
3237 ),
3238 ],
3239 helpString: `
3240 <div>
3241 ${t`Extracts text from the provided string.`}
3242 </div>
3243 <div>
3244 ${t`If <code>start</code> is omitted, it's treated as 0.<br />`}
3245 ${t`If <code>start</code> < 0, the index is counted from the end of the string.<br />`}
3246 ${t`If <code>start</code> >= the string's length, an empty string is returned.<br />`}
3247 ${t`If <code>end</code> is omitted, or if <code>end</code> >= the string's length, extracts to the end of the string.<br />`}
3248 ${t`If <code>end</code> < 0, the index is counted from the end of the string.<br />`}
3249 ${t`If <code>end</code> <= <code>start</code> after normalizing negative values, an empty string is returned.`}
3250 </div>
3251 <div>
3252 <strong>${t`Example:`}</strong>
3253 <pre>/let x The morning is upon us. || </pre>
3254 <pre>/substr start=-3 {{var::x}} | /echo |/# us. ||</pre>
3255 <pre>/substr start=-3 end=-1 {{var::x}} | /echo |/# us ||</pre>
3256 <pre>/substr end=-1 {{var::x}} | /echo |/# The morning is upon us ||</pre>
3257 <pre>/substr start=4 end=-1 {{var::x}} | /echo |/# morning is upon us ||</pre>
3258 </div>
3259 `,
3260 }));
3261 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3262 name: 'is-mobile',
3263 callback: () => String(isMobile()),
3264 returns: ARGUMENT_TYPE.BOOLEAN,
3265 helpString: t`Returns true if the current device is a mobile device, false otherwise. Equivalent to <code>{{isMobile}}</code> macro.`,
3266 }));
3267 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3268 name: 'chat-render',
3269 helpString: t`Renders a specified number of messages into the chat window. Displays all messages if no argument is provided.`,
3270 callback: async (args, number) => {
3271 await showMoreMessages(number && !isNaN(Number(number)) ? Number(number) : Number.MAX_SAFE_INTEGER);
3272 if (isTrueBoolean(String(args?.scroll ?? ''))) {
3273 $('#chat').scrollTop(0);
3274 }
3275 return '';
3276 },
3277 namedArgumentList: [
3278 SlashCommandNamedArgument.fromProps({
3279 name: 'scroll',
3280 description: t`scroll to the top after rendering`,
3281 typeList: [ARGUMENT_TYPE.BOOLEAN],
3282 defaultValue: 'false',
3283 enumList: commonEnumProviders.boolean('trueFalse')(),
3284 }),
3285 ],
3286 unnamedArgumentList: [
3287 new SlashCommandArgument(
3288 t`number of messages`, [ARGUMENT_TYPE.NUMBER], false,
3289 ),
3290 ],
3291 }));
3292 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3293 name: 'chat-reload',
3294 helpString: t`Reloads the current chat.`,
3295 callback: async () => {
3296 await reloadCurrentChat();
3297 return '';
3298 },
3299 }));
3300 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3301 name: 'replace',
3302 aliases: ['re'],
3303 callback: (async ({ mode = 'literal', pattern, replacer = '' }, text) => {
3304 if (!pattern) {
3305 throw new Error(t`Argument of 'pattern=' cannot be empty`);
3306 }
3307 text = text.toString();
3308 pattern = pattern.toString();
3309 replacer = replacer.toString();
3310 switch (mode) {
3311 case 'literal':
3312 return text.replaceAll(pattern, replacer);
3313 case 'regex':
3314 return text.replace(regexFromString(pattern), replacer);
3315 default:
3316 throw new Error(t`Invalid '/replace mode=' argument specified!`);
3317 }
3318 }),
3319 returns: t`replaced text`,
3320 namedArgumentList: [
3321 SlashCommandNamedArgument.fromProps({
3322 name: 'mode',
3323 description: t`Replaces occurrence(s) of a pattern`,
3324 typeList: [ARGUMENT_TYPE.STRING],
3325 defaultValue: 'literal',
3326 enumList: ['literal', 'regex'],
3327 }),
3328 new SlashCommandNamedArgument(
3329 'pattern', t`pattern to search with`, [ARGUMENT_TYPE.STRING], true, false,
3330 ),
3331 new SlashCommandNamedArgument(
3332 'replacer', t`replacement text for matches`, [ARGUMENT_TYPE.STRING], false, false, '',
3333 ),
3334 ],
3335 unnamedArgumentList: [
3336 new SlashCommandArgument(
3337 t`text to affect`, [ARGUMENT_TYPE.STRING], true, false,
3338 ),
3339 ],
3340 helpString: `
3341 <div>
3342 ${t`Replaces text within the provided string based on the pattern.`}
3343 </div>
3344 <div>
3345 ${t`If <code>mode</code> is <code>literal</code> (or omitted), <code>pattern</code> is a literal search string (case-sensitive).<br />`}
3346 ${t`If <code>mode</code> is <code>regex</code>, <code>pattern</code> is parsed as an ECMAScript Regular Expression.<br />`}
3347 ${t`The <code>replacer</code> replaces based on the <code>pattern</code> in the input text.<br />`}
3348 ${t`If <code>replacer</code> is omitted, the replacement(s) will be an empty string.<br />`}
3349 </div>
3350 <div>
3351 <strong>${t`Example:`}</strong>
3352 <pre><code class="language-stscript">/let x Blue house and blue car || </code></pre>
3353 <pre><code class="language-stscript">/replace pattern="blue" {{var::x}} | /echo |/# Blue house and car ||</code></pre>
3354 <pre><code class="language-stscript">/replace pattern="blue" replacer="red" {{var::x}} | /echo |/# Blue house and red car ||</code></pre>
3355 <pre><code class="language-stscript">/replace mode=regex pattern="/blue/i" replacer="red" {{var::x}} | /echo |/# red house and blue car ||</code></pre>
3356 <pre><code class="language-stscript">/replace mode=regex pattern="/blue/gi" replacer="red" {{var::x}} | /echo |/# red house and red car ||</code></pre>
3357 </div>
3358 `,
3359 }));
3360 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3361 name: 'test',
3362 callback: (({ pattern }, text) => {
3363 if (!pattern) {
3364 throw new Error(t`Argument of 'pattern=' cannot be empty`);
3365 }
3366 const re = regexFromString(pattern.toString());
3367 if (!re) {
3368 throw new Error(t`The value of 'pattern' argument is not a valid regular expression.`);
3369 }
3370 return JSON.stringify(re.test(text.toString()));
3371 }),
3372 returns: 'true | false',
3373 namedArgumentList: [
3374 new SlashCommandNamedArgument(
3375 'pattern', t`pattern to find`, [ARGUMENT_TYPE.STRING], true, false,
3376 ),
3377 ],
3378 unnamedArgumentList: [
3379 new SlashCommandArgument(
3380 t`text to test`, [ARGUMENT_TYPE.STRING], true, false,
3381 ),
3382 ],
3383 helpString: `
3384 <div>
3385 ${t`Tests text for a regular expression match.`}
3386 </div>
3387 <div>
3388 ${t`Returns <code>true</code> if the match is found, <code>false</code> otherwise.`}
3389 </div>
3390 <div>
3391 <strong>${t`Example:`}</strong>
3392 <pre><code class="language-stscript">/let x Blue house and green car ||</code></pre>
3393 <pre><code class="language-stscript">/test pattern="green" {{var::x}} | /echo |/# true ||</code></pre>
3394 <pre><code class="language-stscript">/test pattern="blue" {{var::x}} | /echo |/# false ||</code></pre>
3395 <pre><code class="language-stscript">/test pattern="/blue/i" {{var::x}} | /echo |/# true ||</code></pre>
3396 </div>
3397 `,
3398 }));
3399 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3400 name: 'match',
3401 callback: (({ pattern }, text) => {
3402 if (!pattern) {
3403 throw new Error(t`Argument of 'pattern=' cannot be empty`);
3404 }
3405 const re = regexFromString(pattern.toString());
3406 if (!re) {
3407 throw new Error(t`The value of 'pattern' argument is not a valid regular expression.`);
3408 }
3409 if (re.flags.includes('g')) {
3410 return JSON.stringify([...text.toString().matchAll(re)]);
3411 } else {
3412 const match = text.toString().match(re);
3413 return match ? JSON.stringify(match) : '';
3414 }
3415 }),
3416 returns: t`group array for each match`,
3417 namedArgumentList: [
3418 new SlashCommandNamedArgument(
3419 'pattern', t`pattern to find`, [ARGUMENT_TYPE.STRING], true, false,
3420 ),
3421 ],
3422 unnamedArgumentList: [
3423 new SlashCommandArgument(
3424 t`text to match against`, [ARGUMENT_TYPE.STRING], true, false,
3425 ),
3426 ],
3427 helpString: `
3428 <div>
3429 ${t`Retrieves regular expression matches in the given text`}
3430 </div>
3431 <div>
3432 ${t`Returns an array of groups (with the first group being the full match). If the regex contains the global flag (i.e. <code>/g</code>), multiple nested arrays are returned for each match. If the regex is global, returns <code>[]</code> if no matches are found, otherwise it returns an empty string.`}
3433 </div>
3434 <div>
3435 <strong>${t`Example:`}</strong>
3436 <pre><code class="language-stscript">/let x color_green green lamp color_blue ||</code></pre>
3437 <pre><code class="language-stscript">/match pattern="green" {{var::x}} | /echo |/# [ "green" ] ||</code></pre>
3438 <pre><code class="language-stscript">/match pattern="color_(\\w+)" {{var::x}} | /echo |/# [ "color_green", "green" ] ||</code></pre>
3439 <pre><code class="language-stscript">/match pattern="/color_(\\w+)/g" {{var::x}} | /echo |/# [ [ "color_green", "green" ], [ "color_blue", "blue" ] ] ||</code></pre>
3440 <pre><code class="language-stscript">/match pattern="orange" {{var::x}} | /echo |/# ||</code></pre>
3441 <pre><code class="language-stscript">/match pattern="/orange/g" {{var::x}} | /echo |/# [] ||</code></pre>
3442 </div>
3443 `,
3444 }));
3445
3446 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3447 name: 'chat-jump',
3448 aliases: ['chat-scrollto', 'floor-teleport'],
3449 callback: async (_, index) => {
3450 const messageIndex = Number(index);
3451
3452 if (isNaN(messageIndex) || messageIndex < 0 || messageIndex >= chat.length) {
3453 toastr.warning(t`Invalid message index: ${index}. Please enter a number between 0 and ${chat.length}.`);
3454 console.warn(`WARN: Invalid message index provided for /chat-jump: ${index}. Max index: ${chat.length}`);
3455 return '';
3456 }
3457
3458 // Load more messages if needed
3459 const firstDisplayedMessageId = getFirstDisplayedMessageId();
3460 if (isFinite(firstDisplayedMessageId) && messageIndex < firstDisplayedMessageId) {
3461 const needToLoadCount = firstDisplayedMessageId - messageIndex;
3462 await showMoreMessages(needToLoadCount);
3463 await delay(debounce_timeout.quick);
3464 }
3465
3466 const chatContainer = document.getElementById('chat');
3467 const messageElement = document.querySelector(`#chat .mes[mesid="${messageIndex}"]`);
3468
3469 if (messageElement instanceof HTMLElement && chatContainer instanceof HTMLElement) {
3470 const elementRect = messageElement.getBoundingClientRect();
3471 const containerRect = chatContainer.getBoundingClientRect();
3472
3473 const scrollPosition = elementRect.top - containerRect.top + chatContainer.scrollTop;
3474 chatContainer.scrollTo({
3475 top: scrollPosition,
3476 behavior: 'smooth',
3477 });
3478
3479 flashHighlight($(messageElement), 2000);
3480 } else {
3481 toastr.warning(t`Could not find element for message ${messageIndex}. It might not be rendered yet or the index is invalid.`);
3482 console.warn(`WARN: Element not found for message index ${messageIndex} in /chat-jump.`);
3483 }
3484
3485 return '';
3486 },
3487 unnamedArgumentList: [
3488 SlashCommandArgument.fromProps({
3489 description: t`The message index (0-based) to scroll to.`,
3490 typeList: [ARGUMENT_TYPE.NUMBER],
3491 isRequired: true,
3492 enumProvider: commonEnumProviders.messages(),
3493 }),
3494 ],
3495 helpString: `
3496 <div>
3497 ${t`Scrolls the chat view to the specified message index. Index starts at 0.`}
3498 </div>
3499 <div>
3500 <strong>${t`Example:`}</strong> <pre><code>/chat-jump 10</code></pre> ${t`Scrolls to the 11th message (id=10).`}
3501 </div>
3502 `,
3503 }));
3504
3505 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3506 name: 'clipboard-get',
3507 returns: t`clipboard text`,
3508 callback: async () => {
3509 if (!navigator.clipboard) {
3510 toastr.warning(t`Clipboard API not available in this context.`);
3511 return '';
3512 }
3513
3514 try {
3515 const text = await navigator.clipboard.readText();
3516 return text;
3517 } catch (error) {
3518 console.error('Error reading clipboard:', error);
3519 toastr.warning(t`Failed to read clipboard text. Have you granted the permission?`);
3520 return '';
3521 }
3522 },
3523 helpString: t`Retrieves the text from the OS clipboard. Only works in secure contexts (HTTPS or localhost). Browser may ask for permission.`,
3524 }));
3525
3526 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3527 name: 'clipboard-set',
3528 callback: async (_, text) => {
3529 await copyText(text.toString());
3530 return '';
3531 },
3532 unnamedArgumentList: [
3533 SlashCommandArgument.fromProps({
3534 description: t`text to copy to the clipboard`,
3535 typeList: [ARGUMENT_TYPE.STRING],
3536 isRequired: true,
3537 acceptsMultiple: false,
3538 }),
3539 ],
3540 helpString: t`Copies the provided text to the OS clipboard. Returns an empty string.`,
3541 }));
3542
3543
3544 const promptPostProcessingEnumProvider = () => Array
3545 .from(document.getElementById('custom_prompt_post_processing').querySelectorAll('option'))
3546 .map(option => new SlashCommandEnumValue(option.value || 'none', option.textContent, enumTypes.enum));
3547 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3548 name: 'prompt-post-processing',
3549 aliases: ['ppp'],
3550 helpString: `
3551 <div>
3552 ${t`Sets a "Prompt Post-Processing" type. Gets the current selection if no value is provided.`}
3553 </div>
3554 <div>
3555 <strong>${t`Examples:`}</strong>
3556 </div>
3557 <ul>
3558 <li><pre><code class="language-stscript">/prompt-post-processing | /echo</code></pre></li>
3559 <li><pre><code class="language-stscript">/prompt-post-processing single</code></pre></li>
3560 </ul>
3561 `,
3562 namedArgumentList: [],
3563 unnamedArgumentList: [
3564 SlashCommandArgument.fromProps({
3565 description: t`value`,
3566 typeList: [ARGUMENT_TYPE.STRING],
3567 acceptsMultiple: false,
3568 isRequired: true,
3569 forceEnum: true,
3570 enumProvider: promptPostProcessingEnumProvider,
3571 }),
3572 ],
3573 callback: (_args, value) => {
3574 const stringValue = String(value ?? '').trim().toLowerCase();
3575 if (!stringValue) {
3576 return oai_settings.custom_prompt_post_processing || 'none';
3577 }
3578
3579 const validValues = promptPostProcessingEnumProvider().map(option => option.value);
3580 if (!validValues.includes(stringValue)) {
3581 throw new Error(t`Invalid value "${stringValue}". Valid values are: ${validValues.join(', ')}`);
3582 }
3583
3584 // 'none' value must be coerced to an empty string
3585 oai_settings.custom_prompt_post_processing = stringValue === 'none' ? '' : stringValue;
3586 $('#custom_prompt_post_processing').val(oai_settings.custom_prompt_post_processing);
3587 saveSettingsDebounced();
3588
3589 return oai_settings.custom_prompt_post_processing;
3590 },
3591 }));
3592
3593 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3594 name: 'reroll-pick',
3595 callback: (_, value) => {
3596 const currentSeed = chat_metadata.pick_reroll_seed ?? 0;
3597 const parsedValue = value ? parseInt(String(value), 10) : NaN;
3598
3599 if (!isNaN(parsedValue)) {
3600 chat_metadata.pick_reroll_seed = parsedValue;
3601 } else {
3602 chat_metadata.pick_reroll_seed = currentSeed + 1;
3603 }
3604
3605 saveMetadataDebounced();
3606 return String(chat_metadata.pick_reroll_seed);
3607 },
3608 returns: t`The new reroll seed value.`,
3609 unnamedArgumentList: [
3610 SlashCommandArgument.fromProps({
3611 description: t`Optional seed value to set. If not provided, increments current seed by 1.`,
3612 typeList: [ARGUMENT_TYPE.NUMBER],
3613 }),
3614 ],
3615 helpString: `
3616 <div>
3617 ${t`Rerolls all <code>{{pick}}</code> macro choices in the current chat.`}
3618 </div>
3619 <div>
3620 ${t`The <code>{{pick}}</code> macro normally keeps stable choices per chat. This command changes the seed used for all picks, causing them to resolve to (possibly) different values.`}
3621 </div>
3622 <div>
3623 ${t`If a number is provided, sets the seed to that value. Otherwise, increments the current seed by 1.`}
3624 </div>
3625 <div>
3626 <strong>${t`Example:`}</strong>
3627 <ul>
3628 <li><pre><code>/reroll-pick</code></pre> ${t`Increments the seed by 1.`}</li>
3629 <li><pre><code>/reroll-pick 5</code></pre> ${t`Sets the seed to 5.`}</li>
3630 </ul>
3631 </div>
3632 `,
3633 }));
3634
3635 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3636 name: 'beep',
3637 aliases: ['ding'],
3638 returns: t`an empty string`,
3639 callback: async () => {
3640 playMessageSound({ force: true });
3641 return '';
3642 },
3643 helpString: t`Plays the message received sound effect.`,
3644 }));
3645
3646 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3647 name: 'array-wrap',
3648 aliases: ['list-wrap'],
3649 returns: t`unnamed argument value wrapped into an array`,
3650 helpString: t`Wraps a single unnamed argument into an array if it's not already an array. If the value is an empty string, returns an empty array.`,
3651 namedArgumentList: [
3652 SlashCommandNamedArgument.fromProps({
3653 name: 'stringify',
3654 description: t`Whether JSON primitives (numbers, booleans, nulls) should be treated as strings, i.e. ["null"] when stringify=true vs. [null] when stringify=false.`,
3655 typeList: [ARGUMENT_TYPE.BOOLEAN],
3656 defaultValue: 'true',
3657 enumList: commonEnumProviders.boolean('trueFalse')(),
3658 }),
3659 ],
3660 unnamedArgumentList: [
3661 SlashCommandArgument.fromProps({
3662 description: t`value`,
3663 acceptsMultiple: false,
3664 isRequired: true,
3665 typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.DICTIONARY, ARGUMENT_TYPE.BOOLEAN, ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.LIST],
3666 }),
3667 ],
3668 callback: (args, value) => {
3669 // Closures are not supported
3670 if (value instanceof SlashCommandClosure) {
3671 throw new SlashCommandExecutionError(t`Closures are not supported as unnamed arguments for /array-wrap. Did you forget to call the closure with parentheses?`);
3672 }
3673
3674 // Multiple unnamed arguments are not supported since acceptsMultiple is false, but check just in case
3675 if (Array.isArray(value)) {
3676 throw new SlashCommandExecutionError(t`/array-wrap does not support multiple unnamed arguments.`);
3677 }
3678
3679 // Empty string - empty arrays
3680 if (value === '') {
3681 return JSON.stringify([]);
3682 }
3683
3684 try {
3685 // If the value is a valid JSON string, parse it
3686 const parsedValue = JSON.parse(value);
3687
3688 // Already an array - return as-is
3689 if (Array.isArray(parsedValue)) {
3690 return value;
3691 }
3692
3693 // If it's an object, wrap it into an array and stringify
3694 if (typeof parsedValue === 'object' && parsedValue !== null) {
3695 return JSON.stringify([parsedValue]);
3696 }
3697
3698 // For primitive values, check if we should take the parsed or original value based on the stringify argument
3699 const isJsonPrimitive = parsedValue === null || ['string', 'number', 'boolean'].includes(typeof parsedValue);
3700 if (isJsonPrimitive && isFalseBoolean(String(args?.stringify?.toString()))) {
3701 return JSON.stringify([parsedValue]);
3702 }
3703
3704 // Wrap the original value (string, number, boolean) into an array, preserving quotes for strings
3705 return JSON.stringify([value]);
3706 } catch {
3707 // Not a valid JSON string - wrap the original value
3708 return JSON.stringify([value]);
3709 }
3710 },
3711 }));
3712 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3713 name: 'array-unwrap',
3714 aliases: ['list-unwrap'],
3715 returns: t`unnamed argument value unwrapped from an array`,
3716 helpString: t`Unwraps the first element of an array provided as an unnamed argument. If the value is not an array, returns the value as-is. If the array is empty, returns an empty string.`,
3717 unnamedArgumentList: [
3718 SlashCommandArgument.fromProps({
3719 description: t`value`,
3720 acceptsMultiple: false,
3721 isRequired: true,
3722 typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.DICTIONARY, ARGUMENT_TYPE.BOOLEAN, ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.LIST],
3723 }),
3724 ],
3725 callback: (_args, value) => {
3726 // Closures are not supported
3727 if (value instanceof SlashCommandClosure) {
3728 throw new SlashCommandExecutionError(t`Closures are not supported as unnamed arguments for /array-unwrap. Did you forget to call the closure with parentheses?`);
3729 }
3730
3731 // Multiple unnamed arguments are not supported since acceptsMultiple is false, but check just in case
3732 if (Array.isArray(value)) {
3733 throw new SlashCommandExecutionError(t`/array-unwrap does not support multiple unnamed arguments.`);
3734 }
3735
3736 try {
3737 // If the value is a JSON array, get the first element
3738 const parsed = JSON.parse(value);
3739
3740 if (Array.isArray(parsed)) {
3741 const unwrappedValue = parsed?.[0] ?? '';
3742
3743 // If the first element is null or undefined, return an empty string
3744 if (unwrappedValue === null || unwrappedValue === undefined) {
3745 return '';
3746 }
3747
3748 // If the first element is an object, stringify it.
3749 if (typeof unwrappedValue === 'object') {
3750 return JSON.stringify(unwrappedValue);
3751 }
3752
3753 // Otherwise, return it as a string.
3754 return String(unwrappedValue);
3755 }
3756 return value;
3757 } catch {
3758 // Not a valid JSON - return as-is
3759 return value;
3760 }
3761 },
3762 }));
3763
3764 registerVariableCommands();
3765 registerActionLoaderSlashCommands();
3766}
3767
3768const NARRATOR_NAME_KEY = 'narrator_name';
3769const NARRATOR_NAME_DEFAULT = 'System';
3770export const COMMENT_NAME_DEFAULT = 'Note';
3771const SCRIPT_PROMPT_KEY = 'script_inject_';
3772
3773/**
3774 * Adds a new script injection to the chat.
3775 * @param {import('./slash-commands/SlashCommand.js').NamedArguments} args Named arguments
3776 * @param {import('./slash-commands/SlashCommand.js').UnnamedArguments} value Unnamed argument
3777 */
3778function injectCallback(args, value) {
3779 const positions = {
3780 'before': extension_prompt_types.BEFORE_PROMPT,
3781 'after': extension_prompt_types.IN_PROMPT,
3782 'chat': extension_prompt_types.IN_CHAT,
3783 'none': extension_prompt_types.NONE,
3784 };
3785 const roles = {
3786 'system': extension_prompt_roles.SYSTEM,
3787 'user': extension_prompt_roles.USER,
3788 'assistant': extension_prompt_roles.ASSISTANT,
3789 };
3790
3791 const id = String(args?.id ?? '') || Math.random().toString(36).substring(2);
3792 const ephemeral = isTrueBoolean(String(args?.ephemeral ?? ''));
3793
3794 const defaultPosition = 'after';
3795 const defaultDepth = 4;
3796 const positionValue = args?.position ?? defaultPosition;
3797 const position = positions[positionValue] ?? positions[defaultPosition];
3798 const depthValue = Number(args?.depth ?? defaultDepth);
3799 const depth = isNaN(depthValue) ? defaultDepth : depthValue;
3800 const roleValue = typeof args?.role === 'string' ? args.role.toLowerCase().trim() : Number(args?.role ?? extension_prompt_roles.SYSTEM);
3801 const role = roles[roleValue] ?? extension_prompt_roles.SYSTEM;
3802 const scan = isTrueBoolean(String(args?.scan));
3803 const filter = args?.filter instanceof SlashCommandClosure ? args.filter.rawText : null;
3804 const filterFunction = args?.filter instanceof SlashCommandClosure ? closureToFilter(args.filter) : null;
3805 value = value || '';
3806 if (args?.filter && !String(filter ?? '').trim()) {
3807 throw new Error(t`Failed to parse the filter argument. Make sure it is a valid non-empty closure.`);
3808 }
3809
3810 const prefixedId = `${SCRIPT_PROMPT_KEY}${id}`;
3811
3812 if (!chat_metadata.script_injects) {
3813 chat_metadata.script_injects = {};
3814 }
3815
3816 if (value) {
3817 const inject = { value, position, depth, scan, role, filter };
3818 chat_metadata.script_injects[id] = inject;
3819 } else {
3820 delete chat_metadata.script_injects[id];
3821 }
3822
3823 setExtensionPrompt(prefixedId, String(value), position, depth, scan, role, filterFunction);
3824 saveMetadataDebounced();
3825
3826 if (ephemeral) {
3827 let deleted = false;
3828 const unsetInject = () => {
3829 if (deleted) {
3830 return;
3831 }
3832 console.log('Removing ephemeral script injection', id);
3833 delete chat_metadata.script_injects[id];
3834 setExtensionPrompt(prefixedId, '', position, depth, scan, role, filterFunction);
3835 saveMetadataDebounced();
3836 deleted = true;
3837 };
3838 eventSource.once(event_types.GENERATION_ENDED, unsetInject);
3839 eventSource.once(event_types.GENERATION_STOPPED, unsetInject);
3840 }
3841
3842 return id;
3843}
3844
3845async function listInjectsCallback(args) {
3846 /** @type {import('./slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */
3847 let returnType = args.return;
3848
3849 // Now the actual new return type handling
3850 const buildTextValue = (injects) => {
3851 const injectsStr = Object.entries(injects)
3852 .map(([id, inject]) => {
3853 const position = Object.entries(extension_prompt_types);
3854 const positionName = position.find(([_, value]) => value === inject.position)?.[0] ?? t`unknown`;
3855 return `* **${id}**: <code>${inject.value}</code> (${positionName}, ${t`depth`}: ${inject.depth}, ${t`scan`}: ${inject.scan ?? false}, ${t`role`}: ${inject.role ?? extension_prompt_roles.SYSTEM})`;
3856 })
3857 .join('\n');
3858 return `### ${t`Script injections:`}\n${injectsStr || t`No script injections for the current chat`}`;
3859 };
3860
3861 return await slashCommandReturnHelper.doReturn(returnType ?? 'popup-html', chat_metadata.script_injects ?? {}, { objectToStringFunc: buildTextValue });
3862}
3863
3864/**
3865 * Flushes script injections for the current chat.
3866 * @param {import('./slash-commands/SlashCommand.js').NamedArguments} _ Named arguments
3867 * @param {string} value Unnamed argument
3868 * @returns {string} Empty string
3869 */
3870function flushInjectsCallback(_, value) {
3871 if (!chat_metadata.script_injects) {
3872 return '';
3873 }
3874
3875 const idArgument = value;
3876
3877 for (const [id, inject] of Object.entries(chat_metadata.script_injects)) {
3878 if (idArgument && id !== idArgument) {
3879 continue;
3880 }
3881
3882 const prefixedId = `${SCRIPT_PROMPT_KEY}${id}`;
3883 setExtensionPrompt(prefixedId, '', inject.position, inject.depth, inject.scan, inject.role);
3884 delete chat_metadata.script_injects[id];
3885 }
3886
3887 saveMetadataDebounced();
3888 return '';
3889}
3890
3891export function processChatSlashCommands() {
3892 const context = getContext();
3893
3894 if (!(context.chatMetadata.script_injects)) {
3895 return;
3896 }
3897
3898 for (const id of Object.keys(context.extensionPrompts)) {
3899 if (!id.startsWith(SCRIPT_PROMPT_KEY)) {
3900 continue;
3901 }
3902
3903 console.log('Removing script injection', id);
3904 delete context.extensionPrompts[id];
3905 }
3906
3907 for (const [id, inject] of Object.entries(context.chatMetadata.script_injects)) {
3908 /**
3909 * Rehydrates a filter closure from a string.
3910 * @returns {SlashCommandClosure | null}
3911 */
3912 function reviveFilterClosure() {
3913 if (!inject.filter) {
3914 return null;
3915 }
3916
3917 try {
3918 return new SlashCommandParser().parse(inject.filter, true);
3919 } catch (error) {
3920 console.warn('Failed to revive filter closure for script injection', id, error);
3921 return null;
3922 }
3923 }
3924
3925 const prefixedId = `${SCRIPT_PROMPT_KEY}${id}`;
3926 const filterClosure = reviveFilterClosure();
3927 const filter = filterClosure ? closureToFilter(filterClosure) : null;
3928 console.log('Adding script injection', id);
3929 setExtensionPrompt(prefixedId, inject.value, inject.position, inject.depth, inject.scan, inject.role, filter);
3930 }
3931}
3932
3933function setInputCallback(_, value) {
3934 $('#send_textarea').val(value || '')[0].dispatchEvent(new Event('input', { bubbles: true }));
3935 return value;
3936}
3937
3938function trimStartCallback(_, value) {
3939 if (!value) {
3940 return '';
3941 }
3942
3943 return trimToStartSentence(value);
3944}
3945
3946function trimEndCallback(_, value) {
3947 if (!value) {
3948 return '';
3949 }
3950
3951 return trimToEndSentence(value);
3952}
3953
3954async function trimTokensCallback(arg, value) {
3955 if (!value) {
3956 console.warn('WARN: No argument provided for /trimtokens command');
3957 return '';
3958 }
3959
3960 const limit = Number(resolveVariable(arg.limit));
3961
3962 if (isNaN(limit)) {
3963 console.warn(`WARN: Invalid limit provided for /trimtokens command: ${limit}`);
3964 return value;
3965 }
3966
3967 if (limit <= 0) {
3968 return '';
3969 }
3970
3971 const direction = arg.direction || 'end';
3972 const tokenCount = await getTokenCountAsync(value);
3973
3974 // Token count is less than the limit, do nothing
3975 if (tokenCount <= limit) {
3976 return value;
3977 }
3978
3979 const { tokenizerName, tokenizerId } = getFriendlyTokenizerName(main_api);
3980 console.debug('Requesting tokenization for /trimtokens command', tokenizerName);
3981
3982 try {
3983 const textTokens = getTextTokens(tokenizerId, value);
3984
3985 if (!Array.isArray(textTokens) || !textTokens.length) {
3986 console.warn('WARN: No tokens returned for /trimtokens command, falling back to estimation');
3987 const percentage = limit / tokenCount;
3988 const trimIndex = Math.floor(value.length * percentage);
3989 const trimmedText = direction === 'start' ? value.substring(trimIndex) : value.substring(0, value.length - trimIndex);
3990 return trimmedText;
3991 }
3992
3993 const sliceTokens = direction === 'start' ? textTokens.slice(0, limit) : textTokens.slice(-limit);
3994 const { text } = decodeTextTokens(tokenizerId, sliceTokens);
3995 return text;
3996 } catch (error) {
3997 console.warn('WARN: Tokenization failed for /trimtokens command, returning original', error);
3998 return value;
3999 }
4000}
4001
4002/**
4003 * @typedef {object} ButtonLabel
4004 * @property {string} text - The button text
4005 * @property {string} [tooltip] - Optional tooltip text
4006 * @property {string} [icon] - Optional Font Awesome icon class (e.g., 'fa-floppy-disk')
4007 */
4008
4009/**
4010 * @param {object} args - Named arguments for the command
4011 * @param {string} args.labels - JSON string of an array of button labels (strings or ButtonLabel objects)
4012 * @param {string} [args.multiple=false] - Flag indicating if multiple buttons can be toggled
4013 * @param {string} text - The text content to be displayed within the popup
4014 *
4015 * @returns {Promise<string>} - A promise that resolves to a string of the button labels selected
4016 * If 'multiple' is true, returns a JSON string array of labels.
4017 * If 'multiple' is false, returns a single label string.
4018 */
4019async function buttonsCallback(args, text) {
4020 try {
4021 /** @type {(string|ButtonLabel)[]} */
4022 const rawButtons = JSON.parse(resolveVariable(args?.labels));
4023
4024 if (!Array.isArray(rawButtons) || !rawButtons.length) {
4025 console.warn('WARN: Invalid labels provided for /buttons command');
4026 return '';
4027 }
4028
4029 // Normalize buttons to ButtonLabel format for consistent handling
4030 /** @type {ButtonLabel[]} */
4031 const buttons = rawButtons.map(btn => typeof btn === 'string' ? { text: btn } : btn);
4032
4033 // Validate raw buttons: each entry must be a string or a non-null object with a string `text` field that has content
4034 if (!buttons.every(btn => typeof btn === 'object' && btn !== null && typeof btn.text === 'string' && btn.text)) {
4035 console.warn('WARN: Invalid button label entry provided for /buttons command: each entry must be a string or an object with a "text" property');
4036 return '';
4037 }
4038
4039 /** @type {Set<number>} */
4040 const multipleToggledState = new Set();
4041 const multiple = isTrueBoolean(args?.multiple);
4042
4043 // Map custom buttons to results. Start at 2 because 1 and 0 are reserved for ok and cancel
4044 /** @type {Map<number, ButtonLabel>} */
4045 const resultToButtonMap = new Map(buttons.map((button, index) => [index + 2, button]));
4046
4047 return new Promise(async (resolve) => {
4048 const safeValue = DOMPurify.sanitize(text || '');
4049
4050 /** @type {Popup} */
4051 let popup;
4052
4053 const buttonContainer = document.createElement('div');
4054 buttonContainer.classList.add('flex-container', 'flexFlowColumn', 'wide100p');
4055
4056 const scrollableContainer = document.createElement('div');
4057 scrollableContainer.classList.add('scrollable-buttons-container');
4058
4059 for (const [result, button] of resultToButtonMap) {
4060 const buttonElement = document.createElement('div');
4061 buttonElement.classList.add('menu_button', 'wide100p');
4062
4063 if (multiple) {
4064 buttonElement.classList.add('toggleable');
4065 buttonElement.dataset.toggleValue = String(result);
4066 buttonElement.addEventListener('click', async () => {
4067 buttonElement.classList.toggle('toggled');
4068 if (buttonElement.classList.contains('toggled')) {
4069 multipleToggledState.add(result);
4070 } else {
4071 multipleToggledState.delete(result);
4072 }
4073 });
4074 } else {
4075 buttonElement.classList.add('result-control');
4076 buttonElement.dataset.result = String(result);
4077 }
4078
4079 // Add icon if provided
4080 if (button.icon) {
4081 const icon = document.createElement('i');
4082 icon.className = `fa-solid ${button.icon}`;
4083 icon.style.marginRight = '0.5em';
4084 buttonElement.appendChild(icon);
4085 const textSpan = document.createElement('span');
4086 textSpan.textContent = button.text;
4087 buttonElement.appendChild(textSpan);
4088 } else {
4089 buttonElement.innerText = button.text;
4090 }
4091
4092 // Add tooltip if provided
4093 if (button.tooltip) {
4094 buttonElement.title = button.tooltip;
4095 buttonElement.dataset.i18n = '[title]' + button.tooltip;
4096 }
4097
4098 buttonContainer.appendChild(buttonElement);
4099 }
4100
4101 scrollableContainer.appendChild(buttonContainer);
4102
4103 const popupContainer = document.createElement('div');
4104 popupContainer.innerHTML = safeValue;
4105 popupContainer.appendChild(scrollableContainer);
4106
4107 // Ensure the popup uses flex layout
4108 popupContainer.style.display = 'flex';
4109 popupContainer.style.flexDirection = 'column';
4110 popupContainer.style.maxHeight = '80vh'; // Limit the overall height of the popup
4111
4112 popup = new Popup(popupContainer, POPUP_TYPE.TEXT, '', { okButton: multiple ? t`Ok` : t`Cancel`, allowVerticalScrolling: true });
4113 popup.show()
4114 .then((result => resolve(getResult(result))))
4115 .catch(() => resolve(''));
4116
4117 /** @returns {string} @param {string|number|boolean} result */
4118 function getResult(result) {
4119 if (multiple) {
4120 const array = result === POPUP_RESULT.AFFIRMATIVE ? Array.from(multipleToggledState).map(r => resultToButtonMap.get(r)?.text ?? '') : [];
4121 return JSON.stringify(array);
4122 }
4123 return typeof result === 'number' ? resultToButtonMap.get(result)?.text ?? '' : '';
4124 }
4125 });
4126 } catch {
4127 return '';
4128 }
4129}
4130
4131async function popupCallback(args, value) {
4132 const safeBody = DOMPurify.sanitize(value || '');
4133 const safeHeader = args?.header && typeof args?.header === 'string' ? DOMPurify.sanitize(args.header) : null;
4134 const requestedResult = isTrueBoolean(args?.result);
4135
4136 /** @type {import('./popup.js').PopupOptions} */
4137 const popupOptions = {
4138 allowVerticalScrolling: !isFalseBoolean(args?.scroll),
4139 large: isTrueBoolean(args?.large),
4140 wide: isTrueBoolean(args?.wide),
4141 wider: isTrueBoolean(args?.wider),
4142 transparent: isTrueBoolean(args?.transparent),
4143 okButton: args?.okButton !== undefined && typeof args?.okButton === 'string' ? args.okButton : t`OK`,
4144 cancelButton: args?.cancelButton !== undefined && typeof args?.cancelButton === 'string' ? args.cancelButton : null,
4145 tooltip: args?.tooltip !== undefined && typeof args?.tooltip === 'string' ? args.tooltip : null,
4146 };
4147 const result = await Popup.show.text(safeHeader, safeBody, popupOptions);
4148 return String(requestedResult ? result ?? '' : value);
4149}
4150
4151async function getMessagesCallback(args, value) {
4152 const includeNames = !isFalseBoolean(args?.names);
4153 const includeHidden = isTrueBoolean(args?.hidden);
4154 const role = args?.role;
4155 const range = stringToRange(value, 0, chat.length - 1);
4156
4157 if (!range) {
4158 console.warn(`WARN: Invalid range provided for /messages command: ${value}`);
4159 return '';
4160 }
4161
4162 const filterByRole = (mes) => {
4163 if (!role) {
4164 return true;
4165 }
4166
4167 const isNarrator = mes.extra?.type === system_message_types.NARRATOR;
4168
4169 if (role === 'system') {
4170 return isNarrator && !mes.is_user;
4171 }
4172
4173 if (role === 'assistant') {
4174 return !isNarrator && !mes.is_user;
4175 }
4176
4177 if (role === 'user') {
4178 return !isNarrator && mes.is_user;
4179 }
4180
4181 throw new Error(t`Invalid role provided. Expected one of: system, assistant, user. Got: ${role}`);
4182 };
4183
4184 const processMessage = async (mesId) => {
4185 const msg = chat[mesId];
4186 if (!msg) {
4187 console.warn(`WARN: No message found with ID ${mesId}`);
4188 return null;
4189 }
4190
4191 if (role && !filterByRole(msg)) {
4192 console.debug(`/messages: Skipping message with ID ${mesId} due to role filter`);
4193 return null;
4194 }
4195
4196 if (!includeHidden && msg.is_system) {
4197 console.debug(`/messages: Skipping hidden message with ID ${mesId}`);
4198 return null;
4199 }
4200
4201 return includeNames ? `${msg.name}: ${msg.mes}` : msg.mes;
4202 };
4203
4204 const messagePromises = [];
4205
4206 for (let rInd = range.start; rInd <= range.end; ++rInd)
4207 messagePromises.push(processMessage(rInd));
4208
4209 const messages = await Promise.all(messagePromises);
4210
4211 return messages.filter(m => m !== null).join('\n\n');
4212}
4213
4214async function runCallback(args, name) {
4215 if (!name) {
4216 throw new Error(t`No name provided for /run command`);
4217 }
4218
4219 if (name instanceof SlashCommandClosure) {
4220 name.breakController = new SlashCommandBreakController();
4221 return (await name.execute())?.pipe;
4222 }
4223
4224 /**@type {SlashCommandScope} */
4225 const scope = args._scope;
4226 if (scope.existsVariable(name)) {
4227 const closure = scope.getVariable(name);
4228 if (!(closure instanceof SlashCommandClosure)) {
4229 throw new Error(t`"${name}" is not callable.`);
4230 }
4231 closure.scope.parent = scope;
4232 closure.breakController = new SlashCommandBreakController();
4233 if (args._debugController && !closure.debugController) {
4234 closure.debugController = args._debugController;
4235 }
4236 while (closure.providedArgumentList.pop());
4237 closure.argumentList.forEach(arg => {
4238 if (Object.keys(args).includes(arg.name)) {
4239 const providedArg = new SlashCommandNamedArgumentAssignment();
4240 providedArg.name = arg.name;
4241 providedArg.value = args[arg.name];
4242 closure.providedArgumentList.push(providedArg);
4243 }
4244 });
4245 const result = await closure.execute();
4246 return result.pipe;
4247 }
4248
4249 if (typeof globalThis.executeQuickReplyByName !== 'function') {
4250 throw new Error(t`Quick Reply extension is not loaded`);
4251 }
4252
4253 try {
4254 name = name.trim();
4255 /**@type {ExecuteSlashCommandsOptions} */
4256 const options = {
4257 abortController: args._abortController,
4258 debugController: args._debugController,
4259 };
4260 return await globalThis.executeQuickReplyByName(name, args, options);
4261 } catch (error) {
4262 throw new Error(t`Error running Quick Reply "${name}": ${error.message}`);
4263 }
4264}
4265
4266/**
4267 *
4268 * @param {import('./slash-commands/SlashCommand.js').NamedArguments} param0
4269 * @param {string} [reason]
4270 */
4271function abortCallback({ _abortController, quiet }, reason) {
4272 if (quiet instanceof SlashCommandClosure) throw new Error(t`argument 'quiet' cannot be a closure for command /abort`);
4273 _abortController.abort((reason ?? '').toString().length == 0 ? t`/abort command executed` : reason, !isFalseBoolean(quiet?.toString() ?? 'true'));
4274 return '';
4275}
4276
4277async function delayCallback(_, amount) {
4278 if (!amount) {
4279 console.warn('WARN: No amount provided for /delay command');
4280 return '';
4281 }
4282
4283 amount = Number(amount);
4284 if (isNaN(amount)) {
4285 amount = 0;
4286 }
4287
4288 await delay(amount);
4289 return '';
4290}
4291
4292
4293async function inputCallback(args, prompt) {
4294 const safeValue = DOMPurify.sanitize(prompt || '');
4295 const defaultInput = args?.default !== undefined && typeof args?.default === 'string' ? args.default : '';
4296 const popupOptions = {
4297 large: isTrueBoolean(args?.large),
4298 wide: isTrueBoolean(args?.wide),
4299 okButton: args?.okButton !== undefined && typeof args?.okButton === 'string' ? args.okButton : t`Ok`,
4300 rows: args?.rows !== undefined && typeof args?.rows === 'string' ? isNaN(Number(args.rows)) ? 4 : Number(args.rows) : 4,
4301 placeholder: args?.placeholder !== undefined && typeof args?.placeholder === 'string' ? args.placeholder : null,
4302 tooltip: args?.tooltip !== undefined && typeof args?.tooltip === 'string' ? args.tooltip : null,
4303 };
4304 // Do not remove this delay, otherwise the prompt will not show up
4305 await delay(1);
4306 const result = await callGenericPopup(safeValue, POPUP_TYPE.INPUT, defaultInput, popupOptions);
4307 await delay(1);
4308
4309 // Input will return null on nothing entered, and false on cancel clicked
4310 if (result === null || result === false) {
4311 // Veryify if a cancel handler exists and it is valid
4312 if (args?.onCancel) {
4313 if (!(args.onCancel instanceof SlashCommandClosure)) {
4314 throw new Error(t`argument 'onCancel' must be a closure for command /input`);
4315 }
4316 await args.onCancel.execute();
4317 }
4318 } else {
4319 // Verify if an ok handler exists and it is valid
4320 if (args?.onSuccess) {
4321 if (!(args.onSuccess instanceof SlashCommandClosure)) {
4322 throw new Error(t`argument 'onSuccess' must be a closure for command /input`);
4323 }
4324 await args.onSuccess.execute();
4325 }
4326 }
4327
4328 return String(result || '');
4329}
4330
4331/**
4332 * Each item in "args.list" is searched within "search_item" using fuzzy search. If any matches it returns the matched "item".
4333 * @param {FuzzyCommandArgs} args - arguments containing "list" (JSON array) and optionaly "threshold" (float between 0.0 and 1.0)
4334 * @param {string} searchInValue - the string where items of list are searched
4335 * @returns {string} - the matched item from the list
4336 * @typedef {{list: string, threshold: string, mode:string}} FuzzyCommandArgs - arguments for /fuzzy command
4337 * @example /fuzzy list=["down","left","up","right"] "he looks up" | /echo // should return "up"
4338 * @link https://www.fusejs.io/
4339 */
4340function fuzzyCallback(args, searchInValue) {
4341 if (!searchInValue) {
4342 console.warn('WARN: No argument provided for /fuzzy command');
4343 return '';
4344 }
4345
4346 if (!args.list) {
4347 console.warn('WARN: No list argument provided for /fuzzy command');
4348 return '';
4349 }
4350
4351 try {
4352 const list = JSON.parse(resolveVariable(args.list));
4353 if (!Array.isArray(list)) {
4354 console.warn('WARN: Invalid list argument provided for /fuzzy command');
4355 return '';
4356 }
4357
4358 const params = {
4359 includeScore: true,
4360 findAllMatches: true,
4361 ignoreLocation: true,
4362 threshold: 0.4,
4363 };
4364 // threshold determines how strict is the match, low threshold value is very strict, at 1 (nearly?) everything matches
4365 if ('threshold' in args) {
4366 params.threshold = parseFloat(args.threshold);
4367 if (isNaN(params.threshold)) {
4368 console.warn('WARN: \'threshold\' argument must be a float between 0.0 and 1.0 for /fuzzy command');
4369 return '';
4370 }
4371 if (params.threshold < 0) {
4372 params.threshold = 0;
4373 }
4374 if (params.threshold > 1) {
4375 params.threshold = 1;
4376 }
4377 }
4378
4379 function getFirstMatch() {
4380 const fuse = new Fuse([searchInValue], params);
4381 // each item in the "list" is searched within "search_item", if any matches it returns the matched "item"
4382 for (const searchItem of list) {
4383 const result = fuse.search(searchItem);
4384 console.debug('/fuzzy: result', result);
4385 if (result.length > 0) {
4386 console.info('/fuzzy: first matched', searchItem);
4387 return searchItem;
4388 }
4389 }
4390
4391 console.info('/fuzzy: no match');
4392 return '';
4393 }
4394
4395 function getBestMatch() {
4396 const fuse = new Fuse(list, params);
4397 const result = fuse.search(searchInValue);
4398 console.debug('/fuzzy: result', result);
4399 if (result.length > 0) {
4400 console.info('/fuzzy: best matched', result[0].item);
4401 return result[0].item;
4402 }
4403
4404 console.info('/fuzzy: no match');
4405 return '';
4406 }
4407
4408 switch (String(args.mode).trim().toLowerCase()) {
4409 case 'best':
4410 return getBestMatch();
4411 case 'first':
4412 default:
4413 return getFirstMatch();
4414 }
4415 } catch {
4416 console.warn('WARN: Invalid list argument provided for /fuzzy command');
4417 return '';
4418 }
4419}
4420
4421function setEphemeralStopStrings(value) {
4422 if (typeof value === 'string' && value.length) {
4423 try {
4424 const stopStrings = JSON.parse(value);
4425 if (Array.isArray(stopStrings)) {
4426 stopStrings.forEach(stopString => addEphemeralStoppingString(stopString));
4427 }
4428 } catch {
4429 // Do nothing
4430 }
4431 }
4432}
4433
4434async function generateRawCallback(args, value) {
4435 if (!value) {
4436 console.warn('WARN: No argument provided for /genraw command');
4437 return '';
4438 }
4439
4440 // Prevent generate recursion
4441 $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
4442 const lock = isTrueBoolean(args?.lock);
4443 const as = args?.as || 'system';
4444 const quietToLoud = as === 'char';
4445 const systemPrompt = resolveVariable(args?.system) || '';
4446 const prefillPrompt = resolveVariable(args?.prefill) || '';
4447 const length = Number(resolveVariable(args?.length) ?? 0) || 0;
4448 const trimNames = !isFalseBoolean(args?.trim);
4449
4450 try {
4451 if (lock) {
4452 deactivateSendButtons();
4453 }
4454
4455 setEphemeralStopStrings(resolveVariable(args?.stop));
4456 /** @type {import('../script.js').GenerateRawParams} */
4457 const params = {
4458 prompt: value,
4459 instructOverride: isFalseBoolean(args?.instruct),
4460 quietToLoud: quietToLoud,
4461 systemPrompt: systemPrompt,
4462 responseLength: length,
4463 trimNames: trimNames,
4464 prefill: prefillPrompt,
4465 };
4466 const result = await generateRaw(params);
4467 return result;
4468 } catch (err) {
4469 console.error('Error on /genraw generation', err);
4470 toastr.error(err.message, t`API Error`, { preventDuplicates: true });
4471 } finally {
4472 if (lock) {
4473 activateSendButtons();
4474 }
4475 flushEphemeralStoppingStrings();
4476 }
4477 return '';
4478}
4479
4480/**
4481 * Callback for the /gen command
4482 * @param {object} args Named arguments
4483 * @param {string} value Unnamed argument
4484 * @returns {Promise<string>} The generated text
4485 */
4486async function generateCallback(args, value) {
4487 // Prevent generate recursion
4488 $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
4489 const lock = isTrueBoolean(args?.lock);
4490 const trim = isTrueBoolean(args?.trim?.toString());
4491 const as = args?.as || 'system';
4492 const quietToLoud = as === 'char';
4493 const length = Number(resolveVariable(args?.length) ?? 0) || 0;
4494
4495 try {
4496 if (lock) {
4497 deactivateSendButtons();
4498 }
4499
4500 setEphemeralStopStrings(resolveVariable(args?.stop));
4501 const name = args?.name;
4502 const char = name ? findChar({ name: name }) : null;
4503 /** @type {import('../script.js').GenerateQuietPromptParams} */
4504 const params = {
4505 quietPrompt: value,
4506 quietToLoud: quietToLoud,
4507 quietName: char?.name ?? name,
4508 responseLength: length,
4509 trimToSentence: trim,
4510 forceChId: char ? characters.indexOf(char) : null,
4511 };
4512 const result = await generateQuietPrompt(params);
4513 return result;
4514 } catch (err) {
4515 console.error('Error on /gen generation', err);
4516 toastr.error(err.message, t`API Error`, { preventDuplicates: true });
4517 } finally {
4518 if (lock) {
4519 activateSendButtons();
4520 }
4521 flushEphemeralStoppingStrings();
4522 }
4523 return '';
4524}
4525
4526/**
4527 *
4528 * @param {{title?: string, severity?: string, timeout?: string, extendedTimeout?: string, preventDuplicates?: string, awaitDismissal?: string, cssClass?: string, color?: string, escapeHtml?: string, onClick?: SlashCommandClosure}} args - named arguments from the slash command
4529 * @param {string} value - The string to echo (unnamed argument from the slash command)
4530 * @returns {Promise<string>} The text that was echoed
4531 */
4532async function echoCallback(args, value) {
4533 // Note: We don't need to sanitize input, as toastr is set up by default to escape HTML via toastr options
4534 if (value === '') {
4535 console.warn('WARN: No argument provided for /echo command');
4536 return '';
4537 }
4538
4539 if (args.severity && !['error', 'warning', 'success', 'info'].includes(args.severity)) {
4540 toastr.warning(t`Invalid severity provided for /echo command: ${args.severity}`);
4541 args.severity = null;
4542 }
4543
4544 // Make sure that the value is a string
4545 value = String(value);
4546
4547 let title = args.title ? args.title : undefined;
4548 const severity = args.severity ? args.severity : 'info';
4549
4550 /** @type {ToastrOptions} */
4551 const options = {};
4552 if (args.timeout && !isNaN(parseInt(args.timeout))) options.timeOut = parseInt(args.timeout);
4553 if (args.extendedTimeout && !isNaN(parseInt(args.extendedTimeout))) options.extendedTimeOut = parseInt(args.extendedTimeout);
4554 if (isTrueBoolean(args.preventDuplicates)) options.preventDuplicates = true;
4555 if (args.cssClass) options.toastClass = [options.toastClass, args.cssClass].filter(Boolean).join(' ');
4556 options.escapeHtml = args.escapeHtml !== undefined ? isTrueBoolean(args.escapeHtml) : true;
4557
4558 // Prepare possible await handling
4559 let awaitDismissal = isTrueBoolean(args.awaitDismissal);
4560 let resolveToastDismissal;
4561
4562 if (awaitDismissal) {
4563 options.onHidden = () => resolveToastDismissal(value);
4564 }
4565 if (args.onClick) {
4566 if (args.onClick instanceof SlashCommandClosure) {
4567 options.onclick = async () => {
4568 // Execute the slash command directly, with its internal scope and everything. Clear progress handler so it doesn't interfere with command execution progress.
4569 args.onClick.onProgress = null;
4570 await args.onClick.execute();
4571 };
4572 } else {
4573 toastr.warning(t`Invalid onClick provided for /echo command. This is not a closure`);
4574 }
4575 }
4576
4577 // If we allow HTML, we need to sanitize it to prevent security risks
4578 if (!options.escapeHtml) {
4579 if (title) title = DOMPurify.sanitize(title, { FORBID_TAGS: ['style'] });
4580 value = DOMPurify.sanitize(value, { FORBID_TAGS: ['style'] });
4581 }
4582
4583 let toast;
4584 switch (severity) {
4585 case 'error':
4586 toast = toastr.error(value, title, options);
4587 break;
4588 case 'warning':
4589 toast = toastr.warning(value, title, options);
4590 break;
4591 case 'success':
4592 toast = toastr.success(value, title, options);
4593 break;
4594 case 'info':
4595 default:
4596 toast = toastr.info(value, title, options);
4597 break;
4598 }
4599
4600 if (args.color) {
4601 toast.css('background-color', args.color);
4602 }
4603
4604 if (awaitDismissal) {
4605 return new Promise((resolve) => {
4606 resolveToastDismissal = resolve;
4607 });
4608 } else {
4609 return value;
4610 }
4611}
4612
4613/**
4614 * @param {{switch?: string}} args - named arguments
4615 * @param {string} value - The swipe text to add (unnamed argument)
4616 */
4617async function addSwipeCallback(args, value) {
4618 const lastMessage = chat[chat.length - 1];
4619
4620 if (!lastMessage) {
4621 toastr.warning(t`No messages to add swipes to.`);
4622 return '';
4623 }
4624
4625 if (!value) {
4626 console.warn('WARN: No argument provided for /addswipe command');
4627 return '';
4628 }
4629
4630 if (lastMessage.is_user) {
4631 toastr.warning(t`Can't add swipes to user messages.`);
4632 return '';
4633 }
4634
4635 if (lastMessage.is_system) {
4636 toastr.warning(t`Can't add swipes to system messages.`);
4637 return '';
4638 }
4639
4640 if (!Array.isArray(lastMessage.swipes)) {
4641 lastMessage.swipes = [lastMessage.mes];
4642 lastMessage.swipe_info = [{}];
4643 lastMessage.swipe_id = 0;
4644 }
4645 if (!Array.isArray(lastMessage.swipe_info)) {
4646 lastMessage.swipe_info = lastMessage.swipes.map(() => ({}));
4647 }
4648
4649 lastMessage.swipes.push(value);
4650 lastMessage.swipe_info.push({
4651 send_date: getMessageTimeStamp(),
4652 gen_started: null,
4653 gen_finished: null,
4654 extra: {
4655 bias: extractMessageBias(value),
4656 gen_id: Date.now(),
4657 api: 'manual',
4658 model: 'slash command',
4659 },
4660 });
4661
4662 const newSwipeId = lastMessage.swipes.length - 1;
4663
4664 if (isTrueBoolean(args.switch)) {
4665 // Make sure ad-hoc changes to extras are saved before swiping away
4666 syncMesToSwipe();
4667 lastMessage.swipe_id = newSwipeId;
4668 lastMessage.mes = lastMessage.swipes[newSwipeId];
4669 lastMessage.extra = structuredClone(lastMessage.swipe_info?.[newSwipeId]?.extra ?? lastMessage.extra ?? {});
4670 }
4671
4672 await saveChatConditional();
4673 await reloadCurrentChat();
4674
4675 return String(newSwipeId);
4676}
4677
4678async function deleteSwipeCallback(_, arg) {
4679 // Take the provided argument. Null if none provided, which will target the current swipe.
4680 const swipeId = arg && !isNaN(Number(arg)) ? (Number(arg) - 1) : null;
4681
4682 const newSwipeId = await deleteSwipe(swipeId);
4683
4684 return String(newSwipeId);
4685}
4686
4687async function askCharacter(args, text) {
4688 // Prevent generate recursion
4689 $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
4690
4691 // Not supported in group chats
4692 // TODO: Maybe support group chats?
4693 if (selected_group) {
4694 toastr.warning(t`Cannot run /ask command in a group chat!`);
4695 return '';
4696 }
4697
4698 if (!args.name) {
4699 toastr.warning(t`You must specify a name of the character to ask.`);
4700 return '';
4701 }
4702
4703 const prevChId = this_chid;
4704
4705 // Find the character
4706 const character = findChar({ name: args?.name });
4707 if (!character) {
4708 toastr.error(t`Character not found.`);
4709 return '';
4710 }
4711
4712 const chId = getCharIndex(character);
4713
4714 if (text) {
4715 const mesText = getRegexedString(text.trim(), regex_placement.SLASH_COMMAND);
4716 // Sending a message implicitly saves the chat, so this needs to be done before changing the character
4717 // Otherwise, a corruption will occur
4718 await sendMessageAsUser(mesText, '');
4719 }
4720
4721 // Override character and send a user message
4722 setCharacterId(String(chId));
4723
4724 const { name, force_avatar, original_avatar } = getNameAndAvatarForMessage(character, args?.name);
4725
4726 setCharacterName(name);
4727
4728 const restoreCharacter = () => {
4729 if (String(this_chid) !== String(chId)) {
4730 return;
4731 }
4732
4733 if (prevChId !== undefined) {
4734 setCharacterId(prevChId);
4735 setCharacterName(characters[prevChId].name);
4736 } else {
4737 setCharacterId(undefined);
4738 setCharacterName(neutralCharacterName);
4739 }
4740
4741 // Only force the new avatar if the character name is the same
4742 // This skips if an error was fired
4743 const lastMessage = chat[chat.length - 1];
4744 if (lastMessage && lastMessage?.name === name) {
4745 lastMessage.force_avatar = force_avatar;
4746 lastMessage.original_avatar = original_avatar;
4747 }
4748 };
4749
4750 let askResult = '';
4751
4752 // Run generate and restore previous character
4753 try {
4754 eventSource.once(event_types.MESSAGE_RECEIVED, restoreCharacter);
4755 toastr.info(t`Asking ${name} something...`);
4756 askResult = await Generate('normal');
4757 } catch (error) {
4758 restoreCharacter();
4759 console.error('Error running /ask command', error);
4760 } finally {
4761 if (String(this_chid) === String(prevChId)) {
4762 await saveChatConditional();
4763 } else {
4764 toastr.error(t`It is strongly recommended to reload the page.`, t`Something went wrong`);
4765 }
4766 }
4767
4768 const message = askResult ? chat[chat.length - 1] : null;
4769
4770 return await slashCommandReturnHelper.doReturn(args.return ?? 'pipe', message, { objectToStringFunc: x => x.mes });
4771}
4772
4773async function hideMessageCallback(args, value) {
4774 const range = value ? stringToRange(value, 0, chat.length - 1) : { start: chat.length - 1, end: chat.length - 1 };
4775
4776 if (!range) {
4777 console.warn(`WARN: Invalid range provided for /hide command: ${value}`);
4778 return '';
4779 }
4780
4781 const nameFilter = String(args.name ?? '').trim();
4782 await hideChatMessageRange(range.start, range.end, false, nameFilter);
4783 return '';
4784}
4785
4786async function unhideMessageCallback(args, value) {
4787 const range = value ? stringToRange(value, 0, chat.length - 1) : { start: chat.length - 1, end: chat.length - 1 };
4788
4789 if (!range) {
4790 console.warn(`WARN: Invalid range provided for /unhide command: ${value}`);
4791 return '';
4792 }
4793
4794 const nameFilter = String(args.name ?? '').trim();
4795 await hideChatMessageRange(range.start, range.end, true, nameFilter);
4796 return '';
4797}
4798
4799/**
4800 * Copium for running group actions when the member is offscreen.
4801 * @param {number} chid - character ID
4802 * @param {string} action - one of 'enable', 'disable', 'up', 'down', 'view', 'remove'
4803 * @returns {void}
4804 */
4805function performGroupMemberAction(chid, action) {
4806 const memberSelector = `.group_member[data-chid="${chid}"]`;
4807 // Do not optimize. Paginator gets recreated on every action
4808 const paginationSelector = '#rm_group_members_pagination';
4809 const pageSizeSelector = '#rm_group_members_pagination select';
4810 let wasOffscreen = false;
4811 let paginationValue = null;
4812 let pageValue = null;
4813
4814 if ($(memberSelector).length === 0) {
4815 wasOffscreen = true;
4816 paginationValue = Number($(pageSizeSelector).val());
4817 pageValue = $(paginationSelector).pagination('getCurrentPageNum');
4818 $(pageSizeSelector).val($(pageSizeSelector).find('option').last().val()).trigger('change');
4819 }
4820
4821 $(memberSelector).find(`[data-action="${action}"]`).trigger('click');
4822
4823 if (wasOffscreen) {
4824 $(pageSizeSelector).val(paginationValue).trigger('change');
4825 if ($(paginationSelector).length) {
4826 $(paginationSelector).pagination('go', pageValue);
4827 }
4828 }
4829}
4830
4831async function disableGroupMemberCallback(_, arg) {
4832 if (!selected_group) {
4833 toastr.warning(t`Cannot run /member-disable command outside of a group chat.`);
4834 return '';
4835 }
4836
4837 const chid = findGroupMemberId(arg);
4838
4839 if (chid === undefined) {
4840 console.warn(`WARN: No group member found for argument ${arg}`);
4841 return '';
4842 }
4843
4844 performGroupMemberAction(chid, 'disable');
4845 return '';
4846}
4847
4848async function enableGroupMemberCallback(_, arg) {
4849 if (!selected_group) {
4850 toastr.warning(t`Cannot run /member-enable command outside of a group chat.`);
4851 return '';
4852 }
4853
4854 const chid = findGroupMemberId(arg);
4855
4856 if (chid === undefined) {
4857 console.warn(`WARN: No group member found for argument ${arg}`);
4858 return '';
4859 }
4860
4861 performGroupMemberAction(chid, 'enable');
4862 return '';
4863}
4864
4865async function moveGroupMemberUpCallback(_, arg) {
4866 if (!selected_group) {
4867 toastr.warning(t`Cannot run /member-up command outside of a group chat.`);
4868 return '';
4869 }
4870
4871 const chid = findGroupMemberId(arg);
4872
4873 if (chid === undefined) {
4874 console.warn(`WARN: No group member found for argument ${arg}`);
4875 return '';
4876 }
4877
4878 performGroupMemberAction(chid, 'up');
4879 return '';
4880}
4881
4882async function moveGroupMemberDownCallback(_, arg) {
4883 if (!selected_group) {
4884 toastr.warning(t`Cannot run /member-down command outside of a group chat.`);
4885 return '';
4886 }
4887
4888 const chid = findGroupMemberId(arg);
4889
4890 if (chid === undefined) {
4891 console.warn(`WARN: No group member found for argument ${arg}`);
4892 return '';
4893 }
4894
4895 performGroupMemberAction(chid, 'down');
4896 return '';
4897}
4898
4899async function peekCallback(_, arg) {
4900 if (!selected_group) {
4901 toastr.warning(t`Cannot run /member-peek command outside of a group chat.`);
4902 return '';
4903 }
4904
4905 if (is_group_generating) {
4906 toastr.warning(t`Cannot run /member-peek command while the group reply is generating.`);
4907 return '';
4908 }
4909
4910 const chid = findGroupMemberId(arg);
4911
4912 if (chid === undefined) {
4913 console.warn(`WARN: No group member found for argument ${arg}`);
4914 return '';
4915 }
4916
4917 performGroupMemberAction(chid, 'view');
4918 return '';
4919}
4920
4921async function countGroupMemberCallback() {
4922 if (!selected_group) {
4923 toastr.warning(t`Cannot run /member-count command outside of a group chat.`);
4924 return '';
4925 }
4926
4927 return String(getGroupMembers(selected_group).length);
4928}
4929
4930async function removeGroupMemberCallback(_, arg) {
4931 if (!selected_group) {
4932 toastr.warning(t`Cannot run /member-remove command outside of a group chat.`);
4933 return '';
4934 }
4935
4936 const chid = findGroupMemberId(arg);
4937
4938 if (chid === undefined) {
4939 console.warn(`WARN: No group member found for argument ${arg}`);
4940 return '';
4941 }
4942
4943 performGroupMemberAction(chid, 'remove');
4944 return '';
4945}
4946
4947async function addGroupMemberCallback(_, name) {
4948 if (!selected_group) {
4949 toastr.warning(t`Cannot run /memberadd command outside of a group chat.`);
4950 return '';
4951 }
4952
4953 if (!name) {
4954 console.warn('WARN: No argument provided for /memberadd command');
4955 return '';
4956 }
4957
4958 const character = findChar({ name: name, preferCurrentChar: false });
4959 if (!character) {
4960 console.warn(`WARN: No character found for argument ${name}`);
4961 return '';
4962 }
4963
4964 const group = groups.find(x => x.id === selected_group);
4965
4966 if (!group || !Array.isArray(group.members)) {
4967 console.warn(`WARN: No group found for ID ${selected_group}`);
4968 return '';
4969 }
4970
4971 const avatar = character.avatar;
4972
4973 if (group.members.includes(avatar)) {
4974 toastr.warning(t`${character.name} is already a member of this group.`);
4975 return '';
4976 }
4977
4978 group.members.push(avatar);
4979 await saveGroupChat(selected_group, true);
4980
4981 // Trigger to reload group UI
4982 $('#rm_button_selected_ch').trigger('click');
4983 return character.name;
4984}
4985
4986async function triggerGenerationCallback(args, value) {
4987 const shouldAwait = isTrueBoolean(args?.await);
4988 const outerPromise = new Promise((outerResolve) => setTimeout(async () => {
4989 try {
4990 await waitUntilCondition(() => !is_send_press && !is_group_generating, 10000, 100);
4991 } catch {
4992 console.warn('Timeout waiting for generation unlock');
4993 toastr.warning(t`Cannot run /trigger command while the reply is being generated.`);
4994 outerResolve(Promise.resolve(''));
4995 return '';
4996 }
4997
4998 // Prevent generate recursion
4999 $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
5000
5001 let chid = undefined;
5002
5003 if (selected_group && value) {
5004 chid = findGroupMemberId(value);
5005
5006 if (chid === undefined) {
5007 console.warn(`WARN: No group member found for argument ${value}`);
5008 }
5009 }
5010
5011 outerResolve(new Promise(innerResolve => setTimeout(() => innerResolve(Generate('normal', { force_chid: chid })), 100)));
5012 }, 1));
5013
5014 if (shouldAwait) {
5015 const innerPromise = await outerPromise;
5016 await innerPromise;
5017 }
5018
5019 return '';
5020}
5021
5022async function sendUserMessageCallback(args, text) {
5023 text = String(text ?? '').trim();
5024 const compact = isTrueBoolean(args?.compact);
5025 const bias = extractMessageBias(text);
5026
5027 let insertAt = Number(args?.at);
5028
5029 // Convert possible depth parameter to index
5030 if (!isNaN(insertAt) && (insertAt < 0 || Object.is(insertAt, -0))) {
5031 // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)
5032 insertAt = chat.length + insertAt;
5033 }
5034
5035 let message;
5036 if ('name' in args) {
5037 const name = args.name || '';
5038 const avatar = findPersona({ name })?.avatar || user_avatar;
5039 message = await sendMessageAsUser(text, bias, insertAt, compact, name, avatar);
5040 } else {
5041 message = await sendMessageAsUser(text, bias, insertAt, compact);
5042 }
5043
5044 return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
5045}
5046
5047async function deleteMessagesByNameCallback(_, name) {
5048 if (!name) {
5049 console.warn('WARN: No name provided for /delname command');
5050 return;
5051 }
5052
5053 // Search for a matching character to get the real name, or take the name provided
5054 const character = findChar({ name: name });
5055 name = character?.name || name;
5056
5057 const messagesToDelete = [];
5058 chat.forEach((value) => {
5059 if (value.name === name) {
5060 messagesToDelete.push(value);
5061 }
5062 });
5063
5064 if (!messagesToDelete.length) {
5065 console.debug('/delname: Nothing to delete');
5066 return;
5067 }
5068
5069 for (const message of messagesToDelete) {
5070 const index = chat.indexOf(message);
5071 if (index !== -1) {
5072 console.debug(`/delname: Deleting message #${index}`, message);
5073 chat.splice(index, 1);
5074 }
5075 }
5076
5077 await saveChatConditional();
5078 await reloadCurrentChat();
5079
5080 toastr.info(t`Deleted ${messagesToDelete.length} messages from ${name}`);
5081 return '';
5082}
5083
5084async function goToCharacterCallback(_, name) {
5085 if (!name) {
5086 console.warn('WARN: No character name provided for /go command');
5087 return;
5088 }
5089
5090 const character = findChar({ name: name });
5091 if (character) {
5092 const chid = getCharIndex(character);
5093 await openChat(String(chid));
5094 setActiveCharacter(character.avatar);
5095 setActiveGroup(null);
5096 return character.name;
5097 }
5098 const group = groups.find(it => equalsIgnoreCaseAndAccents(it.name, name));
5099 if (group) {
5100 await openGroupById(group.id);
5101 setActiveCharacter(null);
5102 setActiveGroup(group.id);
5103 return group.name;
5104 }
5105 console.warn(`No matches found for name "${name}"`);
5106 return '';
5107}
5108
5109async function openChat(chid) {
5110 resetSelectedGroup();
5111 setCharacterId(chid);
5112 await delay(1);
5113 await reloadCurrentChat();
5114}
5115
5116/**
5117 * Uploads an avatar image to a character.
5118 * @param {string} avatarKey - The character's avatar filename (e.g., "name.png")
5119 * @param {string} base64Data - Base64 data URL of the image
5120 * @param {object} [options={}] - Options
5121 * @param {boolean} [options.resizePrompt=false] - Whether to show the resize/crop prompt
5122 * @returns {Promise<boolean>} True if upload was successful, false if cancelled or failed
5123 */
5124async function uploadCharacterAvatar(avatarKey, base64Data, { resizePrompt = false } = {}) {
5125 if (!base64Data || !avatarKey) {
5126 return false;
5127 }
5128
5129 let finalImageData = base64Data;
5130
5131 // Handle resize prompt
5132 if (resizePrompt) {
5133 if (power_user.never_resize_avatars) {
5134 toastr.warning(t`Avatar resizing is disabled in settings. The image will be uploaded as-is.`);
5135 } else {
5136 const dlg = new Popup(t`Set the crop position of the avatar image`, POPUP_TYPE.CROP, '', { cropImage: base64Data });
5137 const croppedImage = await dlg.show();
5138 if (!croppedImage) {
5139 // User cancelled the crop dialog
5140 return false;
5141 }
5142 // The dialog returns the already-cropped image
5143 finalImageData = String(croppedImage);
5144 }
5145 }
5146
5147 try {
5148 // Convert base64 to blob
5149 const response = await fetch(finalImageData);
5150 const blob = await response.blob();
5151
5152 // Create form data for upload
5153 const formData = new FormData();
5154 formData.append('avatar', blob, 'avatar.png');
5155 formData.append('avatar_url', avatarKey);
5156
5157 const uploadResponse = await fetch('/api/characters/edit-avatar', {
5158 method: 'POST',
5159 headers: getRequestHeaders({ omitContentType: true }),
5160 body: formData,
5161 });
5162
5163 if (!uploadResponse.ok) {
5164 const errorText = await uploadResponse.text();
5165 throw new Error(errorText); // Will be caught and logged below
5166 }
5167
5168 // Bust cache for the avatar thumbnail and character image
5169 const thumbnailUrl = getThumbnailUrl('avatar', avatarKey);
5170 await fetch(thumbnailUrl, { method: 'GET', cache: 'reload' });
5171 await fetch(`/characters/${avatarKey}`, { method: 'GET', cache: 'reload' });
5172
5173 // Refresh all visible avatar images that use this thumbnail URL
5174 // This handles messages, character list, and any other place using the thumbnail
5175 const avatarImages = document.querySelectorAll(`img[src^="${thumbnailUrl}"]`);
5176 for (const img of avatarImages) {
5177 if (img instanceof HTMLImageElement) {
5178 const originalSrc = img.src;
5179 img.src = '';
5180 img.src = originalSrc;
5181 }
5182 }
5183 console.debug(`Refreshed ${avatarImages.length} avatar images for ${avatarKey}`);
5184
5185 return true;
5186 } catch (error) {
5187 console.error('Error uploading character avatar:', error);
5188 toastr.warning(t`Failed to upload avatar: ${error.message}`);
5189 return false;
5190 }
5191}
5192
5193/**
5194 * Creates a new character via the API.
5195 * @param {object} args Named arguments
5196 * @returns {Promise<string>} The avatar key of the created character
5197 */
5198async function createCharacterCallback(args) {
5199 const name = args.name;
5200 const description = args.description;
5201 const firstMessage = args.firstMessage;
5202
5203 if (!name || typeof name !== 'string' || !name.trim()) {
5204 toastr.warning(t`Character name is required`);
5205 return '';
5206 }
5207
5208 // Build the character data object matching the server's expected format
5209 const characterData = {
5210 ch_name: name.trim(),
5211 description: description,
5212 first_mes: firstMessage,
5213 personality: args.personality ?? '',
5214 scenario: args.scenario ?? '',
5215 mes_example: args.messageExamples ?? '',
5216 creator_notes: args.creatorNotes ?? '',
5217 system_prompt: args.systemPrompt ?? '',
5218 post_history_instructions: args.postHistoryInstructions ?? '',
5219 creator: args.creator ?? '',
5220 character_version: args.characterVersion ?? '',
5221 tags: args.tags ? args.tags.split(',').map(t => t.trim()).filter(t => t) : [],
5222 talkativeness: args.talkativeness ?? '0.5',
5223 world: args.world ?? '',
5224 depth_prompt_prompt: args.depthPrompt ?? '',
5225 depth_prompt_depth: args.depthPromptDepth ?? '4',
5226 depth_prompt_role: args.depthPromptRole ?? 'system',
5227 fav: isTrueBoolean(args.favorite) ? 'true' : 'false',
5228 alternate_greetings: [],
5229 extensions: '{}',
5230 };
5231
5232 // Handle avatar if provided (URL or base64)
5233 const avatarData = args.avatar ? await resolveAvatarData(args.avatar) : null;
5234
5235 try {
5236 const response = await fetch('/api/characters/create', {
5237 method: 'POST',
5238 headers: getRequestHeaders(),
5239 body: JSON.stringify(characterData),
5240 });
5241
5242 if (!response.ok) {
5243 const errorText = await response.text();
5244 throw new Error(errorText); // Will be caught and logged below
5245 }
5246
5247 const avatarKey = await response.text();
5248
5249 // Upload avatar if provided
5250 if (avatarData) {
5251 const resizePrompt = !isFalseBoolean(args.avatarPromptResize);
5252 const uploaded = await uploadCharacterAvatar(avatarKey, avatarData, { resizePrompt });
5253 if (!uploaded && resizePrompt) {
5254 // User cancelled the resize dialog, but character was still created
5255 toastr.info(t`Character created without avatar (resize cancelled)`);
5256 }
5257 }
5258
5259 // Refresh the character list
5260 await getCharacters();
5261
5262 // Select the character if requested (default: true)
5263 const shouldSelect = !isFalseBoolean(args.select);
5264 if (shouldSelect) {
5265 const characterIndex = characters.findIndex(c => c.avatar === avatarKey);
5266 if (characterIndex !== -1) {
5267 // selectCharacterById handles group reset and active character setting
5268 await selectCharacterById(characterIndex);
5269 }
5270 }
5271
5272 toastr.success(t`Character "${name}" created successfully`);
5273 return avatarKey;
5274 } catch (error) {
5275 console.error('Error creating character:', error);
5276 toastr.error(t`Failed to create character: ${error.message}`);
5277 return '';
5278 }
5279}
5280
5281/**
5282 * Updates an existing character via the merge-attributes API.
5283 * @param {object} args Named arguments
5284 * @returns {Promise<string>} The avatar key of the updated character
5285 */
5286async function updateCharacterCallback(args) {
5287 // Find the target character
5288 let character;
5289 let characterIndex;
5290 if (args.char) {
5291 character = findChar({ name: args.char });
5292 if (!character) {
5293 toastr.warning(t`Character "${args.char}" not found`);
5294 return '';
5295 }
5296 characterIndex = String(characters.indexOf(character));
5297 } else {
5298 // Use currently selected character
5299 if (this_chid === undefined || !characters[this_chid]) {
5300 toastr.warning(t`No character selected and no char argument provided`);
5301 return '';
5302 }
5303 character = characters[this_chid];
5304 characterIndex = this_chid;
5305 }
5306
5307 // Build the update object with only provided fields
5308 const updateData = {
5309 avatar: character.avatar,
5310 };
5311
5312 // Map argument names to character data field names
5313 const fieldMappings = {
5314 name: 'name',
5315 description: 'description',
5316 firstMessage: 'first_mes',
5317 personality: 'personality',
5318 scenario: 'scenario',
5319 messageExamples: 'mes_example',
5320 creatorNotes: 'creator_notes',
5321 systemPrompt: 'system_prompt',
5322 postHistoryInstructions: 'post_history_instructions',
5323 creator: 'creator',
5324 characterVersion: 'character_version',
5325 tags: 'tags',
5326 };
5327
5328 // Add provided fields to update data
5329 let hasUpdates = false;
5330 for (const [argName, fieldName] of Object.entries(fieldMappings)) {
5331 if (args[argName] !== undefined) {
5332 let value = args[argName];
5333 // Handle tags as comma-separated array
5334 if (fieldName === 'tags' && typeof value === 'string') {
5335 value = value.split(',').map(t => t.trim()).filter(t => t);
5336 }
5337 updateData[fieldName] = value;
5338 // Also set in data object for V2 spec compliance
5339 if (!updateData.data) updateData.data = {};
5340 updateData.data[fieldName] = value;
5341 hasUpdates = true;
5342 }
5343 }
5344
5345 // Special handling for world / lorebook: store under data.extensions.world
5346 if (args.world !== undefined) {
5347 const value = args.world;
5348 if (!updateData.data) {
5349 updateData.data = {};
5350 }
5351 if (!updateData.data.extensions) {
5352 updateData.data.extensions = {};
5353 }
5354 updateData.data.extensions.world = value;
5355 hasUpdates = true;
5356 }
5357
5358 // Handle talkativeness (stored in extensions)
5359 if (args.talkativeness !== undefined) {
5360 const talkValue = parseFloat(args.talkativeness);
5361 if (!isNaN(talkValue)) {
5362 updateData.talkativeness = talkValue;
5363 if (!updateData.data) updateData.data = {};
5364 if (!updateData.data.extensions) updateData.data.extensions = {};
5365 updateData.data.extensions.talkativeness = talkValue;
5366 hasUpdates = true;
5367 }
5368 }
5369
5370 // Handle favorite
5371 if (args.favorite !== undefined) {
5372 const favValue = isTrueBoolean(args.favorite);
5373 updateData.fav = favValue;
5374 if (!updateData.data) updateData.data = {};
5375 if (!updateData.data.extensions) updateData.data.extensions = {};
5376 updateData.data.extensions.fav = favValue;
5377 hasUpdates = true;
5378 }
5379
5380 // Handle avatar (resolve URL/base64, upload separately after merge)
5381 const avatarData = args.avatar ? await resolveAvatarData(args.avatar) : null;
5382 if (avatarData) {
5383 hasUpdates = true;
5384 }
5385
5386 // Handle depth prompt fields
5387 if (args.depthPrompt !== undefined || args.depthPromptDepth !== undefined || args.depthPromptRole !== undefined) {
5388 if (!updateData.data) updateData.data = {};
5389 if (!updateData.data.extensions) updateData.data.extensions = {};
5390 if (!updateData.data.extensions.depth_prompt) updateData.data.extensions.depth_prompt = {};
5391
5392 if (args.depthPrompt !== undefined) {
5393 updateData.data.extensions.depth_prompt.prompt = args.depthPrompt;
5394 hasUpdates = true;
5395 }
5396 if (args.depthPromptDepth !== undefined) {
5397 updateData.data.extensions.depth_prompt.depth = parseInt(args.depthPromptDepth);
5398 hasUpdates = true;
5399 }
5400 if (args.depthPromptRole !== undefined) {
5401 updateData.data.extensions.depth_prompt.role = args.depthPromptRole;
5402 hasUpdates = true;
5403 }
5404 }
5405
5406 if (!hasUpdates) {
5407 toastr.warning(t`No fields provided to update`);
5408 return character.avatar;
5409 }
5410
5411 try {
5412 const response = await fetch('/api/characters/merge-attributes', {
5413 method: 'POST',
5414 headers: getRequestHeaders(),
5415 body: JSON.stringify(updateData),
5416 });
5417
5418 if (!response.ok) {
5419 const errorData = await response.json().catch(() => ({}));
5420 throw new Error(errorData.message || `Server returned ${response.status}`); // Will be caught and logged below
5421 }
5422
5423 // Upload avatar if provided
5424 if (avatarData) {
5425 const resizePrompt = !isFalseBoolean(args.avatarPromptResize);
5426 const uploaded = await uploadCharacterAvatar(character.avatar, avatarData, { resizePrompt });
5427 if (!uploaded && resizePrompt) {
5428 // User cancelled the resize dialog
5429 toastr.warning(t`Avatar update cancelled`);
5430 }
5431 }
5432
5433 // Refresh the character data
5434 await getOneCharacter(character.avatar);
5435
5436 await eventSource.emit(event_types.CHARACTER_EDITED, { detail: { id: characterIndex, character: characters[characterIndex] } });
5437
5438 // Update the side panel if this is the currently selected character
5439 if (characterIndex === this_chid) {
5440 select_selected_character(this_chid, { switchMenu: false });
5441 }
5442
5443 toastr.success(t`Character "${character.name}" updated successfully`);
5444 return character.avatar;
5445 } catch (error) {
5446 console.error('Error updating character:', error);
5447 toastr.error(t`Failed to update character: ${error.message}`);
5448 return '';
5449 }
5450}
5451
5452/**
5453 * Duplicates a character via the slash command.
5454 * @param {object} args Named arguments
5455 * @returns {Promise<string>} The avatar key of the duplicated character
5456 */
5457async function duplicateCharacterCallback(args) {
5458 // Find the target character if specified
5459 let targetAvatar = null;
5460 if (args.char) {
5461 const character = findChar({ name: args.char });
5462 if (!character) {
5463 toastr.warning(t`Character "${args.char}" not found`);
5464 return '';
5465 }
5466 targetAvatar = character.avatar;
5467 }
5468
5469 // Call the duplicateCharacter utility with silent mode (no popup)
5470 const newAvatarKey = await duplicateCharacter({ avatar: targetAvatar, silent: true });
5471 if (!newAvatarKey) {
5472 toastr.error(t`Failed to duplicate character`);
5473 return '';
5474 }
5475
5476 // Select the character if requested (default: false)
5477 const shouldSelect = isTrueBoolean(args.select);
5478 if (shouldSelect) {
5479 const characterIndex = characters.findIndex(c => c.avatar === newAvatarKey);
5480 if (characterIndex !== -1) {
5481 await selectCharacterById(characterIndex);
5482 }
5483 }
5484
5485 return newAvatarKey;
5486}
5487
5488/**
5489 * Gets character data or a specific field.
5490 * @param {object} args Named arguments
5491 * @returns {Promise<string>} Character data or field value
5492 */
5493async function getCharacterDataCallback(args) {
5494 // Find the target character
5495 let character;
5496 if (args.char) {
5497 character = findChar({ name: args.char });
5498 if (!character) {
5499 toastr.warning(t`Character "${args.char}" not found`);
5500 return '';
5501 }
5502 } else {
5503 // Use currently selected character
5504 if (this_chid === undefined || !characters[this_chid]) {
5505 toastr.warning(t`No character selected and no char argument provided`);
5506 return '';
5507 }
5508 character = characters[this_chid];
5509 }
5510
5511 // If a specific field is requested
5512 if (args.field) {
5513 const fieldName = args.field;
5514
5515 // Try to get from data object first (V2 spec), then fall back to root
5516 let value = character.data?.[fieldName] ?? character[fieldName];
5517
5518 // Handle special cases for nested fields
5519 if (fieldName === 'talkativeness') {
5520 value = character.data?.extensions?.talkativeness ?? character.talkativeness ?? 0.5;
5521 }
5522 if (fieldName === 'tags') {
5523 value = character.data?.tags ?? character.tags ?? [];
5524 if (Array.isArray(value)) {
5525 value = value.join(', ');
5526 }
5527 }
5528
5529 if (value === undefined) {
5530 return '';
5531 }
5532
5533 return await slashCommandReturnHelper.doReturn(args.return ?? 'pipe', value, { objectToStringFunc: x => String(x) });
5534 }
5535
5536 // Return entire character data
5537 const charData = {
5538 avatar: character.avatar,
5539 name: character.name,
5540 description: character.description ?? character.data?.description ?? '',
5541 personality: character.personality ?? character.data?.personality ?? '',
5542 scenario: character.scenario ?? character.data?.scenario ?? '',
5543 first_mes: character.first_mes ?? character.data?.first_mes ?? '',
5544 mes_example: character.mes_example ?? character.data?.mes_example ?? '',
5545 creator_notes: character.data?.creator_notes ?? '',
5546 system_prompt: character.data?.system_prompt ?? '',
5547 post_history_instructions: character.data?.post_history_instructions ?? '',
5548 creator: character.data?.creator ?? '',
5549 character_version: character.data?.character_version ?? '',
5550 tags: character.data?.tags ?? character.tags ?? [],
5551 talkativeness: character.data?.extensions?.talkativeness ?? character.talkativeness ?? 0.5,
5552 fav: character.fav ?? character.data?.extensions?.fav ?? false,
5553 chat: character.chat,
5554 create_date: character.create_date,
5555 };
5556
5557 return await slashCommandReturnHelper.doReturn(args.return ?? 'pipe', charData, { objectToStringFunc: x => JSON.stringify(x, null, 2) });
5558}
5559
5560/**
5561 * Deletes a character using the core deleteCharacter function.
5562 * @param {object} args Named arguments
5563 * @returns {Promise<string>} 'true' if deleted, 'false' otherwise
5564 */
5565async function deleteCharacterCallback(args) {
5566 // Find the target character
5567 let character;
5568 if (args.char) {
5569 character = findChar({ name: args.char });
5570 if (!character) {
5571 toastr.warning(t`Character "${args.char}" not found`);
5572 return 'false';
5573 }
5574 } else {
5575 // Use currently selected character
5576 if (this_chid === undefined || !characters[this_chid]) {
5577 toastr.warning(t`No character selected and no char argument provided`);
5578 return 'false';
5579 }
5580 character = characters[this_chid];
5581 }
5582
5583 const deleteChats = isTrueBoolean(args.deleteChats);
5584 const silent = isTrueBoolean(args.silent);
5585
5586 // Show confirmation popup unless silent mode
5587 if (!silent) {
5588 const confirmMessage = deleteChats
5589 ? t`Are you sure you want to delete "${character.name}" and all associated chats? This action cannot be undone.`
5590 : t`Are you sure you want to delete "${character.name}"? This action cannot be undone.`;
5591
5592 const result = await callGenericPopup(confirmMessage, POPUP_TYPE.CONFIRM);
5593 if (result !== POPUP_RESULT.AFFIRMATIVE) {
5594 return 'false';
5595 }
5596 }
5597
5598 try {
5599 // Use the core deleteCharacter function which handles all cleanup and events
5600 const success = await deleteCharacter(character.avatar, { deleteChats });
5601 return success ? 'true' : 'false';
5602 } catch (error) {
5603 console.error('Error deleting character:', error);
5604 toastr.error(t`Failed to delete character: ${error.message}`);
5605 return 'false';
5606 }
5607}
5608
5609async function continueChatCallback(args, prompt) {
5610 const shouldAwait = isTrueBoolean(args?.await);
5611
5612 const outerPromise = new Promise(async (resolve, reject) => {
5613 try {
5614 await waitUntilCondition(() => !is_send_press && !is_group_generating, 10000, 100);
5615 } catch {
5616 console.warn('Timeout waiting for generation unlock');
5617 toastr.warning(t`Cannot run /continue command while the reply is being generated.`);
5618 return reject();
5619 }
5620
5621 try {
5622 // Prevent infinite recursion
5623 $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
5624
5625 const options = prompt?.trim() ? { quiet_prompt: prompt.trim(), quietToLoud: true } : {};
5626 await Generate('continue', options);
5627
5628 resolve();
5629 } catch (error) {
5630 console.error('Error running /continue command:', error);
5631 reject(error);
5632 }
5633 });
5634
5635 if (shouldAwait) {
5636 await outerPromise;
5637 }
5638
5639 return '';
5640}
5641
5642async function regenerateChatCallback(args) {
5643 const shouldAwait = isTrueBoolean(args?.await);
5644
5645 const outerPromise = new Promise((outerResolve) => setTimeout(async () => {
5646 try {
5647 await waitUntilCondition(() => !is_send_press && !is_group_generating, 10000, 100);
5648 } catch {
5649 console.warn('Timeout waiting for generation unlock');
5650 toastr.warning(t`Cannot run /regenerate command while the reply is being generated.`);
5651 outerResolve(Promise.resolve(''));
5652 return '';
5653 }
5654
5655 if (selected_group) {
5656 outerResolve(Promise.resolve(regenerateGroup()));
5657 return '';
5658 }
5659
5660 outerResolve(new Promise(innerResolve => setTimeout(() => {
5661 innerResolve(Generate('regenerate'));
5662 }, 1)));
5663 return '';
5664 }, 1));
5665
5666 if (shouldAwait) {
5667 const innerPromise = await outerPromise;
5668 await innerPromise;
5669 }
5670
5671 return '';
5672}
5673
5674async function swipeChatCallback(args) {
5675 const shouldAwait = isTrueBoolean(args?.await);
5676 const direction = args?.direction === SWIPE_DIRECTION.LEFT ? SWIPE_DIRECTION.LEFT : SWIPE_DIRECTION.RIGHT;
5677
5678 const outerPromise = new Promise((outerResolve) => setTimeout(async () => {
5679 try {
5680 await waitUntilCondition(() => !is_send_press && !is_group_generating, 10000, 100);
5681 } catch {
5682 console.warn('Timeout waiting for generation unlock');
5683 toastr.warning(t`Cannot run /swipe command while the reply is being generated.`);
5684 outerResolve(Promise.resolve(''));
5685 return '';
5686 }
5687
5688 outerResolve(Promise.resolve(swipe(null, direction, { source: SWIPE_SOURCE.SLASH_COMMAND, repeated: false })));
5689 return '';
5690 }, 1));
5691
5692 if (shouldAwait) {
5693 const innerPromise = await outerPromise;
5694 await innerPromise;
5695 }
5696
5697 return '';
5698}
5699
5700export async function generateSystemMessage(args, prompt) {
5701 $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
5702
5703 if (!prompt) {
5704 console.warn('WARN: No prompt provided for /sysgen command');
5705 toastr.warning(t`You must provide a prompt for the system message`);
5706 return '';
5707 }
5708
5709 const trim = isTrueBoolean(args?.trim?.toString());
5710
5711 // Generate and regex the output if applicable
5712 const toast = toastr.info(t`Please wait`, t`Generating...`);
5713 const message = await generateQuietPrompt({ quietPrompt: prompt, trimToSentence: trim });
5714 toastr.clear(toast);
5715
5716 return await sendNarratorMessage(args, getRegexedString(message, regex_placement.SLASH_COMMAND));
5717}
5718
5719function setStoryModeCallback() {
5720 $('#chat_display').val(chat_styles.DOCUMENT).trigger('change');
5721 return '';
5722}
5723
5724function setBubbleModeCallback() {
5725 $('#chat_display').val(chat_styles.BUBBLES).trigger('change');
5726 return '';
5727}
5728
5729function setFlatModeCallback() {
5730 $('#chat_display').val(chat_styles.DEFAULT).trigger('change');
5731 return '';
5732}
5733
5734async function setNarratorName(_, text) {
5735 const name = text || NARRATOR_NAME_DEFAULT;
5736 chat_metadata[NARRATOR_NAME_KEY] = name;
5737 toastr.info(t`System narrator name set to ${name}`);
5738 await saveChatConditional();
5739 return '';
5740}
5741
5742/**
5743 * Checks if an argument is a string array (or undefined), and if not, throws an error
5744 * @param {string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined} arg The named argument to check
5745 * @param {string} name The name of the argument for the error message
5746 * @param {object} [options={}] - The optional arguments
5747 * @param {boolean} [options.allowUndefined=false] - Whether the argument can be undefined
5748 * @throws {Error} If the argument is not an array
5749 * @returns {string[]}
5750 */
5751export function validateArrayArgString(arg, name, { allowUndefined = true } = {}) {
5752 if (arg === undefined) {
5753 if (allowUndefined) return undefined;
5754 throw new Error(t`Argument "${name}" is undefined, but must be a string array`);
5755 }
5756 if (!Array.isArray(arg)) throw new Error(t`Argument "${name}" must be an array`);
5757 if (!arg.every(x => typeof x === 'string')) throw new Error(t`Argument "${name}" must be an array of strings`);
5758 return arg;
5759}
5760
5761/**
5762 * Checks if an argument is a string or closure array (or undefined), and if not, throws an error
5763 * @param {string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined} arg The named argument to check
5764 * @param {string} name The name of the argument for the error message
5765 * @param {object} [options={}] - The optional arguments
5766 * @param {boolean} [options.allowUndefined=false] - Whether the argument can be undefined
5767 * @throws {Error} If the argument is not an array of strings or closures
5768 * @returns {(string|SlashCommandClosure)[]}
5769 */
5770export function validateArrayArg(arg, name, { allowUndefined = true } = {}) {
5771 if (arg === undefined) {
5772 if (allowUndefined) return [];
5773 throw new Error(t`Argument "${name}" is undefined, but must be an array of strings or closures`);
5774 }
5775 if (!Array.isArray(arg)) throw new Error(t`Argument "${name}" must be an array`);
5776 if (!arg.every(x => typeof x === 'string' || x instanceof SlashCommandClosure)) throw new Error(t`Argument "${name}" must be an array of strings or closures`);
5777 return arg;
5778}
5779
5780
5781/**
5782 * Retrieves the name and avatar information for a message
5783 *
5784 * The name of the character will always have precendence over the one given as argument. If you want to specify a different name for the message,
5785 * explicitly implement this in the code using this.
5786 *
5787 * @param {object?} character - The character object to get the avatar data for
5788 * @param {string?} name - The name to get the avatar data for
5789 * @returns {{name: string, force_avatar: string, original_avatar: string}} An object containing the name for the message, forced avatar URL, and original avatar
5790 */
5791export function getNameAndAvatarForMessage(character, name = null) {
5792 const isNeutralCharacter = !character && name2 === neutralCharacterName && name === neutralCharacterName;
5793 const currentChar = characters[this_chid];
5794
5795 let force_avatar, original_avatar;
5796 if (character?.avatar === currentChar?.avatar || isNeutralCharacter) {
5797 // If the targeted character is the currently selected one in a solo chat, we don't need to force any avatars
5798 } else if (character && character.avatar !== 'none') {
5799 force_avatar = getThumbnailUrl('avatar', character.avatar);
5800 original_avatar = character.avatar;
5801 } else {
5802 force_avatar = default_avatar;
5803 original_avatar = default_avatar;
5804 }
5805
5806 return {
5807 name: character?.name || name,
5808 force_avatar: force_avatar,
5809 original_avatar: original_avatar,
5810 };
5811}
5812
5813/**
5814 * Changes the character role on a message at a given index.
5815 * @param {object?} args - Named arguments
5816 * @param {string} role - Role to change to.
5817 *
5818 * @returns {Promise<string>} The updated message role.
5819 */
5820async function messageRoleCallback(args, role) {
5821 let modifyAt = Number(args?.at ?? (chat.length - 1));
5822 // Convert possible depth parameter to index
5823 if (!isNaN(modifyAt) && (modifyAt < 0 || Object.is(modifyAt, -0))) {
5824 // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)
5825 modifyAt = chat.length + modifyAt;
5826 }
5827
5828 const message = chat[modifyAt];
5829 if (!message) {
5830 toastr.warning(t`No message found at the specified index.`);
5831 return '';
5832 }
5833
5834 role = String(role ?? '').trim().toLowerCase();
5835 if (!role || !['user', 'assistant', 'system'].includes(role)) {
5836 return message?.extra?.type === system_message_types.NARRATOR
5837 ? 'system'
5838 : message.is_user ? 'user' : 'assistant';
5839 }
5840
5841 message.extra = message.extra || {};
5842 if (role === 'system') {
5843 message.extra.type = system_message_types.NARRATOR;
5844 } else {
5845 delete message.extra.type;
5846 }
5847 message.is_user = role === 'user';
5848
5849 await eventSource.emit(event_types.MESSAGE_EDITED, modifyAt);
5850 const existingMessage = chatElement.find(`.mes[mesid="${modifyAt}"]`);
5851 if (existingMessage.length) {
5852 const newMessageElement = updateMessageElement(message, { messageId: modifyAt });
5853 existingMessage.after(newMessageElement);
5854 existingMessage.remove();
5855 }
5856 await eventSource.emit(event_types.MESSAGE_UPDATED, modifyAt);
5857 await saveChatConditional();
5858
5859 return role;
5860}
5861
5862/**
5863 * Changes the character name on a message at a given index.
5864 * @param {object?} args - Named arguments
5865 * @param {string} name - Name to change to.
5866 *
5867 * @returns {Promise<string>} The updated message name.
5868 */
5869async function messageNameCallback(args, name) {
5870 let modifyAt = Number(args?.at ?? (chat.length - 1));
5871 // Convert possible depth parameter to index
5872 if (!isNaN(modifyAt) && (modifyAt < 0 || Object.is(modifyAt, -0))) {
5873 // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)
5874 modifyAt = chat.length + modifyAt;
5875 }
5876
5877 const message = chat[modifyAt];
5878 if (!message) {
5879 toastr.warning(t`No message found at the specified index.`);
5880 return '';
5881 }
5882
5883 name = String(name ?? '').trim();
5884 if (!name) {
5885 return message.name;
5886 }
5887
5888 let newName = '';
5889
5890 if (message.is_user) {
5891 const persona = findPersona({ name: name });
5892 if (persona) {
5893 message.name = newName = persona.name;
5894 message.force_avatar = getThumbnailUrl('persona', persona.avatar);
5895 message.original_avatar = persona.avatar;
5896 } else {
5897 message.name = newName = name;
5898 message.force_avatar = default_avatar;
5899 message.original_avatar = default_avatar;
5900 }
5901 } else {
5902 const character = findChar({ name: name });
5903 if (character) {
5904 const characterInfo = getNameAndAvatarForMessage(character, name);
5905 message.name = newName = characterInfo.name;
5906 message.force_avatar = characterInfo.force_avatar;
5907 message.original_avatar = characterInfo.original_avatar;
5908 } else {
5909 message.name = newName = name;
5910 message.force_avatar = default_avatar;
5911 message.original_avatar = default_avatar;
5912 }
5913 }
5914
5915 await eventSource.emit(event_types.MESSAGE_EDITED, modifyAt);
5916 const existingMessage = chatElement.find(`.mes[mesid="${modifyAt}"]`);
5917 if (existingMessage.length) {
5918 const newMessageElement = updateMessageElement(message, { messageId: modifyAt });
5919 existingMessage.after(newMessageElement);
5920 existingMessage.remove();
5921 }
5922 await eventSource.emit(event_types.MESSAGE_UPDATED, modifyAt);
5923 await saveChatConditional();
5924
5925 return newName;
5926}
5927
5928export async function sendMessageAs(args, text) {
5929 let name = args.name?.trim();
5930
5931 if (!name) {
5932 const namelessWarningKey = 'sendAsNamelessWarningShown';
5933 if (accountStorage.getItem(namelessWarningKey) !== 'true') {
5934 toastr.warning(t`To avoid confusion, please use /sendas name="Character Name"`, t`Name defaulted to {{char}}`, { timeOut: 10000 });
5935 accountStorage.setItem(namelessWarningKey, 'true');
5936 }
5937 name = name2;
5938 }
5939
5940 let mesText = String(text ?? '').trim();
5941
5942 // Requires a regex check after the slash command is pushed to output
5943 mesText = getRegexedString(mesText, regex_placement.SLASH_COMMAND, { characterOverride: name });
5944
5945 // Messages that do nothing but set bias will be hidden from the context
5946 const bias = extractMessageBias(mesText);
5947 const isSystem = bias && !removeMacros(mesText).length;
5948 const compact = isTrueBoolean(args?.compact);
5949
5950 const character = findChar({ name: name });
5951
5952 const avatarCharacter = args.avatar ? findChar({ name: args.avatar }) : character;
5953 if (args.avatar && !avatarCharacter) {
5954 toastr.warning(t`Character for avatar ${args.avatar} not found`);
5955 return '';
5956 }
5957
5958 const { name: avatarCharName, force_avatar, original_avatar } = getNameAndAvatarForMessage(avatarCharacter, name);
5959
5960 const message = {
5961 name: character?.name || name || avatarCharName,
5962 is_user: false,
5963 is_system: isSystem,
5964 send_date: getMessageTimeStamp(),
5965 mes: substituteParams(mesText),
5966 force_avatar: force_avatar,
5967 original_avatar: original_avatar,
5968 extra: {
5969 bias: bias.trim().length ? bias : null,
5970 gen_id: Date.now(),
5971 isSmallSys: compact,
5972 api: 'manual',
5973 model: 'slash command',
5974 },
5975 };
5976
5977 message.swipe_id = 0;
5978 message.swipes = [message.mes];
5979 message.swipe_info = [{
5980 send_date: message.send_date,
5981 gen_started: null,
5982 gen_finished: null,
5983 extra: {
5984 bias: message.extra.bias,
5985 gen_id: message.extra.gen_id,
5986 isSmallSys: compact,
5987 api: 'manual',
5988 model: 'slash command',
5989 },
5990 }];
5991
5992 let insertAt = Number(args.at);
5993
5994 // Convert possible depth parameter to index
5995 if (!isNaN(insertAt) && (insertAt < 0 || Object.is(insertAt, -0))) {
5996 // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)
5997 insertAt = chat.length + insertAt;
5998 }
5999
6000 chat_metadata.tainted = true;
6001
6002 if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) {
6003 chat.splice(insertAt, 0, message);
6004 await saveChatConditional();
6005 await eventSource.emit(event_types.MESSAGE_RECEIVED, insertAt, 'command');
6006 await reloadCurrentChat();
6007 await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, insertAt, 'command');
6008 } else {
6009 chat.push(message);
6010 await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1), 'command');
6011 addOneMessage(message);
6012 await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, (chat.length - 1), 'command');
6013 await saveChatConditional();
6014 }
6015
6016 return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
6017}
6018
6019export async function sendNarratorMessage(args, text) {
6020 text = String(text ?? '');
6021 const name = args.name ?? (chat_metadata[NARRATOR_NAME_KEY] || NARRATOR_NAME_DEFAULT);
6022 // Messages that do nothing but set bias will be hidden from the context
6023 const bias = extractMessageBias(text);
6024 const isSystem = bias && !removeMacros(text).length;
6025 const compact = isTrueBoolean(args?.compact);
6026
6027 const message = {
6028 name: name,
6029 is_user: false,
6030 is_system: isSystem,
6031 send_date: getMessageTimeStamp(),
6032 mes: substituteParams(text.trim()),
6033 force_avatar: system_avatar,
6034 extra: {
6035 type: system_message_types.NARRATOR,
6036 bias: bias.trim().length ? bias : null,
6037 gen_id: Date.now(),
6038 isSmallSys: compact,
6039 api: 'manual',
6040 model: 'slash command',
6041 },
6042 };
6043
6044 let insertAt = Number(args.at);
6045
6046 // Convert possible depth parameter to index
6047 if (!isNaN(insertAt) && (insertAt < 0 || Object.is(insertAt, -0))) {
6048 // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)
6049 insertAt = chat.length + insertAt;
6050 }
6051
6052 chat_metadata.tainted = true;
6053
6054 if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) {
6055 chat.splice(insertAt, 0, message);
6056 await saveChatConditional();
6057 await eventSource.emit(event_types.MESSAGE_SENT, insertAt);
6058 await reloadCurrentChat();
6059 await eventSource.emit(event_types.USER_MESSAGE_RENDERED, insertAt);
6060 } else {
6061 chat.push(message);
6062 await eventSource.emit(event_types.MESSAGE_SENT, (chat.length - 1));
6063 addOneMessage(message);
6064 await eventSource.emit(event_types.USER_MESSAGE_RENDERED, (chat.length - 1));
6065 await saveChatConditional();
6066 }
6067
6068 return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
6069}
6070
6071export async function promptQuietForLoudResponse(who, text) {
6072 let character_id = getContext().characterId;
6073 if (who === 'sys') {
6074 text = 'System: ' + text;
6075 } else if (who === 'user') {
6076 text = name1 + ': ' + text;
6077 } else if (who === 'char') {
6078 text = characters[character_id].name + ': ' + text;
6079 } else if (who === 'raw') {
6080 // We don't need to modify the text
6081 }
6082
6083 //text = `${text}${power_user.instruct.enabled ? '' : '\n'}${(power_user.always_force_name2 && who != 'raw') ? characters[character_id].name + ":" : ""}`
6084
6085 let reply = await generateQuietPrompt({ quietPrompt: text, quietToLoud: true });
6086 text = await getRegexedString(reply, regex_placement.SLASH_COMMAND);
6087
6088 const message = {
6089 name: characters[character_id].name,
6090 is_user: false,
6091 is_name: true,
6092 is_system: false,
6093 send_date: getMessageTimeStamp(),
6094 mes: substituteParams(text.trim()),
6095 extra: {
6096 type: system_message_types.COMMENT,
6097 gen_id: Date.now(),
6098 api: 'manual',
6099 model: 'slash command',
6100 },
6101 };
6102
6103 chat_metadata.tainted = true;
6104
6105 chat.push(message);
6106 await eventSource.emit(event_types.MESSAGE_SENT, (chat.length - 1));
6107 addOneMessage(message);
6108 await eventSource.emit(event_types.USER_MESSAGE_RENDERED, (chat.length - 1));
6109 await saveChatConditional();
6110}
6111
6112async function sendCommentMessage(args, text) {
6113 const compact = isTrueBoolean(args?.compact);
6114 const message = {
6115 name: COMMENT_NAME_DEFAULT,
6116 is_user: false,
6117 is_system: true,
6118 send_date: getMessageTimeStamp(),
6119 mes: substituteParams(String(text ?? '').trim()),
6120 force_avatar: comment_avatar,
6121 extra: {
6122 type: system_message_types.COMMENT,
6123 gen_id: Date.now(),
6124 isSmallSys: compact,
6125 api: 'manual',
6126 model: 'slash command',
6127 },
6128 };
6129
6130 let insertAt = Number(args.at);
6131
6132 // Convert possible depth parameter to index
6133 if (!isNaN(insertAt) && (insertAt < 0 || Object.is(insertAt, -0))) {
6134 // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)
6135 insertAt = chat.length + insertAt;
6136 }
6137
6138 chat_metadata.tainted = true;
6139
6140 if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) {
6141 chat.splice(insertAt, 0, message);
6142 await saveChatConditional();
6143 await eventSource.emit(event_types.MESSAGE_SENT, insertAt);
6144 await reloadCurrentChat();
6145 await eventSource.emit(event_types.USER_MESSAGE_RENDERED, insertAt);
6146 } else {
6147 chat.push(message);
6148 await eventSource.emit(event_types.MESSAGE_SENT, (chat.length - 1));
6149 addOneMessage(message);
6150 await eventSource.emit(event_types.USER_MESSAGE_RENDERED, (chat.length - 1));
6151 await saveChatConditional();
6152 }
6153
6154 return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
6155}
6156
6157/**
6158 * Displays a help message from the slash command
6159 * @param {any} _ Unused
6160 * @param {string} type Type of help to display
6161 */
6162function helpCommandCallback(_, type) {
6163 switch (type?.trim()?.toLowerCase()) {
6164 case 'slash':
6165 case 'commands':
6166 case 'slashes':
6167 case 'slash commands':
6168 case '1':
6169 sendSystemMessage(system_message_types.SLASH_COMMANDS);
6170 break;
6171 case 'format':
6172 case 'formatting':
6173 case 'formats':
6174 case 'chat formatting':
6175 case '2':
6176 sendSystemMessage(system_message_types.FORMATTING);
6177 break;
6178 case 'hotkeys':
6179 case 'hotkey':
6180 case '3':
6181 sendSystemMessage(system_message_types.HOTKEYS);
6182 break;
6183 case 'macros':
6184 case 'macro':
6185 case '4':
6186 sendSystemMessage(system_message_types.MACROS);
6187 break;
6188 default:
6189 sendSystemMessage(system_message_types.HELP);
6190 break;
6191 }
6192
6193 return '';
6194}
6195
6196$(document).on('click', '[data-displayHelp]', function (e) {
6197 e.preventDefault();
6198 const page = String($(this).data('displayhelp'));
6199 helpCommandCallback(null, page);
6200});
6201
6202function setBackgroundCallback(_, bg) {
6203 if (!bg) {
6204 // allow reporting of the background name if called without args
6205 // for use in ST Scripts via pipe
6206 return background_settings.name;
6207 }
6208
6209 console.log('Set background to ' + bg);
6210
6211 const bgElements = Array.from(document.querySelectorAll('.bg_example')).map((x) => ({ element: x, bgfile: x.getAttribute('bgfile') }));
6212
6213 const fuse = new Fuse(bgElements, { keys: ['bgfile'] });
6214 const result = fuse.search(bg);
6215
6216 if (!result.length) {
6217 toastr.error(t`No background found with name "${bg}"`);
6218 return '';
6219 }
6220
6221 const bgElement = result[0].item.element;
6222
6223 if (bgElement instanceof HTMLElement) {
6224 bgElement.click();
6225 }
6226
6227 return '';
6228}
6229
6230/**
6231 * Retrieves the available model options based on the currently selected main API and its subtype
6232 * @param {boolean} quiet - Whether to suppress toasts
6233 *
6234 * @returns {{control: HTMLSelectElement|HTMLInputElement, options: HTMLOptionElement[]}?} An array of objects representing the available model options, or null if not supported
6235 */
6236function getModelOptions(quiet) {
6237 const nullResult = { control: null, options: null };
6238 const modelSelectMap = [
6239 { id: 'generic_model_textgenerationwebui', api: 'textgenerationwebui', type: textgen_types.GENERIC },
6240 { id: 'custom_model_textgenerationwebui', api: 'textgenerationwebui', type: textgen_types.OOBA },
6241 { id: 'model_togetherai_select', api: 'textgenerationwebui', type: textgen_types.TOGETHERAI },
6242 { id: 'openrouter_model', api: 'textgenerationwebui', type: textgen_types.OPENROUTER },
6243 { id: 'model_infermaticai_select', api: 'textgenerationwebui', type: textgen_types.INFERMATICAI },
6244 { id: 'model_dreamgen_select', api: 'textgenerationwebui', type: textgen_types.DREAMGEN },
6245 { id: 'mancer_model', api: 'textgenerationwebui', type: textgen_types.MANCER },
6246 { id: 'vllm_model', api: 'textgenerationwebui', type: textgen_types.VLLM },
6247 { id: 'aphrodite_model', api: 'textgenerationwebui', type: textgen_types.APHRODITE },
6248 { id: 'ollama_model', api: 'textgenerationwebui', type: textgen_types.OLLAMA },
6249 { id: 'tabby_model', api: 'textgenerationwebui', type: textgen_types.TABBY },
6250 { id: 'llamacpp_model', api: 'textgenerationwebui', type: textgen_types.LLAMACPP },
6251 { id: 'featherless_model', api: 'textgenerationwebui', type: textgen_types.FEATHERLESS },
6252 { id: 'model_openai_select', api: 'openai', type: chat_completion_sources.OPENAI },
6253 { id: 'model_claude_select', api: 'openai', type: chat_completion_sources.CLAUDE },
6254 { id: 'model_openrouter_select', api: 'openai', type: chat_completion_sources.OPENROUTER },
6255 { id: 'model_ai21_select', api: 'openai', type: chat_completion_sources.AI21 },
6256 { id: 'model_google_select', api: 'openai', type: chat_completion_sources.MAKERSUITE },
6257 { id: 'model_vertexai_select', api: 'openai', type: chat_completion_sources.VERTEXAI },
6258 { id: 'model_mistralai_select', api: 'openai', type: chat_completion_sources.MISTRALAI },
6259 { id: 'custom_model_id', api: 'openai', type: chat_completion_sources.CUSTOM },
6260 { id: 'model_cohere_select', api: 'openai', type: chat_completion_sources.COHERE },
6261 { id: 'model_perplexity_select', api: 'openai', type: chat_completion_sources.PERPLEXITY },
6262 { id: 'model_groq_select', api: 'openai', type: chat_completion_sources.GROQ },
6263 { id: 'model_chutes_select', api: 'openai', type: chat_completion_sources.CHUTES },
6264 { id: 'model_siliconflow_select', api: 'openai', type: chat_completion_sources.SILICONFLOW },
6265 { id: 'model_minimax_select', api: 'openai', type: chat_completion_sources.MINIMAX },
6266 { id: 'model_electronhub_select', api: 'openai', type: chat_completion_sources.ELECTRONHUB },
6267 { id: 'model_nanogpt_select', api: 'openai', type: chat_completion_sources.NANOGPT },
6268 { id: 'model_deepseek_select', api: 'openai', type: chat_completion_sources.DEEPSEEK },
6269 { id: 'model_aimlapi_select', api: 'openai', type: chat_completion_sources.AIMLAPI },
6270 { id: 'model_xai_select', api: 'openai', type: chat_completion_sources.XAI },
6271 { id: 'model_pollinations_select', api: 'openai', type: chat_completion_sources.POLLINATIONS },
6272 { id: 'model_moonshot_select', api: 'openai', type: chat_completion_sources.MOONSHOT },
6273 { id: 'model_fireworks_select', api: 'openai', type: chat_completion_sources.FIREWORKS },
6274 { id: 'model_cometapi_select', api: 'openai', type: chat_completion_sources.COMETAPI },
6275 { id: 'model_zai_select', api: 'openai', type: chat_completion_sources.ZAI },
6276 { id: 'model_workers_ai_select', api: 'openai', type: chat_completion_sources.WORKERS_AI },
6277 { id: 'model_novel_select', api: 'novel', type: null },
6278 { id: 'horde_model', api: 'koboldhorde', type: null },
6279 ];
6280
6281 function getSubType() {
6282 switch (main_api) {
6283 case 'textgenerationwebui':
6284 return textgenerationwebui_settings.type;
6285 case 'openai':
6286 return oai_settings.chat_completion_source;
6287 default:
6288 return null;
6289 }
6290 }
6291
6292 const apiSubType = getSubType();
6293 const modelSelectItem = modelSelectMap.find(x => x.api == main_api && x.type == apiSubType)?.id;
6294
6295 if (!modelSelectItem) {
6296 !quiet && toastr.info(t`Setting a model for your API is not supported or not implemented yet.`);
6297 return nullResult;
6298 }
6299
6300 const modelSelectControl = document.getElementById(modelSelectItem);
6301
6302 if (!(modelSelectControl instanceof HTMLSelectElement) && !(modelSelectControl instanceof HTMLInputElement)) {
6303 !quiet && toastr.error(t`Model select control not found: ${main_api}[${apiSubType}]`);
6304 return nullResult;
6305 }
6306
6307 /**
6308 * Get options from a HTMLSelectElement or HTMLInputElement with a list.
6309 * @param {HTMLSelectElement | HTMLInputElement} control Control containing the options
6310 * @returns {HTMLOptionElement[]} Array of options
6311 */
6312 const getOptions = (control) => {
6313 if (control instanceof HTMLSelectElement) {
6314 return Array.from(control.options);
6315 }
6316
6317 const valueOption = new Option(control.value, control.value);
6318
6319 if (control instanceof HTMLInputElement && control.list instanceof HTMLDataListElement) {
6320 return [valueOption, ...Array.from(control.list.options)];
6321 }
6322
6323 return [valueOption];
6324 };
6325
6326 const options = getOptions(modelSelectControl).filter(x => x.value).filter(onlyUnique);
6327 return { control: modelSelectControl, options };
6328}
6329
6330/**
6331 * Sets a model for the current API.
6332 * @param {object} args Named arguments
6333 * @param {string} model New model name
6334 * @returns {string} New or existing model name
6335 */
6336function modelCallback(args, model) {
6337 const quiet = isTrueBoolean(args?.quiet);
6338 const { control: modelSelectControl, options } = getModelOptions(quiet);
6339
6340 // If no model was found, the reason was already logged, we just return here
6341 if (options === null) {
6342 return '';
6343 }
6344
6345 model = String(model || '').trim();
6346
6347 if (!model) {
6348 return modelSelectControl.value;
6349 }
6350
6351 console.log('Set model to ' + model);
6352
6353 if (modelSelectControl instanceof HTMLInputElement) {
6354 modelSelectControl.value = model;
6355 $(modelSelectControl).trigger('input');
6356 !quiet && toastr.success(t`Model set to "${model}"`);
6357 return model;
6358 }
6359
6360 if (!options.length) {
6361 !quiet && toastr.warning(t`No model options found. Check your API settings.`);
6362 return '';
6363 }
6364
6365 let newSelectedOption = null;
6366
6367 const fuse = new Fuse(options, { keys: ['text', 'value'] });
6368 const fuzzySearchResult = fuse.search(model);
6369
6370 const exactValueMatch = options.find(x => x.value.trim().toLowerCase() === model.trim().toLowerCase());
6371 const exactTextMatch = options.find(x => x.text.trim().toLowerCase() === model.trim().toLowerCase());
6372
6373 if (exactValueMatch) {
6374 newSelectedOption = exactValueMatch;
6375 } else if (exactTextMatch) {
6376 newSelectedOption = exactTextMatch;
6377 } else if (fuzzySearchResult.length) {
6378 newSelectedOption = fuzzySearchResult[0].item;
6379 }
6380
6381 if (newSelectedOption) {
6382 modelSelectControl.value = newSelectedOption.value;
6383 $(modelSelectControl).trigger('change');
6384 !quiet && toastr.success(t`Model set to "${newSelectedOption.text}"`);
6385 return newSelectedOption.value;
6386 } else {
6387 !quiet && toastr.warning(t`No model found with name "${model}"`);
6388 return '';
6389 }
6390}
6391
6392/**
6393 * Gets the state of prompt entries (toggles) either via identifier/uuid or name.
6394 * @param {object} args Object containing arguments
6395 * @param {string} args.identifier Select prompt entry using an identifier (uuid)
6396 * @param {string} args.name Select prompt entry using name
6397 * @param {string} args.return The type of return value to use (simple, list, dict)
6398 * @returns {Object} An object containing the states of the requested prompt entries
6399 */
6400function getPromptEntryCallback(args) {
6401 const prompts = promptManager.serviceSettings.prompts;
6402 let returnType = args.return ?? 'simple';
6403
6404 function parseArgs(arg) {
6405 // Arg is already an array
6406 if (Array.isArray(arg)) {
6407 return arg;
6408 }
6409 const list = [];
6410 try {
6411 // Arg is a JSON-stringified array
6412 const parsedArg = JSON.parse(arg);
6413 list.push(...Array.isArray(parsedArg) ? parsedArg : [arg]);
6414 } catch {
6415 // Arg is a string
6416 list.push(arg);
6417 }
6418 return list;
6419 }
6420
6421 let identifiersList = parseArgs(args.identifier);
6422 let nameList = parseArgs(args.name);
6423
6424 // Check if identifiers exists in prompt, else remove from list
6425 if (identifiersList.length !== 0) {
6426 identifiersList = identifiersList.filter(identifier => prompts.some(prompt => prompt.identifier === identifier));
6427 }
6428
6429 if (nameList.length !== 0) {
6430 nameList.forEach(name => {
6431 let identifiers = prompts
6432 .filter(entry => entry.name === name)
6433 .map(entry => entry.identifier);
6434 identifiersList = identifiersList.concat(identifiers);
6435 });
6436 }
6437
6438 // Get the state for each prompt entry
6439 let promptStates = new Map();
6440 identifiersList.forEach(identifier => {
6441 const promptOrderEntry = promptManager.getPromptOrderEntry(promptManager.activeCharacter, identifier);
6442 if (promptOrderEntry) {
6443 promptStates.set(identifier, promptOrderEntry.enabled);
6444 }
6445 });
6446
6447 // If return is simple (default) but more than one prompt state was retrieved, then change return type
6448 if (returnType === 'simple' && promptStates.size > 1) {
6449 returnType = args.identifier ? 'dict' : 'list';
6450 }
6451
6452 const result = (() => {
6453 if (returnType === 'list') return [...promptStates.values()];
6454 if (returnType === 'dict') return Object.fromEntries(promptStates);
6455 return [...promptStates.values()][0];
6456 })();
6457
6458 return result;
6459}
6460
6461/**
6462 * Sets state of prompt entries (toggles) either via identifier/uuid or name.
6463 * @param {object} args Object containing arguments
6464 * @param {string} args.identifier Select prompt entry using an identifier (uuid)
6465 * @param {string} args.name Select prompt entry using name
6466 * @param {string} targetState The targeted state of the entry/entries
6467 * @returns {String} empty string
6468 */
6469function setPromptEntryCallback(args, targetState) {
6470 // needs promptManager to manipulate prompt entries
6471 const prompts = promptManager.serviceSettings.prompts;
6472
6473 function parseArgs(arg) {
6474 // Arg is already an array
6475 if (Array.isArray(arg)) {
6476 return arg;
6477 }
6478 const list = [];
6479 try {
6480 // Arg is a JSON-stringified array
6481 const parsedArg = JSON.parse(arg);
6482 list.push(...Array.isArray(parsedArg) ? parsedArg : [arg]);
6483 } catch {
6484 // Arg is a string
6485 list.push(arg);
6486 }
6487 return list;
6488 }
6489
6490 let identifiersList = parseArgs(args.identifier);
6491 let nameList = parseArgs(args.name);
6492
6493 // Check if identifiers exists in prompt, else remove from list
6494 if (identifiersList.length !== 0) {
6495 identifiersList = identifiersList.filter(identifier => prompts.some(prompt => prompt.identifier === identifier));
6496 }
6497
6498 if (nameList.length !== 0) {
6499 nameList.forEach(name => {
6500 // one name could potentially have multiple entries, find all identifiers that match given name
6501 let identifiers = [];
6502 prompts.forEach(entry => {
6503 if (entry.name === name) {
6504 identifiers.push(entry.identifier);
6505 }
6506 });
6507 identifiersList = identifiersList.concat(identifiers);
6508 });
6509 }
6510
6511 // Remove duplicates to allow consistent 'toggle'
6512 identifiersList = [...new Set(identifiersList)];
6513 if (identifiersList.length === 0) return '';
6514
6515 // logic adapted from PromptManager.js, handleToggle
6516 const getPromptOrderEntryState = (promptOrderEntry) => {
6517 if (['toggle', 't', ''].includes(targetState.trim().toLowerCase())) {
6518 return !promptOrderEntry.enabled;
6519 }
6520
6521 if (isTrueBoolean(targetState)) {
6522 return true;
6523 }
6524
6525 if (isFalseBoolean(targetState)) {
6526 return false;
6527 }
6528
6529 return promptOrderEntry.enabled;
6530 };
6531
6532 identifiersList.forEach(promptID => {
6533 const promptOrderEntry = promptManager.getPromptOrderEntry(promptManager.activeCharacter, promptID);
6534 const counts = promptManager.tokenHandler.getCounts();
6535
6536 counts[promptID] = null;
6537 promptOrderEntry.enabled = getPromptOrderEntryState(promptOrderEntry);
6538 });
6539
6540 // no need to render for each identifier
6541 promptManager.render();
6542 promptManager.saveServiceSettings();
6543 return '';
6544}
6545
6546/**
6547 * Sets the API URL and triggers the text generation web UI button click.
6548 *
6549 * @param {object} args - named args
6550 * @param {string?} [args.api=null] - the API name to set/get the URL for
6551 * @param {string?} [args.connect=true] - whether to connect to the API after setting
6552 * @param {string?} [args.quiet=false] - whether to suppress toasts
6553 * @param {string} url - the API URL to set
6554 * @returns {Promise<string>}
6555 */
6556async function setApiUrlCallback({ api = null, connect = 'true', quiet = 'false' }, url) {
6557 const isQuiet = isTrueBoolean(quiet);
6558 const autoConnect = isTrueBoolean(connect);
6559
6560 // Special handling for Chat Completion Custom OpenAI compatible, that one can also support API url handling
6561 const isCurrentlyCustomOpenai = main_api === 'openai' && oai_settings.chat_completion_source === chat_completion_sources.CUSTOM;
6562 if (api === chat_completion_sources.CUSTOM || (!api && isCurrentlyCustomOpenai)) {
6563 if (!url) {
6564 return oai_settings.custom_url ?? '';
6565 }
6566
6567 if (!isCurrentlyCustomOpenai && autoConnect) {
6568 toastr.warning(t`Custom OpenAI API is not the currently selected API, so we cannot do an auto-connect. Consider switching to it via /api beforehand.`);
6569 return '';
6570 }
6571
6572 $('#custom_api_url_text').val(url).trigger('input');
6573
6574 if (autoConnect) {
6575 $('#api_button_openai').trigger('click');
6576 }
6577
6578 return url;
6579 }
6580
6581 const isCurrentlyZAI = main_api === 'openai' && oai_settings.chat_completion_source === chat_completion_sources.ZAI;
6582 if (api === chat_completion_sources.ZAI || (!api && isCurrentlyZAI)) {
6583 if (!url) {
6584 return oai_settings.zai_endpoint || ZAI_ENDPOINT.COMMON;
6585 }
6586
6587 const permittedValues = Object.values(ZAI_ENDPOINT);
6588 if (!permittedValues.includes(url)) {
6589 !isQuiet && toastr.warning(t`Valid options are: ${permittedValues.join(', ')}`, t`ZAI endpoint '${url}' is not a valid option.`);
6590 return '';
6591 }
6592
6593 if (!isCurrentlyZAI && autoConnect) {
6594 toastr.warning(t`Z.AI is not the currently selected API, so we cannot do an auto-connect. Consider switching to it via /api beforehand.`);
6595 return '';
6596 }
6597
6598 $('#zai_endpoint').val(url).trigger('input');
6599
6600 if (autoConnect) {
6601 $('#api_button_openai').trigger('click');
6602 }
6603
6604 return oai_settings.zai_endpoint || ZAI_ENDPOINT.COMMON;
6605 }
6606
6607 const isCurrentlySiliconFlow = main_api === 'openai' && oai_settings.chat_completion_source === chat_completion_sources.SILICONFLOW;
6608 if (api === chat_completion_sources.SILICONFLOW || (!api && isCurrentlySiliconFlow)) {
6609 if (!url) {
6610 return oai_settings.siliconflow_endpoint || SILICONFLOW_ENDPOINT.GLOBAL;
6611 }
6612
6613 const permittedValues = Object.values(SILICONFLOW_ENDPOINT);
6614 if (!permittedValues.includes(url)) {
6615 !isQuiet && toastr.warning(t`Valid options are: ${permittedValues.join(', ')}`, t`SiliconFlow endpoint '${url}' is not a valid option.`);
6616 return '';
6617 }
6618
6619 if (!isCurrentlySiliconFlow && autoConnect) {
6620 toastr.warning(t`SiliconFlow is not the currently selected API, so we cannot do an auto-connect. Consider switching to it via /api beforehand.`);
6621 return '';
6622 }
6623
6624 $('#siliconflow_endpoint').val(url).trigger('input');
6625
6626 if (autoConnect) {
6627 $('#api_button_openai').trigger('click');
6628 }
6629
6630 return oai_settings.siliconflow_endpoint || SILICONFLOW_ENDPOINT.GLOBAL;
6631 }
6632
6633 const isCurrentlyMinimax = main_api === 'openai' && oai_settings.chat_completion_source === chat_completion_sources.MINIMAX;
6634 if (api === chat_completion_sources.MINIMAX || (!api && isCurrentlyMinimax)) {
6635 if (!url) {
6636 return oai_settings.minimax_endpoint || MINIMAX_ENDPOINT.GLOBAL;
6637 }
6638
6639 const permittedValues = Object.values(MINIMAX_ENDPOINT);
6640 if (!permittedValues.includes(url)) {
6641 !isQuiet && toastr.warning(t`Valid options are: ${permittedValues.join(', ')}`, t`MiniMax endpoint '${url}' is not a valid option.`);
6642 return '';
6643 }
6644
6645 if (!isCurrentlyMinimax && autoConnect) {
6646 toastr.warning(t`MiniMax is not the currently selected API, so we cannot do an auto-connect. Consider switching to it via /api beforehand.`);
6647 return '';
6648 }
6649
6650 $('#minimax_endpoint').val(url).trigger('input');
6651
6652 if (autoConnect) {
6653 $('#api_button_openai').trigger('click');
6654 }
6655
6656 return oai_settings.minimax_endpoint || MINIMAX_ENDPOINT.GLOBAL;
6657 }
6658
6659 const isCurrentlyVertexAI = main_api === 'openai' && oai_settings.chat_completion_source === chat_completion_sources.VERTEXAI;
6660 if (api === chat_completion_sources.VERTEXAI || (!api && isCurrentlyVertexAI)) {
6661 const defaultRegion = 'us-central1';
6662 const permittedValues = Array
6663 .from(document.querySelectorAll('#vertexai_region_suggestions option'))
6664 .map(e => e instanceof HTMLOptionElement ? e.value : '')
6665 .filter(x => x);
6666
6667 if (!url) {
6668 return oai_settings.vertexai_region || defaultRegion;
6669 }
6670
6671 if (!permittedValues.includes(url)) {
6672 !isQuiet && toastr.info(t`Generation requests may fail.`, t`Unknown VertexAI region '${url}'`);
6673 }
6674
6675 if (!isCurrentlyVertexAI && autoConnect) {
6676 toastr.warning(t`VertexAI is not the currently selected API, so we cannot do an auto-connect. Consider switching to it via /api beforehand.`);
6677 return '';
6678 }
6679
6680 $('#vertexai_region').val(url).trigger('input');
6681
6682 if (autoConnect) {
6683 $('#api_button_openai').trigger('click');
6684 }
6685
6686 return oai_settings.vertexai_region || defaultRegion;
6687 }
6688
6689 // Special handling for Kobold Classic API
6690 const isCurrentlyKoboldClassic = main_api === 'kobold';
6691 if (api === 'kobold' || (!api && isCurrentlyKoboldClassic)) {
6692 if (!url) {
6693 return kai_settings.api_server ?? '';
6694 }
6695
6696 if (!isCurrentlyKoboldClassic && autoConnect) {
6697 toastr.warning(t`Kobold Classic API is not the currently selected API, so we cannot do an auto-connect. Consider switching to it via /api beforehand.`);
6698 return '';
6699 }
6700
6701 $('#api_url_text').val(url).trigger('input');
6702 // trigger blur debounced, so we hide the autocomplete menu
6703 setTimeout(() => $('#api_url_text').trigger('blur'), 1);
6704
6705 if (autoConnect) {
6706 $('#api_button').trigger('click');
6707 }
6708
6709 return kai_settings.api_server ?? '';
6710 }
6711
6712 // Do some checks and get the api type we are targeting with this command
6713 if (api && !Object.values(textgen_types).includes(api)) {
6714 !isQuiet && toastr.warning(t`API '${api}' is not a valid text_gen API.`);
6715 return '';
6716 }
6717 if (!api && !Object.values(textgen_types).includes(textgenerationwebui_settings.type)) {
6718 !isQuiet && toastr.warning(t`API '${textgenerationwebui_settings.type}' is not a valid text_gen API.`);
6719 return '';
6720 }
6721 if (!api && main_api !== 'textgenerationwebui') {
6722 !isQuiet && toastr.warning(t`API type '${main_api}' does not support setting the server URL.`);
6723 return '';
6724 }
6725 if (api && url && autoConnect && api !== textgenerationwebui_settings.type) {
6726 !isQuiet && toastr.warning(t`API '${api}' is not the currently selected API, so we cannot do an auto-connect. Consider switching to it via /api beforehand.`);
6727 return '';
6728 }
6729 const type = api || textgenerationwebui_settings.type;
6730
6731 const inputSelector = SERVER_INPUTS[type];
6732 if (!inputSelector) {
6733 !isQuiet && toastr.warning(t`API '${type}' does not have a server url input.`);
6734 return '';
6735 }
6736
6737 // If no url was provided, return the current one
6738 if (!url) {
6739 return textgenerationwebui_settings.server_urls[type] ?? '';
6740 }
6741
6742 // else, we want to actually set the url
6743 $(inputSelector).val(url).trigger('input');
6744 // trigger blur debounced, so we hide the autocomplete menu
6745 setTimeout(() => $(inputSelector).trigger('blur'), 1);
6746
6747 // Trigger the auto connect via connect button, if requested
6748 if (autoConnect) {
6749 $('#api_button_textgenerationwebui').trigger('click');
6750 }
6751
6752 // We still re-acquire the value, as it might have been modified by the validation on connect
6753 return textgenerationwebui_settings.server_urls[type] ?? '';
6754}
6755
6756async function selectTokenizerCallback(_, name) {
6757 if (!name) {
6758 return getAvailableTokenizers().find(tokenizer => tokenizer.tokenizerId === power_user.tokenizer)?.tokenizerKey ?? '';
6759 }
6760
6761 const tokenizers = getAvailableTokenizers();
6762 const fuse = new Fuse(tokenizers, { keys: ['tokenizerKey', 'tokenizerName'] });
6763 const result = fuse.search(name);
6764
6765 if (result.length === 0) {
6766 toastr.warning(t`Tokenizer "${name}" not found`);
6767 return '';
6768 }
6769
6770 /** @type {import('./tokenizers.js').Tokenizer} */
6771 const foundTokenizer = result[0].item;
6772 selectTokenizer(foundTokenizer.tokenizerId);
6773
6774 return foundTokenizer.tokenizerKey;
6775}
6776
6777export let isExecutingCommandsFromChatInput = false;
6778export let commandsFromChatInputAbortController;
6779
6780/**
6781 * Show command execution pause/stop buttons next to chat input.
6782 */
6783export function activateScriptButtons() {
6784 document.querySelector('#form_sheld').classList.add('isExecutingCommandsFromChatInput');
6785}
6786
6787/**
6788 * Hide command execution pause/stop buttons next to chat input.
6789 */
6790export function deactivateScriptButtons() {
6791 document.querySelector('#form_sheld').classList.remove('isExecutingCommandsFromChatInput');
6792}
6793
6794/**
6795 * Toggle pause/continue command execution. Only for commands executed via chat input.
6796 */
6797export function pauseScriptExecution() {
6798 if (commandsFromChatInputAbortController) {
6799 if (commandsFromChatInputAbortController.signal.paused) {
6800 commandsFromChatInputAbortController.continue('Clicked pause button');
6801 document.querySelector('#form_sheld').classList.remove('script_paused');
6802 } else {
6803 commandsFromChatInputAbortController.pause('Clicked pause button');
6804 document.querySelector('#form_sheld').classList.add('script_paused');
6805 }
6806 }
6807}
6808
6809/**
6810 * Stop command execution. Only for commands executed via chat input.
6811 */
6812export function stopScriptExecution() {
6813 commandsFromChatInputAbortController?.abort('Clicked stop button');
6814}
6815
6816/**
6817 * Clear up command execution progress bar above chat input.
6818 * @returns Promise<void>
6819 */
6820async function clearCommandProgress() {
6821 if (isExecutingCommandsFromChatInput) return;
6822 const ta = document.getElementById('send_textarea');
6823 const fs = document.getElementById('form_sheld');
6824 if (!ta || !fs) return;
6825 ta.style.setProperty('--progDone', '1');
6826 await delay(250);
6827 if (isExecutingCommandsFromChatInput) return;
6828 ta.style.transition = 'none';
6829 await delay(1);
6830 ta.style.setProperty('--prog', '0%');
6831 ta.style.setProperty('--progDone', '0');
6832 fs.classList.remove('script_success');
6833 fs.classList.remove('script_error');
6834 fs.classList.remove('script_aborted');
6835 await delay(1);
6836 ta.style.transition = null;
6837}
6838/**
6839 * Debounced version of clearCommandProgress.
6840 */
6841const clearCommandProgressDebounced = debounce(clearCommandProgress);
6842
6843/**
6844 * @typedef ExecuteSlashCommandsOptions
6845 * @prop {boolean} [handleParserErrors] (true) Whether to handle parser errors (show toast on error) or throw.
6846 * @prop {SlashCommandScope} [scope] (null) The scope to be used when executing the commands.
6847 * @prop {boolean} [handleExecutionErrors] (false) Whether to handle execution errors (show toast on error) or throw
6848 * @prop {import('./slash-commands/SlashCommandParser.js').ParserFlags} [parserFlags] (null) Parser flags to apply
6849 * @prop {SlashCommandAbortController} [abortController] (null) Controller used to abort or pause command execution
6850 * @prop {SlashCommandDebugController} [debugController] (null) Controller used to control debug execution
6851 * @prop {(done:number, total:number)=>void} [onProgress] (null) Callback to handle progress events
6852 * @prop {string} [source] (null) String indicating where the code come from (e.g., QR name)
6853 */
6854
6855/**
6856 * @typedef ExecuteSlashCommandsOnChatInputOptions
6857 * @prop {SlashCommandScope} [scope] (null) The scope to be used when executing the commands.
6858 * @prop {import('./slash-commands/SlashCommandParser.js').ParserFlags} [parserFlags] (null) Parser flags to apply
6859 * @prop {boolean} [clearChatInput] (false) Whether to clear the chat input textarea
6860 * @prop {string} [source] (null) String indicating where the code come from (e.g., QR name)
6861 */
6862
6863/**
6864 * Execute slash commands while showing progress indicator and pause/stop buttons on
6865 * chat input.
6866 * @param {string} text Slash command text
6867 * @param {ExecuteSlashCommandsOnChatInputOptions} options
6868 */
6869export async function executeSlashCommandsOnChatInput(text, options = {}) {
6870 if (isExecutingCommandsFromChatInput) return null;
6871
6872 options = Object.assign({
6873 scope: null,
6874 parserFlags: null,
6875 clearChatInput: false,
6876 source: null,
6877 }, options);
6878
6879 isExecutingCommandsFromChatInput = true;
6880 commandsFromChatInputAbortController?.abort('processCommands was called');
6881 activateScriptButtons();
6882
6883 /** @type {HTMLTextAreaElement} */
6884 const ta = document.querySelector('#send_textarea');
6885 const fs = document.querySelector('#form_sheld');
6886
6887 if (options.clearChatInput) {
6888 ta.value = '';
6889 ta.dispatchEvent(new Event('input', { bubbles: true }));
6890 }
6891
6892 ta.style.setProperty('--prog', '0%');
6893 ta.style.setProperty('--progDone', '0');
6894 fs.classList.remove('script_success');
6895 fs.classList.remove('script_error');
6896 fs.classList.remove('script_aborted');
6897
6898 /**@type {SlashCommandClosureResult} */
6899 let result = null;
6900 let currentProgress = 0;
6901 try {
6902 commandsFromChatInputAbortController = new SlashCommandAbortController();
6903 result = await executeSlashCommandsWithOptions(text, {
6904 abortController: commandsFromChatInputAbortController,
6905 onProgress: (done, total) => {
6906 const newProgress = done / total;
6907 if (newProgress > currentProgress) {
6908 currentProgress = newProgress;
6909 ta.style.setProperty('--prog', `${newProgress * 100}%`);
6910 }
6911 },
6912 parserFlags: options.parserFlags,
6913 scope: options.scope,
6914 source: options.source,
6915 });
6916 if (commandsFromChatInputAbortController.signal.aborted) {
6917 document.querySelector('#form_sheld').classList.add('script_aborted');
6918 } else {
6919 document.querySelector('#form_sheld').classList.add('script_success');
6920 }
6921 } catch (e) {
6922 document.querySelector('#form_sheld').classList.add('script_error');
6923 result = new SlashCommandClosureResult();
6924 result.isError = true;
6925 result.errorMessage = e.message || t`An unknown error occurred`;
6926 if (e.cause !== 'abort') {
6927 if (e instanceof SlashCommandExecutionError) {
6928 /**@type {SlashCommandExecutionError}*/
6929 const ex = e;
6930 const toast = `
6931 <div>${ex.message}</div>
6932 <div>${t`Line`}: ${ex.line} ${t`Column`}: ${ex.column}</div>
6933 <pre style="text-align:left;">${ex.hint}</pre>
6934 `;
6935 const clickHint = `<p>${t`Click to see details`}</p>`;
6936 toastr.error(
6937 `${toast}${clickHint}`,
6938 'Slash Command Execution Error',
6939 { escapeHtml: false, timeOut: 10000, onclick: () => callGenericPopup(toast, POPUP_TYPE.TEXT, '', { allowHorizontalScrolling: true, allowVerticalScrolling: true }) },
6940 );
6941 } else {
6942 toastr.error(result.errorMessage);
6943 }
6944 }
6945 } finally {
6946 delay(1000).then(() => clearCommandProgressDebounced());
6947
6948 commandsFromChatInputAbortController = null;
6949 deactivateScriptButtons();
6950 isExecutingCommandsFromChatInput = false;
6951 }
6952 return result;
6953}
6954
6955/**
6956 *
6957 * @param {string} text Slash command text
6958 * @param {ExecuteSlashCommandsOptions} [options]
6959 * @returns {Promise<SlashCommandClosureResult>}
6960 */
6961async function executeSlashCommandsWithOptions(text, options = {}) {
6962 if (!text) {
6963 return null;
6964 }
6965 options = Object.assign({
6966 handleParserErrors: true,
6967 scope: null,
6968 handleExecutionErrors: false,
6969 parserFlags: null,
6970 abortController: null,
6971 debugController: null,
6972 onProgress: null,
6973 source: null,
6974 }, options);
6975
6976 let closure;
6977 try {
6978 closure = parser.parse(text, true, options.parserFlags, options.abortController ?? new SlashCommandAbortController());
6979 closure.scope.parent = options.scope;
6980 closure.onProgress = options.onProgress;
6981 closure.debugController = options.debugController;
6982 closure.source = options.source;
6983 } catch (e) {
6984 if (options.handleParserErrors && e instanceof SlashCommandParserError) {
6985 /**@type {SlashCommandParserError}*/
6986 const ex = e;
6987 const toast = `
6988 <div>${ex.message}</div>
6989 <div>${t`Line`}: ${ex.line} ${t`Column`}: ${ex.column}</div>
6990 <pre style="text-align:left;">${ex.hint}</pre>
6991 `;
6992 const clickHint = `<p>${t`Click to see details`}</p>`;
6993 toastr.error(
6994 `${toast}${clickHint}`,
6995 'SlashCommandParserError',
6996 { escapeHtml: false, timeOut: 10000, onclick: () => callGenericPopup(toast, POPUP_TYPE.TEXT, '', { allowHorizontalScrolling: true, allowVerticalScrolling: true }) },
6997 );
6998 const result = new SlashCommandClosureResult();
6999 return result;
7000 } else {
7001 throw e;
7002 }
7003 }
7004
7005 try {
7006 const result = await closure.execute();
7007 if (result.isAborted && !result.isQuietlyAborted) {
7008 toastr.warning(result.abortReason, t`Command execution aborted`);
7009 closure.abortController.signal.isQuiet = true;
7010 }
7011 return result;
7012 } catch (e) {
7013 if (options.handleExecutionErrors) {
7014 if (e instanceof SlashCommandExecutionError) {
7015 /**@type {SlashCommandExecutionError}*/
7016 const ex = e;
7017 const toast = `
7018 <div>${ex.message}</div>
7019 <div>Line: ${ex.line} Column: ${ex.column}</div>
7020 <pre style="text-align:left;">${ex.hint}</pre>
7021 `;
7022 const clickHint = '<p>Click to see details</p>';
7023 toastr.error(
7024 `${toast}${clickHint}`,
7025 'SlashCommandExecutionError',
7026 { escapeHtml: false, timeOut: 10000, onclick: () => callGenericPopup(toast, POPUP_TYPE.TEXT, '', { allowHorizontalScrolling: true, allowVerticalScrolling: true }) },
7027 );
7028 } else {
7029 toastr.error(e.message);
7030 }
7031 const result = new SlashCommandClosureResult();
7032 result.isError = true;
7033 result.errorMessage = e.message;
7034 return result;
7035 } else {
7036 throw e;
7037 }
7038 }
7039}
7040/**
7041 * Executes slash commands in the provided text
7042 * @deprecated Use executeSlashCommandWithOptions instead
7043 * @param {string} text Slash command text
7044 * @param {boolean} handleParserErrors Whether to handle parser errors (show toast on error) or throw
7045 * @param {SlashCommandScope} scope The scope to be used when executing the commands.
7046 * @param {boolean} handleExecutionErrors Whether to handle execution errors (show toast on error) or throw
7047 * @param {{[id:import('./slash-commands/SlashCommandParser.js').PARSER_FLAG]:boolean}} parserFlags Parser flags to apply
7048 * @param {SlashCommandAbortController} abortController Controller used to abort or pause command execution
7049 * @param {(done:number, total:number)=>void} onProgress Callback to handle progress events
7050 * @returns {Promise<SlashCommandClosureResult>}
7051 */
7052async function executeSlashCommands(text, handleParserErrors = true, scope = null, handleExecutionErrors = false, parserFlags = null, abortController = null, onProgress = null) {
7053 return executeSlashCommandsWithOptions(text, {
7054 handleParserErrors,
7055 scope,
7056 handleExecutionErrors,
7057 parserFlags,
7058 abortController,
7059 onProgress,
7060 });
7061}
7062
7063/**
7064 *
7065 * @param {HTMLTextAreaElement} textarea The textarea to receive autocomplete
7066 * @param {Boolean} isFloating Whether to show the auto complete as a floating window (e.g., large QR editor)
7067 * @returns {Promise<AutoComplete>}
7068 */
7069export async function setSlashCommandAutoComplete(textarea, isFloating = false) {
7070 if (!canUseNegativeLookbehind()) {
7071 console.warn('Cannot use negative lookbehind in this browser');
7072 return;
7073 }
7074
7075 const parser = new SlashCommandParser();
7076 const ac = new AutoComplete(
7077 textarea,
7078 () => ac.text[0] == '/' && (power_user.stscript.autocomplete.state === AUTOCOMPLETE_STATE.ALWAYS || power_user.stscript.autocomplete.state === AUTOCOMPLETE_STATE.MIN_LENGTH && ac.text.length > 2),
7079 async (text, index) => await parser.getNameAt(text, index),
7080 isFloating,
7081 );
7082 return ac;
7083}
7084
7085export async function initSlashCommandAutoComplete() {
7086 const sendTextarea = /** @type {HTMLTextAreaElement} */ (document.querySelector('#send_textarea'));
7087 setSlashCommandAutoComplete(sendTextarea);
7088 sendTextarea.addEventListener('input', () => {
7089 if (sendTextarea.value && sendTextarea.value[0] == '/') {
7090 sendTextarea.style.fontFamily = 'var(--monoFontFamily, monospace)';
7091 } else {
7092 sendTextarea.style.fontFamily = null;
7093 }
7094 });
7095}