Merge branch 'staging' into translation-improvements

fcc00e0b263bcdea477e45dfecce36d2df28d0fa

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

10 files changed, +1509 -986Ignore whitespace
jsconfig.json+6 -1
@@ -15,6 +15,11 @@
1515 "**/node_modules/*",
1616 "public/lib",
1717 "backups/*",
1818 "data/*",
19+ "**/dist/*",
20+ "dist/*",
21+ "cache/*",
22+ "src/tokenizers/*",
23+ "docker/*",
1924 ]
2025}
public/script.js+2 -1
@@ -159,7 +159,7 @@ import {
159159import { debounce_timeout } from './scripts/constants.js';
160160
161161import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, loadExtensionSettings, renderExtensionTemplate, renderExtensionTemplateAsync, runGenerationInterceptors, saveMetadataDebounced, writeExtensionField } from './scripts/extensions.js';
162162import { COMMENT_NAME_DEFAULT, executeSlashCommands, executeSlashCommandsOnChatInput, getSlashCommandsHelp, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommands, registerSlashCommand, stopScriptExecution } from './scripts/slash-commands.js';
163163import {
164164 tag_map,
165165 tags,
@@ -911,6 +911,7 @@ async function firstLoadInit() {
911911 initKeyboard();
912912 initDynamicStyles();
913913 initTags();
914+ initDefaultSlashCommands();
914915 await getUserAvatars(true, user_avatar);
915916 await getCharacters();
916917 await getBackgrounds();
public/scripts/extensions/caption/index.js+1 -0
@@ -356,6 +356,7 @@ jQuery(async function () {
356356 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'llamacpp' && textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]) ||
357357 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ooba' && textgenerationwebui_settings.server_urls[textgen_types.OOBA]) ||
358358 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'koboldcpp' && textgenerationwebui_settings.server_urls[textgen_types.KOBOLDCPP]) ||
359+ (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'vllm' && textgenerationwebui_settings.server_urls[textgen_types.VLLM]) ||
359360 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'custom') ||
360361 extension_settings.caption.source === 'local' ||
361362 extension_settings.caption.source === 'horde';
public/scripts/extensions/caption/settings.html+2 -0
@@ -26,6 +26,7 @@
2626 <option value="openai">OpenAI</option>
2727 <option value="openrouter">OpenRouter</option>
2828 <option value="ooba" data-i18n="Text Generation WebUI (oobabooga)">Text Generation WebUI (oobabooga)</option>
29+ <option value="vllm">vLLM</option>
2930 </select>
3031 </div>
3132 <div class="flex1 flex-container flexFlowColumn flexNoGap">
@@ -66,6 +67,7 @@
6667 <option data-type="llamacpp" value="llamacpp_current" data-i18n="currently_loaded">[Currently loaded]</option>
6768 <option data-type="ooba" value="ooba_current" data-i18n="currently_loaded">[Currently loaded]</option>
6869 <option data-type="koboldcpp" value="koboldcpp_current" data-i18n="currently_loaded">[Currently loaded]</option>
70+ <option data-type="vllm" value="vllm_current" data-i18n="currently_selected">[Currently selected]</option>
6971 <option data-type="custom" value="custom_current" data-i18n="currently_selected">[Currently selected]</option>
7072 </select>
7173 </div>
public/scripts/extensions/shared.js+17 -0
@@ -34,6 +34,7 @@ export async function getMultimodalCaption(base64Img, prompt) {
3434 const isCustom = extension_settings.caption.multimodal_api === 'custom';
3535 const isOoba = extension_settings.caption.multimodal_api === 'ooba';
3636 const isKoboldCpp = extension_settings.caption.multimodal_api === 'koboldcpp';
37+ const isVllm = extension_settings.caption.multimodal_api === 'vllm';
3738 const base64Bytes = base64Img.length * 0.75;
3839 const compressionLimit = 2 * 1024 * 1024;
3940 if ((['google', 'openrouter'].includes(extension_settings.caption.multimodal_api) && base64Bytes > compressionLimit) || isOoba || isKoboldCpp) {
@@ -65,6 +66,14 @@ export async function getMultimodalCaption(base64Img, prompt) {
6566 requestBody.server_url = textgenerationwebui_settings.server_urls[textgen_types.OLLAMA];
6667 }
6768
69+ if (isVllm) {
70+ if (extension_settings.caption.multimodal_model === 'vllm_current') {
71+ requestBody.model = textgenerationwebui_settings.vllm_model;
72+ }
73+
74+ requestBody.server_url = textgenerationwebui_settings.server_urls[textgen_types.VLLM];
75+ }
76+
6877 if (isLlamaCpp) {
6978 requestBody.server_url = textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP];
7079 }
@@ -151,6 +160,14 @@ function throwIfInvalidModel(useReverseProxy) {
151160 throw new Error('KoboldCpp server URL is not set.');
152161 }
153162
163+ if (extension_settings.caption.multimodal_api === 'vllm' && !textgenerationwebui_settings.server_urls[textgen_types.VLLM]) {
164+ throw new Error('vLLM server URL is not set.');
165+ }
166+
167+ if (extension_settings.caption.multimodal_api === 'vllm' && extension_settings.caption.multimodal_model === 'vllm_current' && !textgenerationwebui_settings.vllm_model) {
168+ throw new Error('vLLM model is not set.');
169+ }
170+
154171 if (extension_settings.caption.multimodal_api === 'custom' && !oai_settings.custom_url) {
155172 throw new Error('Custom API URL is not set.');
156173 }
public/scripts/extensions/tts/index.js+2 -0
@@ -10,6 +10,7 @@ import { NovelTtsProvider } from './novel.js';
1010import { power_user } from '../../power-user.js';
1111import { OpenAITtsProvider } from './openai.js';
1212import { XTTSTtsProvider } from './xtts.js';
13+import { VITSTtsProvider } from './vits.js';
1314import { GSVITtsProvider } from './gsvi.js';
1415import { SBVits2TtsProvider } from './sbvits2.js';
1516import { AllTalkTtsProvider } from './alltalk.js';
@@ -83,6 +84,7 @@ const ttsProviders = {
8384 ElevenLabs: ElevenLabsTtsProvider,
8485 Silero: SileroTtsProvider,
8586 XTTSv2: XTTSTtsProvider,
87+ VITS: VITSTtsProvider,
8688 GSVI: GSVITtsProvider,
8789 SBVits2: SBVits2TtsProvider,
8890 System: SystemTtsProvider,
public/scripts/extensions/tts/vits.js+404 -0
@@ -0,0 +1,404 @@
1+import { getPreviewString, saveTtsProviderSettings } from './index.js';
2+
3+export { VITSTtsProvider };
4+
5+class VITSTtsProvider {
6+ //########//
7+ // Config //
8+ //########//
9+
10+ settings;
11+ ready = false;
12+ voices = [];
13+ separator = '. ';
14+ audioElement = document.createElement('audio');
15+
16+ /**
17+ * Perform any text processing before passing to TTS engine.
18+ * @param {string} text Input text
19+ * @returns {string} Processed text
20+ */
21+ processText(text) {
22+ return text;
23+ }
24+
25+ audioFormats = ['wav', 'ogg', 'silk', 'mp3', 'flac'];
26+
27+ languageLabels = {
28+ 'Auto': 'auto',
29+ 'Chinese': 'zh',
30+ 'English': 'en',
31+ 'Japanese': 'ja',
32+ 'Korean': 'ko',
33+ };
34+
35+ langKey2LangCode = {
36+ 'zh': 'zh-CN',
37+ 'en': 'en-US',
38+ 'ja': 'ja-JP',
39+ 'ko': 'ko-KR',
40+ };
41+
42+ modelTypes = {
43+ VITS: 'VITS',
44+ W2V2_VITS: 'W2V2-VITS',
45+ BERT_VITS2: 'BERT-VITS2',
46+ };
47+
48+ defaultSettings = {
49+ provider_endpoint: 'http://localhost:23456',
50+ format: 'wav',
51+ lang: 'auto',
52+ length: 1.0,
53+ noise: 0.33,
54+ noisew: 0.4,
55+ segment_size: 50,
56+ streaming: false,
57+ dim_emotion: 0,
58+ sdp_ratio: 0.2,
59+ emotion: 0,
60+ text_prompt: '',
61+ style_text: '',
62+ style_weight: 1,
63+ };
64+
65+ get settingsHtml() {
66+ let html = `
67+ <label for="vits_lang">Text Language</label>
68+ <select id="vits_lang">`;
69+
70+ for (let language in this.languageLabels) {
71+ if (this.languageLabels[language] == this.settings?.lang) {
72+ html += `<option value="${this.languageLabels[language]}" selected="selected">${language}</option>`;
73+ continue;
74+ }
75+ html += `<option value="${this.languageLabels[language]}">${language}</option>`;
76+ }
77+
78+ html += `
79+ </select>
80+ <label>VITS / W2V2-VITS / Bert-VITS2 Settings:</label><br/>
81+ <label for="vits_endpoint">Provider Endpoint:</label>
82+ <input id="vits_endpoint" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.provider_endpoint}"/>
83+ <span>Use <a target="_blank" href="https://github.com/Artrajz/vits-simple-api">vits-simple-api</a>.</span><br/>
84+
85+ <label for="vits_format">Audio format:</label>
86+ <select id="vits_format">`;
87+
88+ for (let format of this.audioFormats) {
89+ if (format == this.settings?.format) {
90+ html += `<option value="${format}" selected="selected">${format}</option>`;
91+ continue;
92+ }
93+ html += `<option value="${format}">${format}</option>`;
94+ }
95+
96+ html += `
97+ </select>
98+ <label for="vits_length">Audio length: <span id="vits_length_output">${this.defaultSettings.length}</span></label>
99+ <input id="vits_length" type="range" value="${this.defaultSettings.length}" min="0.0" max="5" step="0.01" />
100+
101+ <label for="vits_noise">Noise: <span id="vits_noise_output">${this.defaultSettings.noise}</span></label>
102+ <input id="vits_noise" type="range" value="${this.defaultSettings.noise}" min="0.1" max="2" step="0.01" />
103+
104+ <label for="vits_noisew">SDP noise: <span id="vits_noisew_output">${this.defaultSettings.noisew}</span></label>
105+ <input id="vits_noisew" type="range" value="${this.defaultSettings.noisew}" min="0.1" max="2" step="0.01" />
106+
107+ <label for="vits_segment_size">Segment Size: <span id="vits_segment_size_output">${this.defaultSettings.segment_size}</span></label>
108+ <input id="vits_segment_size" type="range" value="${this.defaultSettings.segment_size}" min="0" max="1000" step="1" />
109+
110+ <label for="vits_streaming" class="checkbox_label">
111+ <input id="vits_streaming" type="checkbox" />
112+ <span>Streaming</span>
113+ </label>
114+
115+ <label>W2V2-VITS Settings:</label><br/>
116+ <label for="vits_dim_emotion">Dimensional emotion:</label>
117+ <input id="vits_dim_emotion" type="number" class="text_pole" min="0" max="5457" step="1" value="${this.defaultSettings.dim_emotion}"/>
118+
119+ <label>BERT-VITS2 Settings:</label><br/>
120+ <label for="vits_sdp_ratio">sdp_ratio: <span id="vits_sdp_ratio_output">${this.defaultSettings.sdp_ratio}</span></label>
121+ <input id="vits_sdp_ratio" type="range" value="${this.defaultSettings.sdp_ratio}" min="0.0" max="1" step="0.01" />
122+
123+ <label for="vits_emotion">emotion: <span id="vits_emotion_output">${this.defaultSettings.emotion}</span></label>
124+ <input id="vits_emotion" type="range" value="${this.defaultSettings.emotion}" min="0" max="9" step="1" />
125+
126+ <label for="vits_text_prompt">Text Prompt:</label>
127+ <input id="vits_text_prompt" type="text" class="text_pole" maxlength="512" value="${this.defaultSettings.text_prompt}"/>
128+
129+ <label for="vits_style_text">Style text:</label>
130+ <input id="vits_style_text" type="text" class="text_pole" maxlength="512" value="${this.defaultSettings.style_text}"/>
131+
132+ <label for="vits_style_weight">Style weight <span id="vits_style_weight_output">${this.defaultSettings.style_weight}</span></label>
133+ <input id="vits_style_weight" type="range" value="${this.defaultSettings.style_weight}" min="0" max="1" step="0.01" />
134+ `;
135+
136+ return html;
137+ }
138+
139+ onSettingsChange() {
140+ // Used when provider settings are updated from UI
141+ this.settings.provider_endpoint = $('#vits_endpoint').val();
142+ this.settings.lang = $('#vits_lang').val();
143+ this.settings.format = $('#vits_format').val();
144+ this.settings.dim_emotion = $('#vits_dim_emotion').val();
145+ this.settings.text_prompt = $('#vits_text_prompt').val();
146+ this.settings.style_text = $('#vits_style_text').val();
147+
148+ // Update the default TTS settings based on input fields
149+ this.settings.length = $('#vits_length').val();
150+ this.settings.noise = $('#vits_noise').val();
151+ this.settings.noisew = $('#vits_noisew').val();
152+ this.settings.segment_size = $('#vits_segment_size').val();
153+ this.settings.streaming = $('#vits_streaming').is(':checked');
154+ this.settings.sdp_ratio = $('#vits_sdp_ratio').val();
155+ this.settings.emotion = $('#vits_emotion').val();
156+ this.settings.style_weight = $('#vits_style_weight').val();
157+
158+ // Update the UI to reflect changes
159+ $('#vits_length_output').text(this.settings.length);
160+ $('#vits_noise_output').text(this.settings.noise);
161+ $('#vits_noisew_output').text(this.settings.noisew);
162+ $('#vits_segment_size_output').text(this.settings.segment_size);
163+ $('#vits_sdp_ratio_output').text(this.settings.sdp_ratio);
164+ $('#vits_emotion_output').text(this.settings.emotion);
165+ $('#vits_style_weight_output').text(this.settings.style_weight);
166+
167+ saveTtsProviderSettings();
168+ this.changeTTSSettings();
169+ }
170+
171+ async loadSettings(settings) {
172+ // Pupulate Provider UI given input settings
173+ if (Object.keys(settings).length == 0) {
174+ console.info('Using default TTS Provider settings');
175+ }
176+
177+ // Only accept keys defined in defaultSettings
178+ this.settings = this.defaultSettings;
179+
180+ for (const key in settings) {
181+ if (key in this.settings) {
182+ this.settings[key] = settings[key];
183+ } else {
184+ console.debug(`Ignoring non-user-configurable setting: ${key}`);
185+ }
186+ }
187+
188+ // Set initial values from the settings
189+ $('#vits_endpoint').val(this.settings.provider_endpoint);
190+ $('#vits_lang').val(this.settings.lang);
191+ $('#vits_format').val(this.settings.format);
192+ $('#vits_length').val(this.settings.length);
193+ $('#vits_noise').val(this.settings.noise);
194+ $('#vits_noisew').val(this.settings.noisew);
195+ $('#vits_segment_size').val(this.settings.segment_size);
196+ $('#vits_streaming').prop('checked', this.settings.streaming);
197+ $('#vits_dim_emotion').val(this.settings.dim_emotion);
198+ $('#vits_sdp_ratio').val(this.settings.sdp_ratio);
199+ $('#vits_emotion').val(this.settings.emotion);
200+ $('#vits_text_prompt').val(this.settings.text_prompt);
201+ $('#vits_style_text').val(this.settings.style_text);
202+ $('#vits_style_weight').val(this.settings.style_weight);
203+
204+ // Update the UI to reflect changes
205+ $('#vits_length_output').text(this.settings.length);
206+ $('#vits_noise_output').text(this.settings.noise);
207+ $('#vits_noisew_output').text(this.settings.noisew);
208+ $('#vits_segment_size_output').text(this.settings.segment_size);
209+ $('#vits_sdp_ratio_output').text(this.settings.sdp_ratio);
210+ $('#vits_emotion_output').text(this.settings.emotion);
211+ $('#vits_style_weight_output').text(this.settings.style_weight);
212+
213+ // Register input/change event listeners to update settings on user interaction
214+ $('#vits_endpoint').on('input', () => { this.onSettingsChange(); });
215+ $('#vits_lang').on('change', () => { this.onSettingsChange(); });
216+ $('#vits_format').on('change', () => { this.onSettingsChange(); });
217+ $('#vits_length').on('change', () => { this.onSettingsChange(); });
218+ $('#vits_noise').on('change', () => { this.onSettingsChange(); });
219+ $('#vits_noisew').on('change', () => { this.onSettingsChange(); });
220+ $('#vits_segment_size').on('change', () => { this.onSettingsChange(); });
221+ $('#vits_streaming').on('change', () => { this.onSettingsChange(); });
222+ $('#vits_dim_emotion').on('change', () => { this.onSettingsChange(); });
223+ $('#vits_sdp_ratio').on('change', () => { this.onSettingsChange(); });
224+ $('#vits_emotion').on('change', () => { this.onSettingsChange(); });
225+ $('#vits_text_prompt').on('change', () => { this.onSettingsChange(); });
226+ $('#vits_style_text').on('change', () => { this.onSettingsChange(); });
227+ $('#vits_style_weight').on('change', () => { this.onSettingsChange(); });
228+
229+ await this.checkReady();
230+
231+ console.info('VITS: Settings loaded');
232+ }
233+
234+ // Perform a simple readiness check by trying to fetch voiceIds
235+ async checkReady() {
236+ await Promise.allSettled([this.fetchTtsVoiceObjects(), this.changeTTSSettings()]);
237+ }
238+
239+ async onRefreshClick() {
240+ return;
241+ }
242+
243+ //#################//
244+ // TTS Interfaces //
245+ //#################//
246+
247+ async getVoice(voiceName) {
248+ if (this.voices.length == 0) {
249+ this.voices = await this.fetchTtsVoiceObjects();
250+ }
251+ const match = this.voices.filter(
252+ v => v.name == voiceName,
253+ )[0];
254+ if (!match) {
255+ throw `TTS Voice name ${voiceName} not found`;
256+ }
257+ return match;
258+ }
259+
260+ async getVoiceById(voiceId) {
261+ if (this.voices.length == 0) {
262+ this.voices = await this.fetchTtsVoiceObjects();
263+ }
264+ const match = this.voices.filter(
265+ v => v.voice_id == voiceId,
266+ )[0];
267+ if (!match) {
268+ throw `TTS Voice id ${voiceId} not found`;
269+ }
270+ return match;
271+ }
272+
273+ async generateTts(text, voiceId) {
274+ const response = await this.fetchTtsGeneration(text, voiceId);
275+ return response;
276+ }
277+
278+ //###########//
279+ // API CALLS //
280+ //###########//
281+ async fetchTtsVoiceObjects() {
282+ const response = await fetch(`${this.settings.provider_endpoint}/voice/speakers`);
283+ if (!response.ok) {
284+ throw new Error(`HTTP ${response.status}: ${await response.json()}`);
285+ }
286+ const jsonData = await response.json();
287+ const voices = [];
288+
289+ const addVoices = (modelType) => {
290+ jsonData[modelType].forEach(voice => {
291+ voices.push({
292+ name: `[${modelType}] ${voice.name} (${voice.lang})`,
293+ voice_id: `${modelType}&${voice.id}`,
294+ preview_url: false,
295+ lang: voice.lang,
296+ });
297+ });
298+ };
299+ for (const key in this.modelTypes) {
300+ addVoices(this.modelTypes[key]);
301+ }
302+
303+ this.voices = voices; // Assign to the class property
304+ return voices; // Also return this list
305+ }
306+
307+ // Each time a parameter is changed, we change the configuration
308+ async changeTTSSettings() {
309+ }
310+
311+ /**
312+ * Fetch TTS generation from the API.
313+ * @param {string} inputText Text to generate TTS for
314+ * @param {string} voiceId Voice ID to use (model_type&speaker_id))
315+ * @returns {Promise<Response|string>} Fetch response
316+ */
317+ async fetchTtsGeneration(inputText, voiceId, lang = null, forceNoStreaming = false) {
318+ console.info(`Generating new TTS for voice_id ${voiceId}`);
319+
320+ const streaming = !forceNoStreaming && this.settings.streaming;
321+ const [model_type, speaker_id] = voiceId.split('&');
322+ const params = new URLSearchParams();
323+ params.append('text', inputText);
324+ params.append('id', speaker_id);
325+ if (streaming) {
326+ params.append('streaming', streaming);
327+ // Streaming response only supports MP3
328+ }
329+ else {
330+ params.append('format', this.settings.format);
331+ }
332+ params.append('lang', lang ?? this.settings.lang);
333+ params.append('length', this.settings.length);
334+ params.append('noise', this.settings.noise);
335+ params.append('noisew', this.settings.noisew);
336+ params.append('segment_size', this.settings.segment_size);
337+
338+ if (model_type == this.modelTypes.W2V2_VITS) {
339+ params.append('emotion', this.settings.dim_emotion);
340+ }
341+ else if (model_type == this.modelTypes.BERT_VITS2) {
342+ params.append('sdp_ratio', this.settings.sdp_ratio);
343+ params.append('emotion', this.settings.emotion);
344+ if (this.settings.text_prompt) {
345+ params.append('text_prompt', this.settings.text_prompt);
346+ }
347+ if (this.settings.style_text) {
348+ params.append('style_text', this.settings.style_text);
349+ params.append('style_weight', this.settings.style_weight);
350+ }
351+ }
352+
353+ const url = `${this.settings.provider_endpoint}/voice/${model_type.toLowerCase()}`;
354+
355+ if (streaming) {
356+ return url + `?${params.toString()}`;
357+ }
358+
359+ const response = await fetch(
360+ url,
361+ {
362+ method: 'POST',
363+ headers: {
364+ 'Content-Type': 'application/x-www-form-urlencoded',
365+ },
366+ body: params,
367+ },
368+ );
369+ if (!response.ok) {
370+ toastr.error(response.statusText, 'TTS Generation Failed');
371+ throw new Error(`HTTP ${response.status}: ${await response.text()}`);
372+ }
373+ return response;
374+ }
375+
376+ /**
377+ * Preview TTS for a given voice ID.
378+ * @param {string} id Voice ID
379+ */
380+ async previewTtsVoice(id) {
381+ this.audioElement.pause();
382+ this.audioElement.currentTime = 0;
383+ const voice = await this.getVoiceById(id);
384+ const lang = voice.lang.includes(this.settings.lang) ? this.settings.lang : voice.lang[0];
385+
386+ let lang_code = this.langKey2LangCode[lang];
387+ const text = getPreviewString(lang_code);
388+ const response = await this.fetchTtsGeneration(text, id, lang, true);
389+ if (typeof response != 'string') {
390+ if (!response.ok) {
391+ throw new Error(`HTTP ${response.status}: ${await response.text()}`);
392+ }
393+ const audio = await response.blob();
394+ const url = URL.createObjectURL(audio);
395+ this.audioElement.src = url;
396+ this.audioElement.play();
397+ }
398+ }
399+
400+ // Interface not used
401+ async fetchTtsFromHistory(history_item_id) {
402+ return Promise.resolve(history_item_id);
403+ }
404+}
public/scripts/slash-commands.js+1064 -977
@@ -38,13 +38,13 @@ import {
3838 system_message_types,
3939 this_chid,
4040} from '../script.js';
4141import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
4242import { SlashCommandParserError } from './slash-commands/SlashCommandParserError.js';
4343import { getMessageTimeStamp } from './RossAscends-mods.js';
4444import { hideChatMessageRange } from './chats.js';
4545import { extension_settings, getContext, saveMetadataDebounced } from './extensions.js';
4646import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
4747import { findGroupMemberId, getGroupMembers, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group } from './group-chats.js';
4848import { chat_completion_sources, oai_settings, setupChatCompletionPromptManager } from './openai.js';
4949import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockState, togglePersonaLock, user_avatar } from './personas.js';
5050import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
@@ -53,7 +53,6 @@ import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCoun
5353import { debounce, delay, isFalseBoolean, isTrueBoolean, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
5454import { registerVariableCommands, resolveVariable } from './variables.js';
5555import { background_settings } from './backgrounds.js';
56-import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
5756import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
5857import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js';
5958import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
@@ -75,78 +74,79 @@ export const parser = new SlashCommandParser();
7574const registerSlashCommand = SlashCommandParser.addCommand.bind(SlashCommandParser);
7675const getSlashCommandsHelp = parser.getHelpString.bind(parser);
7776
78-SlashCommandParser.addCommandObject(SlashCommand.fromProps({
77+export function initDefaultSlashCommands() {
79- name: '?',
78+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
80- callback: helpCommandCallback,
79+ name: '?',
81- aliases: ['help'],
80+ callback: helpCommandCallback,
82- unnamedArgumentList: [SlashCommandArgument.fromProps({
81+ aliases: ['help'],
83- description: 'help topic',
82+ unnamedArgumentList: [SlashCommandArgument.fromProps({
84- typeList: [ARGUMENT_TYPE.STRING],
83+ description: 'help topic',
85- enumList: [
86- new SlashCommandEnumValue('slash', 'slash commands (STscript)', enumTypes.command, '/'),
87- new SlashCommandEnumValue('macros', '{{macros}} (text replacement)', enumTypes.macro, enumIcons.macro),
88- new SlashCommandEnumValue('format', 'chat/text formatting', enumTypes.name, '★'),
89- new SlashCommandEnumValue('hotkeys', 'keyboard shortcuts', enumTypes.enum, '⏎'),
90- ],
91- })],
92- helpString: 'Get help on macros, chat formatting and commands.',
93-}));
94-SlashCommandParser.addCommandObject(SlashCommand.fromProps({
95- name: 'persona',
96- callback: setNameCallback,
97- namedArgumentList: [
98- new SlashCommandNamedArgument(
99- 'mode', 'The mode for persona selection. ("lookup" = search for existing persona, "temp" = create a temporary name, set a temporary name, "all" = allow both in the same command)',
100- [ARGUMENT_TYPE.STRING], false, false, 'all', ['lookup', 'temp', 'all'],
101- ),
102- ],
103- unnamedArgumentList: [
104- SlashCommandArgument.fromProps({
105- description: 'persona name',
10684 typeList: [ARGUMENT_TYPE.STRING],
10785 isRequiredenumList: true,[
108- enumProvider: commonEnumProviders.personas,
86+ new SlashCommandEnumValue('slash', 'slash commands (STscript)', enumTypes.command, '/'),
109- }),
87+ new SlashCommandEnumValue('macros', '{{macros}} (text replacement)', enumTypes.macro, enumIcons.macro),
110- ],
88+ new SlashCommandEnumValue('format', 'chat/text formatting', enumTypes.name, '★'),
111- helpString: 'Selects the given persona with its name and avatar (by name or avatar url). If no matching persona exists, applies a temporary name.',
89+ new SlashCommandEnumValue('hotkeys', 'keyboard shortcuts', enumTypes.enum, '⏎'),
112- aliases: ['name'],
90+ ],
113-}));
91+ })],
114-SlashCommandParser.addCommandObject(SlashCommand.fromProps({
92+ helpString: 'Get help on macros, chat formatting and commands.',
115- name: 'sync',
93+ }));
116- callback: syncCallback,
94+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
117- helpString: 'Syncs the user persona in user-attributed messages in the current chat.',
95+ name: 'persona',
118-}));
96+ callback: setNameCallback,
119-SlashCommandParser.addCommandObject(SlashCommand.fromProps({
97+ namedArgumentList: [
120- name: 'lock',
98+ new SlashCommandNamedArgument(
121- callback: lockPersonaCallback,
99+ 'mode', 'The mode for persona selection. ("lookup" = search for existing persona, "temp" = create a temporary name, set a temporary name, "all" = allow both in the same command)',
122- aliases: ['bind'],
100+ [ARGUMENT_TYPE.STRING], false, false, 'all', ['lookup', 'temp', 'all'],
123- helpString: 'Locks/unlocks a persona (name and avatar) to the current chat',
101+ ),
124- unnamedArgumentList: [
102+ ],
125- SlashCommandArgument.fromProps({
103+ unnamedArgumentList: [
126- description: 'state',
104+ SlashCommandArgument.fromProps({
127- typeList: [ARGUMENT_TYPE.STRING],
105+ description: 'persona name',
128- isRequired: true,
106+ typeList: [ARGUMENT_TYPE.STRING],
129- defaultValue: 'toggle',
107+ isRequired: true,
130108 enumProvider: commonEnumProviders.boolean('onOffToggle')personas,
131109 }),
132110 ],
133-}));
111+ helpString: 'Selects the given persona with its name and avatar (by name or avatar url). If no matching persona exists, applies a temporary name.',
134-SlashCommandParser.addCommandObject(SlashCommand.fromProps({
112+ aliases: ['name'],
135- name: 'bg',
113+ }));
136- callback: setBackgroundCallback,
114+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
137115 aliases name: ['backgroundsync'],
138- returns: 'the current background',
116+ callback: syncCallback,
139- unnamedArgumentList: [
117+ helpString: 'Syncs the user persona in user-attributed messages in the current chat.',
140- SlashCommandArgument.fromProps({
118+ }));
141- description: 'filename',
119+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
142- typeList: [ARGUMENT_TYPE.STRING],
120+ name: 'lock',
143121 isRequired callback: truelockPersonaCallback,
144- enumProvider: () => [...document.querySelectorAll('.bg_example')]
122+ aliases: ['bind'],
145- .map(it => new SlashCommandEnumValue(it.getAttribute('bgfile')))
123+ helpString: 'Locks/unlocks a persona (name and avatar) to the current chat',
146- .filter(it => it.value?.length),
124+ unnamedArgumentList: [
147- }),
125+ SlashCommandArgument.fromProps({
148- ],
126+ description: 'state',
149- helpString: `
127+ typeList: [ARGUMENT_TYPE.STRING],
128+ isRequired: true,
129+ defaultValue: 'toggle',
130+ enumProvider: commonEnumProviders.boolean('onOffToggle'),
131+ }),
132+ ],
133+ }));
134+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
135+ name: 'bg',
136+ callback: setBackgroundCallback,
137+ aliases: ['background'],
138+ returns: 'the current background',
139+ unnamedArgumentList: [
140+ SlashCommandArgument.fromProps({
141+ description: 'filename',
142+ typeList: [ARGUMENT_TYPE.STRING],
143+ isRequired: true,
144+ enumProvider: () => [...document.querySelectorAll('.bg_example')]
145+ .map(it => new SlashCommandEnumValue(it.getAttribute('bgfile')))
146+ .filter(it => it.value?.length),
147+ }),
148+ ],
149+ helpString: `
150150 <div>
151151 Sets a background according to the provided filename. Partial names allowed.
152152 </div>
@@ -159,35 +159,35 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
159159 </ul>
160160 </div>
161161 `,
162162 }));
163163 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
164164 name: 'sendas',
165165 callback: sendMessageAs,
166166 namedArgumentList: [
167167 SlashCommandNamedArgument.fromProps({
168168 name: 'name',
169169 description: 'Character name',
170170 typeList: [ARGUMENT_TYPE.STRING],
171171 isRequired: true,
172172 enumProvider: commonEnumProviders.characters('character'),
173173 forceEnum: false,
174174 }),
175175 new SlashCommandNamedArgument(
176176 'compact', 'Use compact layout', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',
177177 ),
178178 SlashCommandNamedArgument.fromProps({
179179 name: 'at',
180- description: 'position to insert the message',
180+ description: 'position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values 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.',
181181 typeList: [ARGUMENT_TYPE.NUMBER],
182182 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
183183 }),
184184 ],
185185 unnamedArgumentList: [
186186 new SlashCommandArgument(
187187 'text', [ARGUMENT_TYPE.STRING], true,
188188 ),
189189 ],
190190 helpString: `
191191 <div>
192192 Sends a message as a specific character. Uses the character avatar if it exists in the characters list.
193193 </div>
@@ -204,33 +204,33 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
204204 If "compact" is set to true, the message is sent using a compact layout.
205205 </div>
206206 `,
207207 }));
208208 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
209209 name: 'sys',
210210 callback: sendNarratorMessage,
211211 aliases: ['nar'],
212212 namedArgumentList: [
213213 new SlashCommandNamedArgument(
214214 'compact',
215215 'compact layout',
216216 [ARGUMENT_TYPE.BOOLEAN],
217217 false,
218218 false,
219219 'false',
220220 ),
221221 SlashCommandNamedArgument.fromProps({
222222 name: 'at',
223- description: 'position to insert the message',
223+ description: 'position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values 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.',
224224 typeList: [ARGUMENT_TYPE.NUMBER],
225225 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
226226 }),
227227 ],
228228 unnamedArgumentList: [
229229 new SlashCommandArgument(
230230 'text', [ARGUMENT_TYPE.STRING], true,
231231 ),
232232 ],
233233 helpString: `
234234 <div>
235235 Sends a message as a system narrator.
236236 </div>
@@ -249,44 +249,44 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
249249 </ul>
250250 </div>
251251 `,
252252 }));
253253 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
254254 name: 'sysname',
255255 callback: setNarratorName,
256256 unnamedArgumentList: [
257257 new SlashCommandArgument(
258258 'name', [ARGUMENT_TYPE.STRING], false,
259259 ),
260260 ],
261261 helpString: 'Sets a name for future system narrator messages in this chat (display only). Default: System. Leave empty to reset.',
262262 }));
263263 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
264264 name: 'comment',
265265 callback: sendCommentMessage,
266266 namedArgumentList: [
267267 new SlashCommandNamedArgument(
268268 'compact',
269269 'Whether to use a compact layout',
270270 [ARGUMENT_TYPE.BOOLEAN],
271271 false,
272272 false,
273273 'false',
274274 ),
275275 SlashCommandNamedArgument.fromProps({
276276 name: 'at',
277- description: 'position to insert the message',
277+ description: 'position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values 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.',
278278 typeList: [ARGUMENT_TYPE.NUMBER],
279279 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
280280 }),
281281 ],
282282 unnamedArgumentList: [
283283 new SlashCommandArgument(
284284 'text',
285285 [ARGUMENT_TYPE.STRING],
286286 true,
287287 ),
288288 ],
289289 helpString: `
290290 <div>
291291 Adds a note/comment message not part of the chat.
292292 </div>
@@ -305,35 +305,35 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
305305 </ul>
306306 </div>
307307 `,
308308 }));
309309 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
310310 name: 'single',
311311 callback: setStoryModeCallback,
312312 aliases: ['story'],
313313 helpString: 'Sets the message style to single document mode without names or avatars visible.',
314314 }));
315315 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
316316 name: 'bubble',
317317 callback: setBubbleModeCallback,
318318 aliases: ['bubbles'],
319319 helpString: 'Sets the message style to bubble chat mode.',
320320 }));
321321 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
322322 name: 'flat',
323323 callback: setFlatModeCallback,
324324 aliases: ['default'],
325325 helpString: 'Sets the message style to flat chat mode.',
326326 }));
327327 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
328328 name: 'continue',
329329 callback: continueChatCallback,
330330 aliases: ['cont'],
331331 unnamedArgumentList: [
332332 new SlashCommandArgument(
333333 'prompt', [ARGUMENT_TYPE.STRING], false,
334334 ),
335335 ],
336336 helpString: `
337337 <div>
338338 Continues the last message in the chat, with an optional additional prompt.
339339 </div>
@@ -351,87 +351,87 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
351351 </ul>
352352 </div>
353353 `,
354354 }));
355355 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
356356 name: 'go',
357357 callback: goToCharacterCallback,
358358 unnamedArgumentList: [
359359 SlashCommandArgument.fromProps({
360360 description: 'name',
361361 typeList: [ARGUMENT_TYPE.STRING],
362362 isRequired: true,
363363 enumProvider: commonEnumProviders.characters('all'),
364364 }),
365365 ],
366366 helpString: 'Opens up a chat with the character or group by its name',
367367 aliases: ['char'],
368368 }));
369369 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
370370 name: 'rename-char',
371371 /** @param {{silent: string, chats: string}} options @param {string} name */
372372 callback: async ({ silent = 'true', chats = null }, name) => {
373373 const renamed = await renameCharacter(name, { silent: isTrueBoolean(silent), renameChats: chats !== null ? isTrueBoolean(chats) : null });
374374 return String(renamed);
375375 },
376376 returns: 'true/false - Whether the rename was successful',
377377 namedArgumentList: [
378378 new SlashCommandNamedArgument(
379379 'silent', 'Hide any blocking popups. (if false, the name is optional. If not supplied, a popup asking for it will appear)', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true',
380380 ),
381381 new SlashCommandNamedArgument(
382382 'chats', 'Rename char in all previous chats', [ARGUMENT_TYPE.BOOLEAN], false, false, '<null>',
383383 ),
384384 ],
385385 unnamedArgumentList: [
386386 new SlashCommandArgument(
387387 'new char name', [ARGUMENT_TYPE.STRING], true,
388388 ),
389389 ],
390390 helpString: 'Renames the current character.',
391391 }));
392392 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
393393 name: 'sysgen',
394394 callback: generateSystemMessage,
395395 unnamedArgumentList: [
396396 new SlashCommandArgument(
397397 'prompt', [ARGUMENT_TYPE.STRING], true,
398398 ),
399399 ],
400400 helpString: 'Generates a system message using a specified prompt.',
401401 }));
402402 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
403403 name: 'ask',
404404 callback: askCharacter,
405405 namedArgumentList: [
406406 SlashCommandNamedArgument.fromProps({
407407 name: 'name',
408408 description: 'character name',
409409 typeList: [ARGUMENT_TYPE.STRING],
410410 isRequired: true,
411411 enumProvider: commonEnumProviders.characters('character'),
412412 }),
413413 ],
414414 unnamedArgumentList: [
415415 new SlashCommandArgument(
416416 'prompt', [ARGUMENT_TYPE.STRING], true, false,
417417 ),
418418 ],
419419 helpString: 'Asks a specified character card a prompt. Character name must be provided in a named argument.',
420420 }));
421421 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
422422 name: 'delname',
423423 callback: deleteMessagesByNameCallback,
424424 namedArgumentList: [],
425425 unnamedArgumentList: [
426426 SlashCommandArgument.fromProps({
427427 description: 'name',
428428 typeList: [ARGUMENT_TYPE.STRING],
429429 isRequired: true,
430430 enumProvider: commonEnumProviders.characters('character'),
431431 }),
432432 ],
433433 aliases: ['cancel'],
434434 helpString: `
435435 <div>
436436 Deletes all messages attributed to a specified name.
437437 </div>
@@ -444,41 +444,41 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
444444 </ul>
445445 </div>
446446 `,
447447 }));
448448 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
449449 name: 'send',
450450 callback: sendUserMessageCallback,
451451 namedArgumentList: [
452452 new SlashCommandNamedArgument(
453453 'compact',
454454 'whether to use a compact layout',
455455 [ARGUMENT_TYPE.BOOLEAN],
456456 false,
457457 false,
458458 'false',
459459 ),
460460 SlashCommandNamedArgument.fromProps({
461461 name: 'at',
462- description: 'position to insert the message',
462+ description: 'position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values 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.',
463463 typeList: [ARGUMENT_TYPE.NUMBER],
464464 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
465465 }),
466466 SlashCommandNamedArgument.fromProps({
467467 name: 'name',
468468 description: 'display name',
469469 typeList: [ARGUMENT_TYPE.STRING],
470470 defaultValue: '{{user}}',
471471 enumProvider: commonEnumProviders.characters('character'),
472472 }),
473473 ],
474474 unnamedArgumentList: [
475475 new SlashCommandArgument(
476476 'text',
477477 [ARGUMENT_TYPE.STRING],
478478 true,
479479 ),
480480 ],
481481 helpString: `
482482 <div>
483483 Adds a user message to the chat log without triggering a generation.
484484 </div>
@@ -500,29 +500,29 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
500500 </ul>
501501 </div>
502502 `,
503503 }));
504504 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
505505 name: 'trigger',
506506 callback: triggerGenerationCallback,
507507 namedArgumentList: [
508508 new SlashCommandNamedArgument(
509509 'await',
510510 'Whether to await for the triggered generation before continuing',
511511 [ARGUMENT_TYPE.BOOLEAN],
512512 false,
513513 false,
514514 'false',
515515 ),
516516 ],
517517 unnamedArgumentList: [
518518 SlashCommandArgument.fromProps({
519519 description: 'group member index (starts with 0) or name',
520520 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING],
521521 isRequired: false,
522522 enumProvider: commonEnumProviders.groupMembers(),
523523 }),
524524 ],
525525 helpString: `
526526 <div>
527527 Triggers a message generation. If in group, can trigger a message for the specified group member index or name.
528528 </div>
@@ -530,74 +530,74 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
530530 If <code>await=true</code> named argument is passed, the command will await for the triggered generation before continuing.
531531 </div>
532532 `,
533533 }));
534534 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
535535 name: 'hide',
536536 callback: hideMessageCallback,
537537 unnamedArgumentList: [
538538 SlashCommandArgument.fromProps({
539539 description: 'message index (starts with 0) or range',
540540 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE],
541541 isRequired: true,
542542 enumProvider: commonEnumProviders.messages(),
543543 }),
544544 ],
545545 helpString: 'Hides a chat message from the prompt.',
546546 }));
547547 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
548548 name: 'unhide',
549549 callback: unhideMessageCallback,
550550 unnamedArgumentList: [
551551 SlashCommandArgument.fromProps({
552552 description: 'message index (starts with 0) or range',
553553 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE],
554554 isRequired: true,
555555 enumProvider: commonEnumProviders.messages(),
556556 }),
557557 ],
558558 helpString: 'Unhides a message from the prompt.',
559559 }));
560560 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
561561 name: 'member-disable',
562562 callback: disableGroupMemberCallback,
563563 aliases: ['disable', 'disablemember', 'memberdisable'],
564564 unnamedArgumentList: [
565565 SlashCommandArgument.fromProps({
566566 description: 'member index (starts with 0) or name',
567567 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING],
568568 isRequired: true,
569569 enumProvider: commonEnumProviders.groupMembers(),
570570 }),
571571 ],
572572 helpString: 'Disables a group member from being drafted for replies.',
573573 }));
574574 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
575575 name: 'member-enable',
576576 aliases: ['enable', 'enablemember', 'memberenable'],
577577 callback: enableGroupMemberCallback,
578578 unnamedArgumentList: [
579579 SlashCommandArgument.fromProps({
580580 description: 'member index (starts with 0) or name',
581581 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING],
582582 isRequired: true,
583583 enumProvider: commonEnumProviders.groupMembers(),
584584 }),
585585 ],
586586 helpString: 'Enables a group member to be drafted for replies.',
587587 }));
588588 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
589589 name: 'member-add',
590590 callback: addGroupMemberCallback,
591591 aliases: ['addmember', 'memberadd'],
592592 unnamedArgumentList: [
593593 SlashCommandArgument.fromProps({
594594 description: 'character name',
595595 typeList: [ARGUMENT_TYPE.STRING],
596596 isRequired: true,
597597 enumProvider: () => selected_group ? commonEnumProviders.characters('character')() : [],
598598 }),
599599 ],
600600 helpString: `
601601 <div>
602602 Adds a new group member to the group chat.
603603 </div>
@@ -610,20 +610,20 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
610610 </ul>
611611 </div>
612612 `,
613613 }));
614614 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
615615 name: 'member-remove',
616616 callback: removeGroupMemberCallback,
617617 aliases: ['removemember', 'memberremove'],
618618 unnamedArgumentList: [
619619 SlashCommandArgument.fromProps({
620620 description: 'member index (starts with 0) or name',
621621 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING],
622622 isRequired: true,
623623 enumProvider: commonEnumProviders.groupMembers(),
624624 }),
625625 ],
626626 helpString: `
627627 <div>
628628 Removes a group member from the group chat.
629629 </div>
@@ -637,47 +637,47 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
637637 </ul>
638638 </div>
639639 `,
640640 }));
641641 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
642642 name: 'member-up',
643643 callback: moveGroupMemberUpCallback,
644644 aliases: ['upmember', 'memberup'],
645645 unnamedArgumentList: [
646646 SlashCommandArgument.fromProps({
647647 description: 'member index (starts with 0) or name',
648648 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING],
649649 isRequired: true,
650650 enumProvider: commonEnumProviders.groupMembers(),
651651 }),
652652 ],
653653 helpString: 'Moves a group member up in the group chat list.',
654654 }));
655655 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
656656 name: 'member-down',
657657 callback: moveGroupMemberDownCallback,
658658 aliases: ['downmember', 'memberdown'],
659659 unnamedArgumentList: [
660660 SlashCommandArgument.fromProps({
661661 description: 'member index (starts with 0) or name',
662662 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING],
663663 isRequired: true,
664664 enumProvider: commonEnumProviders.groupMembers(),
665665 }),
666666 ],
667667 helpString: 'Moves a group member down in the group chat list.',
668668 }));
669669 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
670670 name: 'peek',
671671 callback: peekCallback,
672672 unnamedArgumentList: [
673673 SlashCommandArgument.fromProps({
674674 description: 'member index (starts with 0) or name',
675675 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING],
676676 isRequired: true,
677677 enumProvider: commonEnumProviders.groupMembers(),
678678 }),
679679 ],
680680 helpString: `
681681 <div>
682682 Shows a group member character card without switching chats.
683683 </div>
@@ -691,22 +691,22 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
691691 </ul>
692692 </div>
693693 `,
694694 }));
695695 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
696696 name: 'delswipe',
697697 callback: deleteSwipeCallback,
698698 aliases: ['swipedel'],
699699 unnamedArgumentList: [
700700 SlashCommandArgument.fromProps({
701701 description: '1-based swipe id',
702702 typeList: [ARGUMENT_TYPE.NUMBER],
703703 isRequired: true,
704704 enumProvider: () => Array.isArray(chat[chat.length - 1]?.swipes) ?
705705 chat[chat.length - 1].swipes.map((/** @type {string} */ swipe, /** @type {number} */ i) => new SlashCommandEnumValue(String(i + 1), swipe, enumTypes.enum, enumIcons.message))
706706 : [],
707707 }),
708708 ],
709709 helpString: `
710710 <div>
711711 Deletes a swipe from the last chat message. If swipe id is not provided, it deletes the current swipe.
712712 </div>
@@ -724,34 +724,60 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
724724 </ul>
725725 </div>
726726 `,
727727 }));
728728 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
729729 name: 'echo',
730730 callback: echoCallback,
731731 returns: 'the text',
732732 namedArgumentList: [
733733 new SlashCommandNamedArgument(
734734 'title', 'title of the toast message', [ARGUMENT_TYPE.STRING], false,
735735 ),
736736 SlashCommandNamedArgument.fromProps({
737737 name: 'severity',
738738 description: 'severity level of the toast message',
739739 typeList: [ARGUMENT_TYPE.STRING],
740740 defaultValue: 'info',
741741 enumProvider: () => [
742742 new SlashCommandEnumValue('info', 'info', enumTypes.macro, 'ℹ️'),
743743 new SlashCommandEnumValue('warning', 'warning', enumTypes.enum, '⚠️'),
744744 new SlashCommandEnumValue('error', 'error', enumTypes.enum, '❗'),
745745 new SlashCommandEnumValue('success', 'success', enumTypes.enum, '✅'),
746746 ],
747747 }),
748- ],
748+ SlashCommandNamedArgument.fromProps({
749- unnamedArgumentList: [
749+ name: 'timeout',
750- new SlashCommandArgument(
750+ description: 'time in milliseconds to display the toast message. Set this and \'extendedTimeout\' to 0 to show indefinitely until dismissed.',
751- 'text', [ARGUMENT_TYPE.STRING], true,
751+ typeList: [ARGUMENT_TYPE.NUMBER],
752- ),
752+ defaultValue: `${toastr.options.timeOut}`,
753- ],
753+ }),
754- helpString: `
754+ SlashCommandNamedArgument.fromProps({
755+ name: 'extendedTimeout',
756+ description: 'time in milliseconds to display the toast message. Set this and \'timeout\' to 0 to show indefinitely until dismissed.',
757+ typeList: [ARGUMENT_TYPE.NUMBER],
758+ defaultValue: `${toastr.options.extendedTimeOut}`,
759+ }),
760+ SlashCommandNamedArgument.fromProps({
761+ name: 'preventDuplicates',
762+ description: 'prevent duplicate toasts with the same message from being displayed.',
763+ typeList: [ARGUMENT_TYPE.BOOLEAN],
764+ defaultValue: 'false',
765+ enumList: commonEnumProviders.boolean('trueFalse')(),
766+ }),
767+ SlashCommandNamedArgument.fromProps({
768+ name: 'awaitDismissal',
769+ description: 'wait for the toast to be dismissed before continuing.',
770+ typeList: [ARGUMENT_TYPE.BOOLEAN],
771+ defaultValue: 'false',
772+ enumList: commonEnumProviders.boolean('trueFalse')(),
773+ }),
774+ ],
775+ unnamedArgumentList: [
776+ new SlashCommandArgument(
777+ 'text', [ARGUMENT_TYPE.STRING], true,
778+ ),
779+ ],
780+ helpString: `
755781 <div>
756782 Echoes the provided text to a toast message. Useful for pipes debugging.
757783 </div>
@@ -764,42 +790,42 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
764790 </ul>
765791 </div>
766792 `,
767793 }));
768794 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
769795 name: 'gen',
770796 callback: generateCallback,
771797 returns: 'generated text',
772798 namedArgumentList: [
773799 new SlashCommandNamedArgument(
774800 'lock', 'lock user input during generation', [ARGUMENT_TYPE.BOOLEAN], false, false, null, commonEnumProviders.boolean('onOff')(),
775801 ),
776802 SlashCommandNamedArgument.fromProps({
777803 name: 'name',
778804 description: 'in-prompt name for instruct mode',
779805 typeList: [ARGUMENT_TYPE.STRING],
780806 defaultValue: 'System',
781807 enumProvider: () => [...commonEnumProviders.characters('character')(), new SlashCommandEnumValue('System', null, enumTypes.enum, enumIcons.assistant)],
782808 forceEnum: false,
783809 }),
784810 new SlashCommandNamedArgument(
785811 'length', 'API response length in tokens', [ARGUMENT_TYPE.NUMBER], false,
786812 ),
787813 SlashCommandNamedArgument.fromProps({
788814 name: 'as',
789815 description: 'role of the output prompt',
790816 typeList: [ARGUMENT_TYPE.STRING],
791817 enumList: [
792818 new SlashCommandEnumValue('system', null, enumTypes.enum, enumIcons.assistant),
793819 new SlashCommandEnumValue('char', null, enumTypes.enum, enumIcons.character),
794820 ],
795821 }),
796822 ],
797823 unnamedArgumentList: [
798824 new SlashCommandArgument(
799825 'prompt', [ARGUMENT_TYPE.STRING], true,
800826 ),
801827 ],
802828 helpString: `
803829 <div>
804830 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").
805831 </div>
@@ -807,43 +833,43 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
807833 "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.
808834 </div>
809835 `,
810836 }));
811837 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
812838 name: 'genraw',
813839 callback: generateRawCallback,
814840 returns: 'generated text',
815841 namedArgumentList: [
816842 new SlashCommandNamedArgument(
817843 'lock', 'lock user input during generation', [ARGUMENT_TYPE.BOOLEAN], false, false, null, commonEnumProviders.boolean('onOff')(),
818844 ),
819845 new SlashCommandNamedArgument(
820846 'instruct', 'use instruct mode', [ARGUMENT_TYPE.BOOLEAN], false, false, 'on', commonEnumProviders.boolean('onOff')(),
821847 ),
822848 new SlashCommandNamedArgument(
823849 'stop', 'one-time custom stop strings', [ARGUMENT_TYPE.LIST], false,
824850 ),
825851 SlashCommandNamedArgument.fromProps({
826852 name: 'as',
827853 description: 'role of the output prompt',
828854 typeList: [ARGUMENT_TYPE.STRING],
829855 enumList: [
830856 new SlashCommandEnumValue('system', null, enumTypes.enum, enumIcons.assistant),
831857 new SlashCommandEnumValue('char', null, enumTypes.enum, enumIcons.character),
832858 ],
833859 }),
834860 new SlashCommandNamedArgument(
835861 'system', 'system prompt at the start', [ARGUMENT_TYPE.STRING], false,
836862 ),
837863 new SlashCommandNamedArgument(
838864 'length', 'API response length in tokens', [ARGUMENT_TYPE.NUMBER], false,
839865 ),
840866 ],
841867 unnamedArgumentList: [
842868 new SlashCommandArgument(
843869 'prompt', [ARGUMENT_TYPE.STRING], true,
844870 ),
845871 ],
846872 helpString: `
847873 <div>
848874 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.
849875 </div>
@@ -860,55 +886,55 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
860886 If "length" argument is provided as a number in tokens, allows to temporarily override an API response length.
861887 </div>
862888 `,
863889 }));
864890 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
865891 name: 'addswipe',
866892 callback: addSwipeCallback,
867893 aliases: ['swipeadd'],
868894 unnamedArgumentList: [
869895 new SlashCommandArgument(
870896 'text', [ARGUMENT_TYPE.STRING], true,
871897 ),
872898 ],
873899 helpString: 'Adds a swipe to the last chat message.',
874900 }));
875901 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
876902 name: 'abort',
877903 callback: abortCallback,
878904 namedArgumentList: [
879905 SlashCommandNamedArgument.fromProps({
880906 name: 'quiet',
881907 description: 'Whether to suppress the toast message notifying about the /abort call.',
882908 typeList: [ARGUMENT_TYPE.BOOLEAN],
883909 defaultValue: 'true',
884910 }),
885911 ],
886912 unnamedArgumentList: [
887913 SlashCommandArgument.fromProps({
888914 description: 'The reason for aborting command execution. Shown when quiet=false',
889915 typeList: [ARGUMENT_TYPE.STRING],
890916 }),
891917 ],
892918 helpString: 'Aborts the slash command batch execution.',
893919 }));
894920 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
895921 name: 'fuzzy',
896922 callback: fuzzyCallback,
897923 returns: 'first matching item',
898924 namedArgumentList: [
899925 new SlashCommandNamedArgument(
900926 'list', 'list of items to match against', [ARGUMENT_TYPE.LIST], true,
901927 ),
902928 new SlashCommandNamedArgument(
903929 'threshold', 'fuzzy match threshold (0.0 to 1.0)', [ARGUMENT_TYPE.NUMBER], false, false, '0.4',
904930 ),
905931 ],
906932 unnamedArgumentList: [
907933 new SlashCommandArgument(
908934 'text to search', [ARGUMENT_TYPE.STRING], true,
909935 ),
910936 ],
911937 helpString: `
912938 <div>
913939 Performs a fuzzy match of each item in the <code>list</code> against the <code>text to search</code>.
914940 If any item matches, then its name is returned. If no item matches the text, no value is returned.
@@ -930,23 +956,23 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
930956 </ul>
931957 </div>
932958 `,
933959 }));
934960 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
935961 name: 'pass',
936962 callback: (_, arg) => {
937963 // We do not support arrays of closures. Arrays of strings will be send as JSON
938964 if (Array.isArray(arg) && arg.some(x => x instanceof SlashCommandClosure)) throw new Error('Command /pass does not support multiple closures');
939965 if (Array.isArray(arg)) return JSON.stringify(arg);
940966 return arg;
941967 },
942968 returns: 'the provided value',
943969 unnamedArgumentList: [
944970 new SlashCommandArgument(
945971 'text', [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.BOOLEAN, ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.DICTIONARY, ARGUMENT_TYPE.CLOSURE], true,
946972 ),
947973 ],
948974 aliases: ['return'],
949975 helpString: `
950976 <div>
951977 <pre><span class="monospace">/pass (text)</span> – passes the text to the next command through the pipe.</pre>
952978 </div>
@@ -957,17 +983,17 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
957983 </ul>
958984 </div>
959985 `,
960986 }));
961987 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
962988 name: 'delay',
963989 callback: delayCallback,
964990 aliases: ['wait', 'sleep'],
965991 unnamedArgumentList: [
966992 new SlashCommandArgument(
967993 'milliseconds', [ARGUMENT_TYPE.NUMBER], true,
968994 ),
969995 ],
970996 helpString: `
971997 <div>
972998 Delays the next command in the pipe by the specified number of milliseconds.
973999 </div>
@@ -980,101 +1006,101 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9801006 </ul>
9811007 </div>
9821008 `,
9831009 }));
9841010 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9851011 name: 'input',
9861012 aliases: ['prompt'],
9871013 callback: inputCallback,
9881014 returns: 'user input',
9891015 namedArgumentList: [
9901016 new SlashCommandNamedArgument(
9911017 'default', 'default value of the input field', [ARGUMENT_TYPE.STRING], false, false, '"string"',
9921018 ),
9931019 new SlashCommandNamedArgument(
9941020 'large', 'show large input field', [ARGUMENT_TYPE.BOOLEAN], false, false, 'off', commonEnumProviders.boolean('onOff')(),
9951021 ),
9961022 new SlashCommandNamedArgument(
9971023 'wide', 'show wide input field', [ARGUMENT_TYPE.BOOLEAN], false, false, 'off', commonEnumProviders.boolean('onOff')(),
9981024 ),
9991025 new SlashCommandNamedArgument(
10001026 'okButton', 'text for the ok button', [ARGUMENT_TYPE.STRING], false,
10011027 ),
10021028 new SlashCommandNamedArgument(
10031029 'rows', 'number of rows for the input field', [ARGUMENT_TYPE.NUMBER], false,
10041030 ),
10051031 ],
10061032 unnamedArgumentList: [
10071033 new SlashCommandArgument(
10081034 'text to display', [ARGUMENT_TYPE.STRING], false,
10091035 ),
10101036 ],
10111037 helpString: `
10121038 <div>
10131039 Shows a popup with the provided text and an input field.
10141040 The <code>default</code> argument is the default value of the input field, and the text argument is the text to display.
10151041 </div>
10161042 `,
10171043 }));
10181044 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
10191045 name: 'run',
10201046 aliases: ['call', 'exec'],
10211047 callback: runCallback,
10221048 returns: 'result of the executed closure of QR',
10231049 namedArgumentList: [
10241050 new SlashCommandNamedArgument(
10251051 'args', 'named arguments', [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.BOOLEAN, ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.DICTIONARY], false, true,
10261052 ),
10271053 ],
10281054 unnamedArgumentList: [
10291055 SlashCommandArgument.fromProps({
10301056 description: 'scoped variable or qr label',
10311057 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING],
10321058 isRequired: true,
10331059 enumProvider: () => [
10341060 ...commonEnumProviders.variables('scope')(),
10351061 ...(typeof window['qrEnumProviderExecutables'] === 'function') ? window['qrEnumProviderExecutables']() : [],
10361062 ],
10371063 }),
10381064 ],
10391065 helpString: `
10401066 <div>
10411067 Runs a closure from a scoped variable, or a Quick Reply with the specified name from a currently active preset or from another preset.
10421068 Named arguments can be referenced in a QR with <code>{{arg::key}}</code>.
10431069 </div>
10441070 `,
10451071 }));
10461072 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
10471073 name: 'messages',
10481074 callback: getMessagesCallback,
10491075 aliases: ['message'],
10501076 namedArgumentList: [
10511077 new SlashCommandNamedArgument(
10521078 'names', 'show message author names', [ARGUMENT_TYPE.BOOLEAN], false, false, 'off', commonEnumProviders.boolean('onOff')(),
10531079 ),
10541080 new SlashCommandNamedArgument(
10551081 'hidden', 'include hidden messages', [ARGUMENT_TYPE.BOOLEAN], false, false, 'on', commonEnumProviders.boolean('onOff')(),
10561082 ),
10571083 SlashCommandNamedArgument.fromProps({
10581084 name: 'role',
10591085 description: 'filter messages by role',
10601086 typeList: [ARGUMENT_TYPE.STRING],
10611087 enumList: [
10621088 new SlashCommandEnumValue('system', null, enumTypes.enum, enumIcons.system),
10631089 new SlashCommandEnumValue('assistant', null, enumTypes.enum, enumIcons.assistant),
10641090 new SlashCommandEnumValue('user', null, enumTypes.enum, enumIcons.user),
10651091 ],
10661092 }),
10671093 ],
10681094 unnamedArgumentList: [
10691095 SlashCommandArgument.fromProps({
10701096 description: 'message index (starts with 0) or range',
10711097 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE],
10721098 isRequired: true,
10731099 enumProvider: commonEnumProviders.messages(),
10741100 }),
10751101 ],
10761102 returns: 'the specified message or range of messages as a string',
10771103 helpString: `
10781104 <div>
10791105 Returns the specified message or range of messages as a string.
10801106 </div>
@@ -1098,16 +1124,16 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
10981124 </ul>
10991125 </div>
11001126 `,
11011127 }));
11021128 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
11031129 name: 'setinput',
11041130 callback: setInputCallback,
11051131 unnamedArgumentList: [
11061132 new SlashCommandArgument(
11071133 'text', [ARGUMENT_TYPE.STRING], true,
11081134 ),
11091135 ],
11101136 helpString: `
11111137 <div>
11121138 Sets the user input to the specified text and passes it to the next command through the pipe.
11131139 </div>
@@ -1120,28 +1146,28 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
11201146 </ul>
11211147 </div>
11221148 `,
11231149 }));
11241150 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
11251151 name: 'popup',
11261152 callback: popupCallback,
11271153 returns: 'popup text',
11281154 namedArgumentList: [
11291155 new SlashCommandNamedArgument(
11301156 'large', 'show large popup', [ARGUMENT_TYPE.BOOLEAN], false, false, null, commonEnumProviders.boolean('onOff')(),
11311157 ),
11321158 new SlashCommandNamedArgument(
11331159 'wide', 'show wide popup', [ARGUMENT_TYPE.BOOLEAN], false, false, null, commonEnumProviders.boolean('onOff')(),
11341160 ),
11351161 new SlashCommandNamedArgument(
11361162 'okButton', 'text for the OK button', [ARGUMENT_TYPE.STRING], false,
11371163 ),
11381164 ],
11391165 unnamedArgumentList: [
11401166 new SlashCommandArgument(
11411167 'text', [ARGUMENT_TYPE.STRING], true,
11421168 ),
11431169 ],
11441170 helpString: `
11451171 <div>
11461172 Shows a blocking popup with the specified text and buttons.
11471173 Returns the popup text.
@@ -1155,22 +1181,22 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
11551181 </ul>
11561182 </div>
11571183 `,
11581184 }));
11591185 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
11601186 name: 'buttons',
11611187 callback: buttonsCallback,
11621188 returns: 'clicked button label',
11631189 namedArgumentList: [
11641190 new SlashCommandNamedArgument(
11651191 'labels', 'button labels', [ARGUMENT_TYPE.LIST], true,
11661192 ),
11671193 ],
11681194 unnamedArgumentList: [
11691195 new SlashCommandArgument(
11701196 'text', [ARGUMENT_TYPE.STRING], true,
11711197 ),
11721198 ],
11731199 helpString: `
11741200 <div>
11751201 Shows a blocking popup with the specified text and buttons.
11761202 Returns the clicked button label into the pipe or empty string if canceled.
@@ -1184,32 +1210,32 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
11841210 </ul>
11851211 </div>
11861212 `,
11871213 }));
11881214 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
11891215 name: 'trimtokens',
11901216 callback: trimTokensCallback,
11911217 returns: 'trimmed text',
11921218 namedArgumentList: [
11931219 new SlashCommandNamedArgument(
11941220 'limit', 'number of tokens to keep', [ARGUMENT_TYPE.NUMBER], true,
11951221 ),
11961222 SlashCommandNamedArgument.fromProps({
11971223 name: 'direction',
11981224 description: 'trim direction',
11991225 typeList: [ARGUMENT_TYPE.STRING],
12001226 isRequired: true,
12011227 enumList: [
12021228 new SlashCommandEnumValue('start', null, enumTypes.enum, '⏪'),
12031229 new SlashCommandEnumValue('end', null, enumTypes.enum, '⏩'),
12041230 ],
12051231 }),
12061232 ],
12071233 unnamedArgumentList: [
12081234 new SlashCommandArgument(
12091235 'text', [ARGUMENT_TYPE.STRING], false,
12101236 ),
12111237 ],
12121238 helpString: `
12131239 <div>
12141240 Trims the start or end of text to the specified number of tokens.
12151241 </div>
@@ -1222,17 +1248,17 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
12221248 </ul>
12231249 </div>
12241250 `,
12251251 }));
12261252 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
12271253 name: 'trimstart',
12281254 callback: trimStartCallback,
12291255 returns: 'trimmed text',
12301256 unnamedArgumentList: [
12311257 new SlashCommandArgument(
12321258 'text', [ARGUMENT_TYPE.STRING], true,
12331259 ),
12341260 ],
12351261 helpString: `
12361262 <div>
12371263 Trims the text to the start of the first full sentence.
12381264 </div>
@@ -1245,148 +1271,149 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
12451271 </ul>
12461272 </div>
12471273 `,
12481274 }));
12491275 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
12501276 name: 'trimend',
12511277 callback: trimEndCallback,
12521278 returns: 'trimmed text',
12531279 unnamedArgumentList: [
12541280 new SlashCommandArgument(
12551281 'text', [ARGUMENT_TYPE.STRING], true,
12561282 ),
12571283 ],
12581284 helpString: 'Trims the text to the end of the last full sentence.',
12591285 }));
12601286 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
12611287 name: 'inject',
12621288 callback: injectCallback,
12631289 namedArgumentList: [
12641290 SlashCommandNamedArgument.fromProps({
12651291 name: 'id',
12661292 description: 'injection ID or variable name pointing to ID',
12671293 typeList: [ARGUMENT_TYPE.STRING],
12681294 isRequired: true,
12691295 enumProvider: commonEnumProviders.injects,
12701296 }),
12711297 new SlashCommandNamedArgument(
12721298 'position', 'injection position', [ARGUMENT_TYPE.STRING], false, false, 'after', ['before', 'after', 'chat'],
12731299 ),
12741300 new SlashCommandNamedArgument(
12751301 'depth', 'injection depth', [ARGUMENT_TYPE.NUMBER], false, false, '4',
12761302 ),
12771303 new SlashCommandNamedArgument(
12781304 'scan', 'include injection content into World Info scans', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',
12791305 ),
12801306 SlashCommandNamedArgument.fromProps({
12811307 name: 'role',
12821308 description: 'role for in-chat injections',
12831309 typeList: [ARGUMENT_TYPE.STRING],
12841310 isRequired: false,
12851311 enumList: [
12861312 new SlashCommandEnumValue('system', null, enumTypes.enum, enumIcons.system),
12871313 new SlashCommandEnumValue('assistant', null, enumTypes.enum, enumIcons.assistant),
12881314 new SlashCommandEnumValue('user', null, enumTypes.enum, enumIcons.user),
12891315 ],
12901316 }),
12911317 new SlashCommandNamedArgument(
12921318 'ephemeral', 'remove injection after generation', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',
12931319 ),
12941320 ],
12951321 unnamedArgumentList: [
12961322 new SlashCommandArgument(
12971323 'text', [ARGUMENT_TYPE.STRING], false,
12981324 ),
12991325 ],
13001326 helpString: 'Injects a text into the LLM prompt for the current chat. Requires a unique injection ID. Positions: "before" main prompt, "after" main prompt, in-"chat" (default: after). Depth: injection depth for the prompt (default: 4). Role: role for in-chat injections (default: system). Scan: include injection content into World Info scans (default: false).',
13011327 }));
13021328 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
13031329 name: 'listinjects',
13041330 callback: listInjectsCallback,
13051331 helpString: 'Lists all script injections for the current chat.',
13061332 }));
13071333 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
13081334 name: 'flushinject',
13091335 aliases: ['flushinjects'],
13101336 unnamedArgumentList: [
13111337 SlashCommandArgument.fromProps({
13121338 description: 'injection ID or a variable name pointing to ID',
13131339 typeList: [ARGUMENT_TYPE.STRING],
13141340 defaultValue: '',
13151341 enumProvider: commonEnumProviders.injects,
13161342 }),
13171343 ],
13181344 callback: flushInjectsCallback,
13191345 helpString: 'Removes a script injection for the current chat. If no ID is provided, removes all script injections.',
13201346 }));
13211347 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
13221348 name: 'tokens',
13231349 callback: (_, text) => {
13241350 if (text instanceof SlashCommandClosure || Array.isArray(text)) throw new Error('Unnamed argument cannot be a closure for command /tokens');
13251351 return getTokenCountAsync(text).then(count => String(count));
13261352 },
13271353 returns: 'number of tokens',
13281354 unnamedArgumentList: [
13291355 new SlashCommandArgument(
13301356 'text', [ARGUMENT_TYPE.STRING], true,
13311357 ),
13321358 ],
13331359 helpString: 'Counts the number of tokens in the provided text.',
13341360 }));
13351361 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
13361362 name: 'model',
13371363 callback: modelCallback,
13381364 returns: 'current model',
13391365 unnamedArgumentList: [
13401366 SlashCommandArgument.fromProps({
13411367 description: 'model name',
13421368 typeList: [ARGUMENT_TYPE.STRING],
13431369 enumProvider: () => getModelOptions()?.options.map(option => new SlashCommandEnumValue(option.value, option.value !== option.text ? option.text : null)),
13441370 }),
13451371 ],
13461372 helpString: 'Sets the model for the current API. Gets the current model name if no argument is provided.',
13471373 }));
13481374 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
13491375 name: 'setpromptentry',
13501376 aliases: ['setpromptentries'],
13511377 callback: setPromptEntryCallback,
13521378 namedArgumentList: [
13531379 SlashCommandNamedArgument.fromProps({
13541380 name: 'identifier',
13551381 description: 'Prompt entry identifier(s) to target',
13561382 typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.LIST],
13571383 acceptsMultiple: true,
13581384 enumProvider: () => {
13591385 const promptManager = setupChatCompletionPromptManager(oai_settings);
13601386 const prompts = promptManager.serviceSettings.prompts;
13611387 return prompts.map(prompt => new SlashCommandEnumValue(prompt.identifier, prompt.name, enumTypes.enum));
13621388 },
13631389 }),
13641390 SlashCommandNamedArgument.fromProps({
13651391 name: 'name',
13661392 description: 'Prompt entry name(s) to target',
13671393 typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.LIST],
13681394 acceptsMultiple: true,
13691395 enumProvider: () => {
13701396 const promptManager = setupChatCompletionPromptManager(oai_settings);
13711397 const prompts = promptManager.serviceSettings.prompts;
13721398 return prompts.map(prompt => new SlashCommandEnumValue(prompt.name, prompt.identifier, enumTypes.enum));
13731399 },
13741400 }),
13751401 ],
13761402 unnamedArgumentList: [
13771403 SlashCommandArgument.fromProps({
13781404 description: 'Set entry/entries on or off',
13791405 typeList: [ARGUMENT_TYPE.STRING],
13801406 isRequired: true,
13811407 acceptsMultiple: false,
13821408 defaultValue: 'toggle', // unnamed arguments don't support default values yet
13831409 enumList: commonEnumProviders.boolean('onOffToggle')(),
13841410 }),
13851411 ],
13861412 helpString: 'Sets the specified prompt manager entry/entries on or off.',
13871413 }));
13881414
13891415 registerVariableCommands();
1416+}
13901417
13911418const NARRATOR_NAME_KEY = 'narrator_name';
13921419const NARRATOR_NAME_DEFAULT = 'System';
@@ -1938,32 +1965,67 @@ async function generateCallback(args, value) {
19381965 }
19391966}
19401967
1968+/**
1969+ *
1970+ * @param {{title?: string, severity?: string, timeout?: string, extendedTimeout?: string, preventDuplicates?: string, awaitDismissal?: string}} args - named arguments from the slash command
1971+ * @param {string} value - The string to echo (unnamed argument from the slash command)
1972+ * @returns {Promise<string>} The text that was echoed
1973+ */
19411974async function echoCallback(args, value) {
19421975 // Note: We don't need to sanitize input, as toastr is set up by default to escape HTML via toastr options
19431976 if (value === '') {
19441977 console.warn('WARN: No argument provided for /echo command');
19451978 return '';
19461979 }
1947- const title = args?.title !== undefined && typeof args?.title === 'string' ? args.title : undefined;
1980+
1948- const severity = args?.severity !== undefined && typeof args?.severity === 'string' ? args.severity : 'info';
1981+ if (args.severity && !['error', 'warning', 'success', 'info'].includes(args.severity)) {
1982+ toastr.warning(`Invalid severity provided for /echo command: ${args.severity}`);
1983+ args.severity = null;
1984+ }
1985+
1986+ const title = args.title ? args.title : undefined;
1987+ const severity = args.severity ? args.severity : 'info';
1988+
1989+ /** @type {ToastrOptions} */
1990+ const options = {};
1991+ if (args.timeout && !isNaN(parseInt(args.timeout))) options.timeOut = parseInt(args.timeout);
1992+ if (args.extendedTimeout && !isNaN(parseInt(args.extendedTimeout))) options.extendedTimeOut = parseInt(args.extendedTimeout);
1993+ if (isTrueBoolean(args.preventDuplicates)) options.preventDuplicates = true;
1994+
1995+ // Prepare possible await handling
1996+ let awaitDismissal = isTrueBoolean(args.awaitDismissal);
1997+ let resolveToastDismissal;
1998+
1999+ if (awaitDismissal) {
2000+ options.onHidden = () => resolveToastDismissal(value);
2001+ }
2002+
19492003 switch (severity) {
19502004 case 'error':
19512005 toastr.error(value, title, options);
19522006 break;
19532007 case 'warning':
19542008 toastr.warning(value, title, options);
19552009 break;
19562010 case 'success':
19572011 toastr.success(value, title, options);
19582012 break;
19592013 case 'info':
19602014 default:
19612015 toastr.info(value, title, options);
19622016 break;
19632017 }
1964- return value;
2018+
2019+ if (awaitDismissal) {
2020+ return new Promise((resolve) => {
2021+ resolveToastDismissal = resolve;
2022+ });
2023+ } else {
2024+ return value;
2025+ }
19652026}
19662027
2028+
19672029async function addSwipeCallback(_, arg) {
19682030 const lastMessage = chat[chat.length - 1];
19692031
@@ -2428,7 +2490,14 @@ async function sendUserMessageCallback(args, text) {
24282490 text = text.trim();
24292491 const compact = isTrueBoolean(args?.compact);
24302492 const bias = extractMessageBias(text);
2431- const insertAt = Number(args?.at);
2493+
2494+ let insertAt = Number(args?.at);
2495+
2496+ // Convert possible depth parameter to index
2497+ if (!isNaN(insertAt) && (insertAt < 0 || insertAt === Number(-0))) {
2498+ // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)
2499+ insertAt = chat.length + insertAt;
2500+ }
24322501
24332502 if ('name' in args) {
24342503 const name = args.name || '';
@@ -2737,7 +2806,13 @@ export async function sendMessageAs(args, text) {
27372806 },
27382807 }];
27392808
27402809 constlet insertAt = Number(args.at);
2810+
2811+ // Convert possible depth parameter to index
2812+ if (!isNaN(insertAt) && (insertAt < 0 || insertAt === Number(-0))) {
2813+ // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)
2814+ insertAt = chat.length + insertAt;
2815+ }
27412816
27422817 if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) {
27432818 chat.splice(insertAt, 0, message);
@@ -2784,7 +2859,13 @@ export async function sendNarratorMessage(args, text) {
27842859 },
27852860 };
27862861
27872862 constlet insertAt = Number(args.at);
2863+
2864+ // Convert possible depth parameter to index
2865+ if (!isNaN(insertAt) && (insertAt < 0 || insertAt === Number(-0))) {
2866+ // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)
2867+ insertAt = chat.length + insertAt;
2868+ }
27882869
27892870 if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) {
27902871 chat.splice(insertAt, 0, message);
@@ -2866,7 +2947,13 @@ async function sendCommentMessage(args, text) {
28662947 },
28672948 };
28682949
28692950 constlet insertAt = Number(args.at);
2951+
2952+ // Convert possible depth parameter to index
2953+ if (!isNaN(insertAt) && (insertAt < 0 || insertAt === Number(-0))) {
2954+ // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)
2955+ insertAt = chat.length + insertAt;
2956+ }
28702957
28712958 if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) {
28722959 chat.splice(insertAt, 0, message);
public/scripts/slash-commands/SlashCommandArgument.js+5 -5
@@ -59,7 +59,7 @@ export class SlashCommandArgument {
5959 * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} enums
6060 * @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} enumProvider function that returns auto complete options
6161 */
6262 constructor(description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], enumProvider = null, forceEnum = truefalse) {
6363 this.description = description;
6464 this.typeList = types ? Array.isArray(types) ? types : [types] : [];
6565 this.isRequired = isRequired ?? false;
@@ -90,7 +90,7 @@ export class SlashCommandNamedArgument extends SlashCommandArgument {
9090 * @param {string|SlashCommandClosure} [props.defaultValue=null] default value if no value is provided
9191 * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList=[]] list of accepted values
9292 * @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} [props.enumProvider=null] function that returns auto complete options
9393 * @param {boolean} [props.forceEnum=truefalse] default: truefalse - whether the input must match one of the enum values
9494 */
9595 static fromProps(props) {
9696 return new SlashCommandNamedArgument(
@@ -103,7 +103,7 @@ export class SlashCommandNamedArgument extends SlashCommandArgument {
103103 props.enumList ?? [],
104104 props.aliasList ?? [],
105105 props.enumProvider ?? null,
106106 props.forceEnum ?? truefalse,
107107 );
108108 }
109109
@@ -120,9 +120,9 @@ export class SlashCommandNamedArgument extends SlashCommandArgument {
120120 * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [enums=[]]
121121 * @param {string[]} [aliases=[]]
122122 * @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} [enumProvider=null] function that returns auto complete options
123123 * @param {boolean} [forceEnum=truefalse]
124124 */
125125 constructor(name, description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], aliases = [], enumProvider = null, forceEnum = truefalse) {
126126 super(description, types, isRequired, acceptsMultiple, defaultValue, enums, enumProvider, forceEnum);
127127 this.name = name;
128128 this.aliasList = aliases ? Array.isArray(aliases) ? aliases : [aliases] : [];
src/endpoints/openai.js+6 -2
@@ -43,7 +43,11 @@ router.post('/caption-image', jsonParser, async (request, response) => {
4343 key = readSecret(request.user.directories, SECRET_KEYS.KOBOLDCPP);
4444 }
4545
46- if (!key && !request.body.reverse_proxy && ['custom', 'ooba', 'koboldcpp'].includes(request.body.api) === false) {
46+ if (request.body.api === 'vllm') {
47+ key = readSecret(request.user.directories, SECRET_KEYS.VLLM);
48+ }
49+
50+ if (!key && !request.body.reverse_proxy && ['custom', 'ooba', 'koboldcpp', 'vllm'].includes(request.body.api) === false) {
4751 console.log('No key found for API', request.body.api);
4852 return response.sendStatus(400);
4953 }
@@ -110,7 +114,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
110114 });
111115 }
112116
113117 if (request.body.api === 'koboldcpp' || request.body.api === 'vllm') {
114118 apiUrl = `${trimV1(request.body.server_url)}/v1/chat/completions`;
115119 }
116120