Merge branch 'staging' into swipe-nums-for-all-mes

77470502339c829640eeab9fe78ff66c7395acf5

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

16 files changed, +743 -305Ignore whitespace
UpdateAndStart.bat+12 -3
@@ -2,17 +2,26 @@
2pushd %~dp02pushd %~dp0
3git --version > nul 2>&13git --version > nul 2>&1
4if %errorlevel% neq 0 (4if %errorlevel% neq 0 (
5 echo Git is not installed on this system. Skipping update.5 echo Git is not installed on this system.
6 echo If you installed with a zip file, you will need to download the new zip and install it manually.6 echo Install it from https://git-scm.com/downloads
7 goto end
7) else (8) else (
9 if not exist .git (
10 echo Not running from a Git repository. Reinstall using an officially supported method to get updates.
11 echo See: https://docs.sillytavern.app/installation/windows/
12 goto end
13 )
8 call git pull --rebase --autostash14 call git pull --rebase --autostash
9 if %errorlevel% neq 0 (15 if %errorlevel% neq 0 (
10 REM incase there is still something wrong16 REM incase there is still something wrong
11 echo There were errors while updating. Please download the latest version manually.17 echo There were errors while updating.
18 echo See the update FAQ at https://docs.sillytavern.app/usage/update/#common-update-problems
19 goto end
12 )20 )
13)21)
14set NODE_ENV=production22set NODE_ENV=production
15call npm install --no-audit --no-fund --loglevel=error --no-progress --omit=dev23call npm install --no-audit --no-fund --loglevel=error --no-progress --omit=dev
16node server.js %*24node server.js %*
25:end
17pause26pause
18popd27popd
UpdateForkAndStart.bat+10 -3
@@ -5,8 +5,14 @@ pushd %~dp0
5echo Checking Git installation5echo Checking Git installation
6git --version > nul 2>&16git --version > nul 2>&1
7if %errorlevel% neq 0 (7if %errorlevel% neq 0 (
8 echo Git is not installed on this system. Skipping update.8 echo Git is not installed on this system.
9 echo If you installed with a zip file, you will need to download the new zip and install it manually.9 echo Install it from https://git-scm.com/downloads
10 goto end
11)
12
13if not exist .git (
14 echo Not running from a Git repository. Reinstall using an officially supported method to get updates.
15 echo See: https://docs.sillytavern.app/installation/windows/
10 goto end16 goto end
11)17)
1218
@@ -89,7 +95,8 @@ git pull --rebase --autostash origin %TARGET_BRANCH%
8995
90:install96:install
91if %errorlevel% neq 0 (97if %errorlevel% neq 0 (
92 echo There were errors while updating. Please check manually.98 echo There were errors while updating.
99 echo See the update FAQ at https://docs.sillytavern.app/usage/update/#common-update-problems
93 goto end100 goto end
94)101)
95102
default/config.yaml+0 -1
@@ -118,7 +118,6 @@ extras:
118 classificationModel: Cohee/distilbert-base-uncased-go-emotions-onnx118 classificationModel: Cohee/distilbert-base-uncased-go-emotions-onnx
119 captioningModel: Xenova/vit-gpt2-image-captioning119 captioningModel: Xenova/vit-gpt2-image-captioning
120 embeddingModel: Cohee/jina-embeddings-v2-base-en120 embeddingModel: Cohee/jina-embeddings-v2-base-en
121 promptExpansionModel: Cohee/fooocus_expansion-onnx
122 speechToTextModel: Xenova/whisper-small121 speechToTextModel: Xenova/whisper-small
123 textToSpeechModel: Xenova/speecht5_tts122 textToSpeechModel: Xenova/speecht5_tts
124# -- OPENAI CONFIGURATION --123# -- OPENAI CONFIGURATION --
public/script.js+3 -1
@@ -4797,7 +4797,7 @@ export function removeMacros(str) {
4797 * @param {boolean} [compact] Send as a compact display message.4797 * @param {boolean} [compact] Send as a compact display message.
4798 * @param {string} [name] Name of the user sending the message. Defaults to name1.4798 * @param {string} [name] Name of the user sending the message. Defaults to name1.
4799 * @param {string} [avatar] Avatar of the user sending the message. Defaults to user_avatar.4799 * @param {string} [avatar] Avatar of the user sending the message. Defaults to user_avatar.
4800 * @returns {Promise<void>} A promise that resolves when the message is inserted.4800 * @returns {Promise<any>} A promise that resolves to the message when it is inserted.
4801 */4801 */
4802export async function sendMessageAsUser(messageText, messageBias, insertAt = null, compact = false, name = name1, avatar = user_avatar) {4802export async function sendMessageAsUser(messageText, messageBias, insertAt = null, compact = false, name = name1, avatar = user_avatar) {
4803 messageText = getRegexedString(messageText, regex_placement.USER_INPUT);4803 messageText = getRegexedString(messageText, regex_placement.USER_INPUT);
@@ -4844,6 +4844,8 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul
4844 await eventSource.emit(event_types.USER_MESSAGE_RENDERED, chat_id);4844 await eventSource.emit(event_types.USER_MESSAGE_RENDERED, chat_id);
4845 await saveChatConditional();4845 await saveChatConditional();
4846 }4846 }
4847
4848 return message;
4847}4849}
48484850
4849/**4851/**
public/scripts/extensions/expressions/index.js+33 -7
@@ -12,6 +12,8 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '
12import { isFunctionCallingSupported } from '../../openai.js';12import { isFunctionCallingSupported } from '../../openai.js';
13import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';13import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
14import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';14import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
15import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';
16import { SlashCommandClosure } from '../../slash-commands/SlashCommandClosure.js';
15export { MODULE_NAME };17export { MODULE_NAME };
1618
17const MODULE_NAME = 'expressions';19const MODULE_NAME = 'expressions';
@@ -2134,18 +2136,42 @@ function migrateSettings() {
2134 name: 'classify-expressions',2136 name: 'classify-expressions',
2135 aliases: ['expressions'],2137 aliases: ['expressions'],
2136 callback: async (args) => {2138 callback: async (args) => {
2137 const list = await getExpressionsList();2139 /** @type {import('../../slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */
2138 switch (String(args.format).toLowerCase()) {2140 // @ts-ignore
2139 case 'json':2141 let returnType = args.return;
2140 return JSON.stringify(list);2142
2141 default:2143 // Old legacy return type handling
2142 return list.join(', ');2144 if (args.format) {
2145 toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning');
2146 const type = String(args?.format).toLowerCase().trim();
2147 switch (type) {
2148 case 'json':
2149 returnType = 'object';
2150 break;
2151 default:
2152 returnType = 'pipe';
2153 break;
2154 }
2143 }2155 }
2156
2157 // Now the actual new return type handling
2158 const list = await getExpressionsList();
2159
2160 return await slashCommandReturnHelper.doReturn(returnType ?? 'pipe', list, { objectToStringFunc: list => list.join(', ') });
2144 },2161 },
2145 namedArgumentList: [2162 namedArgumentList: [
2146 SlashCommandNamedArgument.fromProps({2163 SlashCommandNamedArgument.fromProps({
2164 name: 'return',
2165 description: 'The way how you want the return value to be provided',
2166 typeList: [ARGUMENT_TYPE.STRING],
2167 defaultValue: 'pipe',
2168 enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
2169 forceEnum: true,
2170 }),
2171 // TODO remove some day
2172 SlashCommandNamedArgument.fromProps({
2147 name: 'format',2173 name: 'format',
2148 description: 'The format to return the list in: comma-separated plain text or JSON array. Default is plain text.',2174 description: '!!! DEPRECATED - use "return" instead !!! The format to return the list in: comma-separated plain text or JSON array. Default is plain text.',
2149 typeList: [ARGUMENT_TYPE.STRING],2175 typeList: [ARGUMENT_TYPE.STRING],
2150 enumList: [2176 enumList: [
2151 new SlashCommandEnumValue('plain', null, enumTypes.enum, ', '),2177 new SlashCommandEnumValue('plain', null, enumTypes.enum, ', '),
public/scripts/extensions/stable-diffusion/index.js+326 -59
@@ -20,7 +20,7 @@ import {
20} from '../../../script.js';20} from '../../../script.js';
21import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules, renderExtensionTemplateAsync, writeExtensionField } from '../../extensions.js';21import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules, renderExtensionTemplateAsync, writeExtensionField } from '../../extensions.js';
22import { selected_group } from '../../group-chats.js';22import { selected_group } from '../../group-chats.js';
23import { stringFormat, initScrollHeight, resetScrollHeight, getCharaFilename, saveBase64AsFile, getBase64Async, delay, isTrueBoolean, debounce } from '../../utils.js';23import { stringFormat, initScrollHeight, resetScrollHeight, getCharaFilename, saveBase64AsFile, getBase64Async, delay, isTrueBoolean, debounce, isFalseBoolean } from '../../utils.js';
24import { getMessageTimeStamp, humanizedDateTime } from '../../RossAscends-mods.js';24import { getMessageTimeStamp, humanizedDateTime } from '../../RossAscends-mods.js';
25import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js';25import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js';
26import { getNovelUnlimitedImageGeneration, getNovelAnlas, loadNovelSubscriptionData } from '../../nai-settings.js';26import { getNovelUnlimitedImageGeneration, getNovelAnlas, loadNovelSubscriptionData } from '../../nai-settings.js';
@@ -31,6 +31,7 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '
31import { debounce_timeout } from '../../constants.js';31import { debounce_timeout } from '../../constants.js';
32import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';32import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
33import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';33import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';
34import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
34export { MODULE_NAME };35export { MODULE_NAME };
3536
36const MODULE_NAME = 'sd';37const MODULE_NAME = 'sd';
@@ -221,7 +222,6 @@ const defaultSettings = {
221222
222 // Refine mode223 // Refine mode
223 refine_mode: false,224 refine_mode: false,
224 expand: false,
225 interactive_mode: false,225 interactive_mode: false,
226 multimodal_captioning: false,226 multimodal_captioning: false,
227 snap: false,227 snap: false,
@@ -240,7 +240,7 @@ const defaultSettings = {
240 drawthings_auth: '',240 drawthings_auth: '',
241241
242 hr_upscaler: 'Latent',242 hr_upscaler: 'Latent',
243 hr_scale: 2.0,243 hr_scale: 1.0,
244 hr_scale_min: 1.0,244 hr_scale_min: 1.0,
245 hr_scale_max: 4.0,245 hr_scale_max: 4.0,
246 hr_scale_step: 0.1,246 hr_scale_step: 0.1,
@@ -260,10 +260,6 @@ const defaultSettings = {
260 clip_skip: 1,260 clip_skip: 1,
261261
262 // NovelAI settings262 // NovelAI settings
263 novel_upscale_ratio_min: 1.0,
264 novel_upscale_ratio_max: 4.0,
265 novel_upscale_ratio_step: 0.1,
266 novel_upscale_ratio: 1.0,
267 novel_anlas_guard: false,263 novel_anlas_guard: false,
268 novel_sm: false,264 novel_sm: false,
269 novel_sm_dyn: false,265 novel_sm_dyn: false,
@@ -416,7 +412,6 @@ async function loadSettings() {
416 $('#sd_hr_scale').val(extension_settings.sd.hr_scale).trigger('input');412 $('#sd_hr_scale').val(extension_settings.sd.hr_scale).trigger('input');
417 $('#sd_denoising_strength').val(extension_settings.sd.denoising_strength).trigger('input');413 $('#sd_denoising_strength').val(extension_settings.sd.denoising_strength).trigger('input');
418 $('#sd_hr_second_pass_steps').val(extension_settings.sd.hr_second_pass_steps).trigger('input');414 $('#sd_hr_second_pass_steps').val(extension_settings.sd.hr_second_pass_steps).trigger('input');
419 $('#sd_novel_upscale_ratio').val(extension_settings.sd.novel_upscale_ratio).trigger('input');
420 $('#sd_novel_anlas_guard').prop('checked', extension_settings.sd.novel_anlas_guard);415 $('#sd_novel_anlas_guard').prop('checked', extension_settings.sd.novel_anlas_guard);
421 $('#sd_novel_sm').prop('checked', extension_settings.sd.novel_sm);416 $('#sd_novel_sm').prop('checked', extension_settings.sd.novel_sm);
422 $('#sd_novel_sm_dyn').prop('checked', extension_settings.sd.novel_sm_dyn);417 $('#sd_novel_sm_dyn').prop('checked', extension_settings.sd.novel_sm_dyn);
@@ -430,7 +425,6 @@ async function loadSettings() {
430 $('#sd_restore_faces').prop('checked', extension_settings.sd.restore_faces);425 $('#sd_restore_faces').prop('checked', extension_settings.sd.restore_faces);
431 $('#sd_enable_hr').prop('checked', extension_settings.sd.enable_hr);426 $('#sd_enable_hr').prop('checked', extension_settings.sd.enable_hr);
432 $('#sd_refine_mode').prop('checked', extension_settings.sd.refine_mode);427 $('#sd_refine_mode').prop('checked', extension_settings.sd.refine_mode);
433 $('#sd_expand').prop('checked', extension_settings.sd.expand);
434 $('#sd_multimodal_captioning').prop('checked', extension_settings.sd.multimodal_captioning);428 $('#sd_multimodal_captioning').prop('checked', extension_settings.sd.multimodal_captioning);
435 $('#sd_auto_url').val(extension_settings.sd.auto_url);429 $('#sd_auto_url').val(extension_settings.sd.auto_url);
436 $('#sd_auto_auth').val(extension_settings.sd.auto_auth);430 $('#sd_auto_auth').val(extension_settings.sd.auto_auth);
@@ -644,37 +638,13 @@ async function onSaveStyleClick() {
644 saveSettingsDebounced();638 saveSettingsDebounced();
645}639}
646640
647async function expandPrompt(prompt) {
648 try {
649 const response = await fetch('/api/sd/expand', {
650 method: 'POST',
651 headers: getRequestHeaders(),
652 body: JSON.stringify({ prompt: prompt }),
653 });
654
655 if (!response.ok) {
656 throw new Error('API returned an error.');
657 }
658
659 const data = await response.json();
660 return data.prompt;
661 } catch {
662 return prompt;
663 }
664}
665
666/**641/**
667 * Modifies prompt based on auto-expansion and user inputs.642 * Modifies prompt based on user inputs.
668 * @param {string} prompt Prompt to refine643 * @param {string} prompt Prompt to refine
669 * @param {boolean} allowExpand Whether to allow auto-expansion
670 * @param {boolean} isNegative Whether the prompt is a negative one644 * @param {boolean} isNegative Whether the prompt is a negative one
671 * @returns {Promise<string>} Refined prompt645 * @returns {Promise<string>} Refined prompt
672 */646 */
673async function refinePrompt(prompt, allowExpand, isNegative = false) {647async function refinePrompt(prompt, isNegative) {
674 if (allowExpand && extension_settings.sd.expand) {
675 prompt = await expandPrompt(prompt);
676 }
677
678 if (extension_settings.sd.refine_mode) {648 if (extension_settings.sd.refine_mode) {
679 const text = isNegative ? '<h3>Review and edit the <i>negative</i> prompt:</h3>' : '<h3>Review and edit the prompt:</h3>';649 const text = isNegative ? '<h3>Review and edit the <i>negative</i> prompt:</h3>' : '<h3>Review and edit the prompt:</h3>';
680 const refinedPrompt = await callGenericPopup(text + 'Press "Cancel" to abort the image generation.', POPUP_TYPE.INPUT, prompt.trim(), { rows: 5, okButton: 'Continue' });650 const refinedPrompt = await callGenericPopup(text + 'Press "Cancel" to abort the image generation.', POPUP_TYPE.INPUT, prompt.trim(), { rows: 5, okButton: 'Continue' });
@@ -800,11 +770,6 @@ function combinePrefixes(str1, str2, macro = '') {
800 return process(result);770 return process(result);
801}771}
802772
803function onExpandInput() {
804 extension_settings.sd.expand = !!$(this).prop('checked');
805 saveSettingsDebounced();
806}
807
808function onRefineModeInput() {773function onRefineModeInput() {
809 extension_settings.sd.refine_mode = !!$('#sd_refine_mode').prop('checked');774 extension_settings.sd.refine_mode = !!$('#sd_refine_mode').prop('checked');
810 saveSettingsDebounced();775 saveSettingsDebounced();
@@ -969,12 +934,6 @@ async function onViewAnlasClick() {
969 toastr.info(`Free image generation: ${unlimitedGeneration ? 'Yes' : 'No'}`, `Anlas: ${anlas}`);934 toastr.info(`Free image generation: ${unlimitedGeneration ? 'Yes' : 'No'}`, `Anlas: ${anlas}`);
970}935}
971936
972function onNovelUpscaleRatioInput() {
973 extension_settings.sd.novel_upscale_ratio = Number($('#sd_novel_upscale_ratio').val());
974 $('#sd_novel_upscale_ratio_value').val(extension_settings.sd.novel_upscale_ratio.toFixed(1));
975 saveSettingsDebounced();
976}
977
978function onNovelAnlasGuardInput() {937function onNovelAnlasGuardInput() {
979 extension_settings.sd.novel_anlas_guard = !!$('#sd_novel_anlas_guard').prop('checked');938 extension_settings.sd.novel_anlas_guard = !!$('#sd_novel_anlas_guard').prop('checked');
980 saveSettingsDebounced();939 saveSettingsDebounced();
@@ -2273,6 +2232,25 @@ function getRawLastMessage() {
2273}2232}
22742233
2275/**2234/**
2235 * Ensure that the selected option exists in the dropdown.
2236 * @param {string} setting Setting key
2237 * @param {string} selector Dropdown selector
2238 * @returns {void}
2239 */
2240function ensureSelectionExists(setting, selector) {
2241 /** @type {HTMLSelectElement} */
2242 const selectElement = document.querySelector(selector);
2243 if (!selectElement) {
2244 return;
2245 }
2246 const options = Array.from(selectElement.options);
2247 const value = extension_settings.sd[setting];
2248 if (selectElement.selectedOptions.length && !options.some(option => option.value === value)) {
2249 extension_settings.sd[setting] = selectElement.selectedOptions[0].value;
2250 }
2251}
2252
2253/**
2276 * Generates an image based on the given trigger word.2254 * Generates an image based on the given trigger word.
2277 * @param {string} initiator The initiator of the image generation2255 * @param {string} initiator The initiator of the image generation
2278 * @param {Record<string, object>} args Command arguments2256 * @param {Record<string, object>} args Command arguments
@@ -2292,8 +2270,8 @@ async function generatePicture(initiator, args, trigger, message, callback) {
2292 return;2270 return;
2293 }2271 }
22942272
2295 extension_settings.sd.sampler = $('#sd_sampler').find(':selected').val();2273 ensureSelectionExists('sampler', '#sd_sampler');
2296 extension_settings.sd.model = $('#sd_model').find(':selected').val();2274 ensureSelectionExists('model', '#sd_model');
22972275
2298 trigger = trigger.trim();2276 trigger = trigger.trim();
2299 const generationType = getGenerationType(trigger);2277 const generationType = getGenerationType(trigger);
@@ -2441,7 +2419,7 @@ async function getPrompt(generationType, message, trigger, quietPrompt, combineN
2441 }2419 }
24422420
2443 if (generationType !== generationMode.FREE) {2421 if (generationType !== generationMode.FREE) {
2444 prompt = await refinePrompt(prompt, true);2422 prompt = await refinePrompt(prompt, false);
2445 }2423 }
24462424
2447 return prompt;2425 return prompt;
@@ -2469,7 +2447,7 @@ function generateFreeModePrompt(trigger, combineNegatives) {
2469 return message.original_avatar.replace(/\.[^/.]+$/, '');2447 return message.original_avatar.replace(/\.[^/.]+$/, '');
2470 }2448 }
2471 }2449 }
2472 throw new Error('No usable messages found.');2450 return '';
2473 };2451 };
24742452
2475 const key = getLastCharacterKey();2453 const key = getLastCharacterKey();
@@ -3031,7 +3009,7 @@ async function generateNovelImage(prompt, negativePrompt, signal) {
3031 width: width,3009 width: width,
3032 height: height,3010 height: height,
3033 negative_prompt: negativePrompt,3011 negative_prompt: negativePrompt,
3034 upscale_ratio: extension_settings.sd.novel_upscale_ratio,3012 upscale_ratio: extension_settings.sd.hr_scale,
3035 decrisper: extension_settings.sd.novel_decrisper,3013 decrisper: extension_settings.sd.novel_decrisper,
3036 sm: sm,3014 sm: sm,
3037 sm_dyn: sm_dyn,3015 sm_dyn: sm_dyn,
@@ -3613,8 +3591,8 @@ async function sdMessageButton(e) {
3613 try {3591 try {
3614 setBusyIcon(true);3592 setBusyIcon(true);
3615 if (hasSavedImage) {3593 if (hasSavedImage) {
3616 const prompt = await refinePrompt(message.extra.title, false, false);3594 const prompt = await refinePrompt(message.extra.title, false);
3617 const negative = hasSavedNegative ? await refinePrompt(message.extra.negative, false, true) : '';3595 const negative = hasSavedNegative ? await refinePrompt(message.extra.negative, true) : '';
3618 message.extra.title = prompt;3596 message.extra.title = prompt;
36193597
3620 const generationType = message?.extra?.generationType ?? generationMode.FREE;3598 const generationType = message?.extra?.generationType ?? generationMode.FREE;
@@ -3756,8 +3734,8 @@ async function onImageSwiped({ message, element, direction }) {
3756 eventSource.once(CUSTOM_STOP_EVENT, stopListener);3734 eventSource.once(CUSTOM_STOP_EVENT, stopListener);
3757 const callback = () => { };3735 const callback = () => { };
3758 const hasNegative = message.extra.negative;3736 const hasNegative = message.extra.negative;
3759 const prompt = await refinePrompt(message.extra.title, false, false);3737 const prompt = await refinePrompt(message.extra.title, false);
3760 const negativePromptPrefix = hasNegative ? await refinePrompt(message.extra.negative, false, true) : '';3738 const negativePromptPrefix = hasNegative ? await refinePrompt(message.extra.negative, true) : '';
3761 const characterName = context.groupId3739 const characterName = context.groupId
3762 ? context.groups[Object.keys(context.groups).filter(x => context.groups[x].id === context.groupId)[0]]?.id?.toString()3740 ? context.groups[Object.keys(context.groups).filter(x => context.groups[x].id === context.groupId)[0]]?.id?.toString()
3763 : context.characters[context.characterId]?.name;3741 : context.characters[context.characterId]?.name;
@@ -3788,12 +3766,85 @@ async function onImageSwiped({ message, element, direction }) {
3788 await context.saveChat();3766 await context.saveChat();
3789}3767}
37903768
3769/**
3770 * Applies the command arguments to the extension settings.
3771 * @typedef {import('../../slash-commands/SlashCommand.js').NamedArguments} NamedArguments
3772 * @typedef {import('../../slash-commands/SlashCommand.js').NamedArgumentsCapture} NamedArgumentsCapture
3773 * @param {NamedArguments | NamedArgumentsCapture} args - Command arguments
3774 * @returns {Record<string, any>} - Current settings before applying the command arguments
3775 */
3776function applyCommandArguments(args) {
3777 const overrideSettings = {};
3778 const currentSettings = {};
3779 const settingMap = {
3780 'edit': 'refine_mode',
3781 'extend': 'free_extend',
3782 'multimodal': 'multimodal_captioning',
3783 'seed': 'seed',
3784 'width': 'width',
3785 'height': 'height',
3786 'steps': 'steps',
3787 'cfg': 'scale',
3788 'skip': 'clip_skip',
3789 'model': 'model',
3790 'sampler': 'sampler',
3791 'scheduler': 'scheduler',
3792 'vae': 'vae',
3793 'upscaler': 'hr_upscaler',
3794 'scale': 'hr_scale',
3795 'hires': 'enable_hr',
3796 'denoise': 'denoising_strength',
3797 '2ndpass': 'hr_second_pass_steps',
3798 'faces': 'restore_faces',
3799 };
3800
3801 for (const [param, setting] of Object.entries(settingMap)) {
3802 if (args[param] === undefined || defaultSettings[setting] === undefined) {
3803 continue;
3804 }
3805 currentSettings[setting] = extension_settings.sd[setting];
3806 const value = String(args[param]);
3807 const type = typeof defaultSettings[setting];
3808 switch (type) {
3809 case 'boolean':
3810 overrideSettings[setting] = isTrueBoolean(value) || !isFalseBoolean(value);
3811 break;
3812 case 'number':
3813 overrideSettings[setting] = Number(value);
3814 break;
3815 default:
3816 overrideSettings[setting] = value;
3817 break;
3818 }
3819 }
3820
3821 Object.assign(extension_settings.sd, overrideSettings);
3822 return currentSettings;
3823}
3824
3791jQuery(async () => {3825jQuery(async () => {
3792 await addSDGenButtons();3826 await addSDGenButtons();
37933827
3828 const getSelectEnumProvider = (id, text) => () => Array.from(document.querySelectorAll(`#${id} > [value]`)).map(x => new SlashCommandEnumValue(x.getAttribute('value'), text ? x.textContent : null));
3829
3794 SlashCommandParser.addCommandObject(SlashCommand.fromProps({3830 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3795 name: 'imagine',3831 name: 'imagine',
3796 callback: (args, trigger) => generatePicture(initiators.command, args, String(trigger)),3832 returns: 'URL of the generated image, or an empty string if the generation failed',
3833 callback: async (args, trigger) => {
3834 const currentSettings = applyCommandArguments(args);
3835
3836 try {
3837 return await generatePicture(initiators.command, args, String(trigger));
3838 } catch (error) {
3839 console.error('Failed to generate image:', error);
3840 return '';
3841 } finally {
3842 if (Object.keys(currentSettings).length) {
3843 Object.assign(extension_settings.sd, currentSettings);
3844 saveSettingsDebounced();
3845 }
3846 }
3847 },
3797 aliases: ['sd', 'img', 'image'],3848 aliases: ['sd', 'img', 'image'],
3798 namedArgumentList: [3849 namedArgumentList: [
3799 new SlashCommandNamedArgument(3850 new SlashCommandNamedArgument(
@@ -3803,6 +3854,164 @@ jQuery(async () => {
3803 name: 'negative',3854 name: 'negative',
3804 description: 'negative prompt prefix',3855 description: 'negative prompt prefix',
3805 typeList: [ARGUMENT_TYPE.STRING],3856 typeList: [ARGUMENT_TYPE.STRING],
3857 isRequired: false,
3858 acceptsMultiple: false,
3859 }),
3860 SlashCommandNamedArgument.fromProps({
3861 name: 'extend',
3862 description: 'auto-extend free mode prompts with the LLM',
3863 typeList: [ARGUMENT_TYPE.BOOLEAN],
3864 enumProvider: commonEnumProviders.boolean('trueFalse'),
3865 isRequired: false,
3866 acceptsMultiple: false,
3867 }),
3868 SlashCommandNamedArgument.fromProps({
3869 name: 'edit',
3870 description: 'edit the prompt before generation',
3871 typeList: [ARGUMENT_TYPE.BOOLEAN],
3872 enumProvider: commonEnumProviders.boolean('trueFalse'),
3873 isRequired: false,
3874 acceptsMultiple: false,
3875 }),
3876 SlashCommandNamedArgument.fromProps({
3877 name: 'multimodal',
3878 description: 'use multimodal captioning (for portraits only)',
3879 typeList: [ARGUMENT_TYPE.BOOLEAN],
3880 enumProvider: commonEnumProviders.boolean('trueFalse'),
3881 isRequired: false,
3882 acceptsMultiple: false,
3883 }),
3884 SlashCommandNamedArgument.fromProps({
3885 name: 'snap',
3886 description: 'snap auto-adjusted dimensions to the nearest known resolution (portraits and backgrounds only)',
3887 typeList: [ARGUMENT_TYPE.BOOLEAN],
3888 enumProvider: commonEnumProviders.boolean('trueFalse'),
3889 isRequired: false,
3890 acceptsMultiple: false,
3891 }),
3892 SlashCommandNamedArgument.fromProps({
3893 name: 'seed',
3894 description: 'random seed',
3895 isRequired: false,
3896 typeList: [ARGUMENT_TYPE.NUMBER],
3897 acceptsMultiple: false,
3898 }),
3899 SlashCommandNamedArgument.fromProps({
3900 name: 'width',
3901 description: 'image width',
3902 isRequired: false,
3903 typeList: [ARGUMENT_TYPE.NUMBER],
3904 acceptsMultiple: false,
3905 }),
3906 SlashCommandNamedArgument.fromProps({
3907 name: 'height',
3908 description: 'image height',
3909 isRequired: false,
3910 typeList: [ARGUMENT_TYPE.NUMBER],
3911 acceptsMultiple: false,
3912 }),
3913 SlashCommandNamedArgument.fromProps({
3914 name: 'steps',
3915 description: 'number of steps',
3916 isRequired: false,
3917 typeList: [ARGUMENT_TYPE.NUMBER],
3918 acceptsMultiple: false,
3919 }),
3920 SlashCommandNamedArgument.fromProps({
3921 name: 'cfg',
3922 description: 'CFG scale',
3923 isRequired: false,
3924 typeList: [ARGUMENT_TYPE.NUMBER],
3925 acceptsMultiple: false,
3926 }),
3927 SlashCommandNamedArgument.fromProps({
3928 name: 'skip',
3929 description: 'CLIP skip layers',
3930 isRequired: false,
3931 typeList: [ARGUMENT_TYPE.NUMBER],
3932 acceptsMultiple: false,
3933 }),
3934 SlashCommandNamedArgument.fromProps({
3935 name: 'model',
3936 description: 'model override',
3937 isRequired: false,
3938 typeList: [ARGUMENT_TYPE.STRING],
3939 acceptsMultiple: false,
3940 forceEnum: true,
3941 enumProvider: getSelectEnumProvider('sd_model', true),
3942 }),
3943 SlashCommandNamedArgument.fromProps({
3944 name: 'sampler',
3945 description: 'sampler override',
3946 isRequired: false,
3947 typeList: [ARGUMENT_TYPE.STRING],
3948 acceptsMultiple: false,
3949 forceEnum: true,
3950 enumProvider: getSelectEnumProvider('sd_sampler', false),
3951 }),
3952 SlashCommandNamedArgument.fromProps({
3953 name: 'scheduler',
3954 description: 'scheduler override',
3955 isRequired: false,
3956 typeList: [ARGUMENT_TYPE.STRING],
3957 acceptsMultiple: false,
3958 forceEnum: true,
3959 enumProvider: getSelectEnumProvider('sd_scheduler', false),
3960 }),
3961 SlashCommandNamedArgument.fromProps({
3962 name: 'vae',
3963 description: 'VAE name override',
3964 isRequired: false,
3965 typeList: [ARGUMENT_TYPE.STRING],
3966 acceptsMultiple: false,
3967 forceEnum: true,
3968 enumProvider: getSelectEnumProvider('sd_vae', false),
3969 }),
3970 SlashCommandNamedArgument.fromProps({
3971 name: 'upscaler',
3972 description: 'upscaler override',
3973 isRequired: false,
3974 typeList: [ARGUMENT_TYPE.STRING],
3975 acceptsMultiple: false,
3976 forceEnum: true,
3977 enumProvider: getSelectEnumProvider('sd_hr_upscaler', false),
3978 }),
3979 SlashCommandNamedArgument.fromProps({
3980 name: 'hires',
3981 description: 'enable high-res fix',
3982 isRequired: false,
3983 typeList: [ARGUMENT_TYPE.BOOLEAN],
3984 acceptsMultiple: false,
3985 enumProvider: commonEnumProviders.boolean('trueFalse'),
3986 }),
3987 SlashCommandNamedArgument.fromProps({
3988 name: 'scale',
3989 description: 'upscale amount',
3990 isRequired: false,
3991 typeList: [ARGUMENT_TYPE.NUMBER],
3992 acceptsMultiple: false,
3993 }),
3994 SlashCommandNamedArgument.fromProps({
3995 name: 'denoise',
3996 description: 'denoising strength',
3997 isRequired: false,
3998 typeList: [ARGUMENT_TYPE.NUMBER],
3999 acceptsMultiple: false,
4000 }),
4001 SlashCommandNamedArgument.fromProps({
4002 name: '2ndpass',
4003 description: 'second pass steps',
4004 isRequired: false,
4005 typeList: [ARGUMENT_TYPE.NUMBER],
4006 acceptsMultiple: false,
4007 }),
4008 SlashCommandNamedArgument.fromProps({
4009 name: 'faces',
4010 description: 'restore faces',
4011 isRequired: false,
4012 typeList: [ARGUMENT_TYPE.BOOLEAN],
4013 acceptsMultiple: false,
4014 enumProvider: commonEnumProviders.boolean('trueFalse'),
3806 }),4015 }),
3807 ],4016 ],
3808 unnamedArgumentList: [4017 unnamedArgumentList: [
@@ -3824,6 +4033,66 @@ jQuery(async () => {
3824 }));4033 }));
38254034
3826 SlashCommandParser.addCommandObject(SlashCommand.fromProps({4035 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
4036 name: 'imagine-source',
4037 aliases: ['sd-source', 'img-source'],
4038 returns: 'a name of the current generation source',
4039 unnamedArgumentList: [
4040 SlashCommandArgument.fromProps({
4041 description: 'source name',
4042 typeList: [ARGUMENT_TYPE.STRING],
4043 isRequired: false,
4044 forceEnum: true,
4045 enumProvider: getSelectEnumProvider('sd_source', true),
4046 }),
4047 ],
4048 helpString: 'If an argument is provided, change the source of the image generation, e.g. <code>/imagine-source comfy</code>. Returns the current source.',
4049 callback: async (_args, name) => {
4050 if (!name) {
4051 return extension_settings.sd.source;
4052 }
4053 const isKnownSource = Object.keys(sources).includes(String(name));
4054 if (!isKnownSource) {
4055 throw new Error('The value provided is not a valid image generation source.');
4056 }
4057 const option = document.querySelector(`#sd_source [value="${name}"]`);
4058 if (!(option instanceof HTMLOptionElement)) {
4059 throw new Error('Could not find the source option in the dropdown.');
4060 }
4061 option.selected = true;
4062 await onSourceChange();
4063 return extension_settings.sd.source;
4064 },
4065 }));
4066
4067 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
4068 name: 'imagine-style',
4069 aliases: ['sd-style', 'img-style'],
4070 returns: 'a name of the current style',
4071 unnamedArgumentList: [
4072 SlashCommandArgument.fromProps({
4073 description: 'style name',
4074 typeList: [ARGUMENT_TYPE.STRING],
4075 isRequired: false,
4076 forceEnum: true,
4077 enumProvider: getSelectEnumProvider('sd_style', false),
4078 }),
4079 ],
4080 helpString: 'If an argument is provided, change the style of the image generation, e.g. <code>/imagine-style MyStyle</code>. Returns the current style.',
4081 callback: async (_args, name) => {
4082 if (!name) {
4083 return extension_settings.sd.style;
4084 }
4085 const option = document.querySelector(`#sd_style [value="${name}"]`);
4086 if (!(option instanceof HTMLOptionElement)) {
4087 throw new Error('Could not find the style option in the dropdown.');
4088 }
4089 option.selected = true;
4090 onStyleSelect();
4091 return extension_settings.sd.style;
4092 },
4093 }));
4094
4095 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3827 name: 'imagine-comfy-workflow',4096 name: 'imagine-comfy-workflow',
3828 callback: changeComfyWorkflow,4097 callback: changeComfyWorkflow,
3829 aliases: ['icw'],4098 aliases: ['icw'],
@@ -3832,7 +4101,7 @@ jQuery(async () => {
3832 description: 'workflow name',4101 description: 'workflow name',
3833 typeList: [ARGUMENT_TYPE.STRING],4102 typeList: [ARGUMENT_TYPE.STRING],
3834 isRequired: true,4103 isRequired: true,
3835 enumProvider: () => Array.from(document.querySelectorAll('#sd_comfy_workflow > [value]')).map(x => x.getAttribute('value')).map(workflow => new SlashCommandEnumValue(workflow)),4104 enumProvider: getSelectEnumProvider('sd_comfy_workflow', false),
3836 }),4105 }),
3837 ],4106 ],
3838 helpString: '(workflowName) - change the workflow to be used for image generation with ComfyUI, e.g. <pre><code>/imagine-comfy-workflow MyWorkflow</code></pre>',4107 helpString: '(workflowName) - change the workflow to be used for image generation with ComfyUI, e.g. <pre><code>/imagine-comfy-workflow MyWorkflow</code></pre>',
@@ -3874,7 +4143,6 @@ jQuery(async () => {
3874 $('#sd_hr_scale').on('input', onHrScaleInput);4143 $('#sd_hr_scale').on('input', onHrScaleInput);
3875 $('#sd_denoising_strength').on('input', onDenoisingStrengthInput);4144 $('#sd_denoising_strength').on('input', onDenoisingStrengthInput);
3876 $('#sd_hr_second_pass_steps').on('input', onHrSecondPassStepsInput);4145 $('#sd_hr_second_pass_steps').on('input', onHrSecondPassStepsInput);
3877 $('#sd_novel_upscale_ratio').on('input', onNovelUpscaleRatioInput);
3878 $('#sd_novel_anlas_guard').on('input', onNovelAnlasGuardInput);4146 $('#sd_novel_anlas_guard').on('input', onNovelAnlasGuardInput);
3879 $('#sd_novel_view_anlas').on('click', onViewAnlasClick);4147 $('#sd_novel_view_anlas').on('click', onViewAnlasClick);
3880 $('#sd_novel_sm').on('input', onNovelSmInput);4148 $('#sd_novel_sm').on('input', onNovelSmInput);
@@ -3887,7 +4155,6 @@ jQuery(async () => {
3887 $('#sd_comfy_open_workflow_editor').on('click', onComfyOpenWorkflowEditorClick);4155 $('#sd_comfy_open_workflow_editor').on('click', onComfyOpenWorkflowEditorClick);
3888 $('#sd_comfy_new_workflow').on('click', onComfyNewWorkflowClick);4156 $('#sd_comfy_new_workflow').on('click', onComfyNewWorkflowClick);
3889 $('#sd_comfy_delete_workflow').on('click', onComfyDeleteWorkflowClick);4157 $('#sd_comfy_delete_workflow').on('click', onComfyDeleteWorkflowClick);
3890 $('#sd_expand').on('input', onExpandInput);
3891 $('#sd_style').on('change', onStyleSelect);4158 $('#sd_style').on('change', onStyleSelect);
3892 $('#sd_save_style').on('click', onSaveStyleClick);4159 $('#sd_save_style').on('click', onSaveStyleClick);
3893 $('#sd_delete_style').on('click', onDeleteStyleClick);4160 $('#sd_delete_style').on('click', onDeleteStyleClick);
public/scripts/extensions/stable-diffusion/settings.html+1 -14
@@ -27,11 +27,6 @@
27 <span data-i18n="sd_free_extend_txt">Extend free mode prompts</span>27 <span data-i18n="sd_free_extend_txt">Extend free mode prompts</span>
28 <small data-i18n="sd_free_extend_small">(interactive/commands)</small>28 <small data-i18n="sd_free_extend_small">(interactive/commands)</small>
29 </label>29 </label>
30 <label for="sd_expand" class="checkbox_label" data-i18n="[title]sd_expand" title="Automatically extend prompts using text generation model">
31 <input id="sd_expand" type="checkbox" />
32 <span data-i18n="sd_expand_txt">Auto-extend prompts</span>
33 <span class="right_menu_button fa-solid fa-triangle-exclamation" data-i18n="[title]sd_expand_warning" title="May produce unexpected results. Manual prompt editing is recommended."></span>
34 </label>
35 <label for="sd_snap" class="checkbox_label" data-i18n="[title]sd_snap" title="Snap generation requests with a forced aspect ratio (portraits, backgrounds) to the nearest known resolution, while trying to preserve the absolute pixel counts (recommended for SDXL).">30 <label for="sd_snap" class="checkbox_label" data-i18n="[title]sd_snap" title="Snap generation requests with a forced aspect ratio (portraits, backgrounds) to the nearest known resolution, while trying to preserve the absolute pixel counts (recommended for SDXL).">
36 <input id="sd_snap" type="checkbox" />31 <input id="sd_snap" type="checkbox" />
37 <span data-i18n="sd_snap_txt">Snap auto-adjusted resolutions</span>32 <span data-i18n="sd_snap_txt">Snap auto-adjusted resolutions</span>
@@ -308,7 +303,7 @@
308 </div>303 </div>
309304
310 <div class="flex-container">305 <div class="flex-container">
311 <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" data-sd-source="auto,vlad,drawthings">306 <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" data-sd-source="auto,vlad,drawthings,novel">
312 <small>307 <small>
313 <span data-i18n="Upscale by">Upscale by</span>308 <span data-i18n="Upscale by">Upscale by</span>
314 </small>309 </small>
@@ -332,14 +327,6 @@
332 <input class="neo-range-input" type="number" id="sd_hr_second_pass_steps_value" data-for="sd_hr_second_pass_steps" max="{{hr_second_pass_steps_max}}" step="{{hr_second_pass_steps_step}}" value="{{hr_second_pass_steps}}" >327 <input class="neo-range-input" type="number" id="sd_hr_second_pass_steps_value" data-for="sd_hr_second_pass_steps" max="{{hr_second_pass_steps_max}}" step="{{hr_second_pass_steps_step}}" value="{{hr_second_pass_steps}}" >
333 </div>328 </div>
334329
335 <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" data-sd-source="novel">
336 <small>
337 <span data-i18n="Upscale by">Upscale by</span>
338 </small>
339 <input class="neo-range-slider" type="range" id="sd_novel_upscale_ratio" name="sd_novel_upscale_ratio" min="{{novel_upscale_ratio_min}}" max="{{novel_upscale_ratio_max}}" step="{{novel_upscale_ratio_step}}" value="{{novel_upscale_ratio}}" >
340 <input class="neo-range-input" type="number" id="sd_novel_upscale_ratio_value" data-for="sd_novel_upscale_ratio" min="{{novel_upscale_ratio_min}}" max="{{novel_upscale_ratio_max}}" step="{{novel_upscale_ratio_step}}" value="{{novel_upscale_ratio}}" >
341 </div>
342
343 <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" data-sd-source="auto,vlad,comfy,horde,drawthings,extras">330 <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" data-sd-source="auto,vlad,comfy,horde,drawthings,extras">
344 <small>331 <small>
345 <span data-i18n="CLIP Skip">CLIP Skip</span>332 <span data-i18n="CLIP Skip">CLIP Skip</span>
public/scripts/instruct-mode.js+4 -0
@@ -384,6 +384,10 @@ export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvata
384 * @returns {string} Formatted instruct mode system prompt.384 * @returns {string} Formatted instruct mode system prompt.
385 */385 */
386export function formatInstructModeSystemPrompt(systemPrompt) {386export function formatInstructModeSystemPrompt(systemPrompt) {
387 if (!systemPrompt) {
388 return '';
389 }
390
387 const separator = power_user.instruct.wrap ? '\n' : '';391 const separator = power_user.instruct.wrap ? '\n' : '';
388392
389 if (power_user.instruct.system_sequence_prefix) {393 if (power_user.instruct.system_sequence_prefix) {
public/scripts/slash-commands.js+111 -56
@@ -70,6 +70,7 @@ import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
70import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';70import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
71import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';71import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
72import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';72import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
73import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';
73export {74export {
74 executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,75 executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
75};76};
@@ -242,6 +243,7 @@ export function initDefaultSlashCommands() {
242 SlashCommandParser.addCommandObject(SlashCommand.fromProps({243 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
243 name: 'sendas',244 name: 'sendas',
244 callback: sendMessageAs,245 callback: sendMessageAs,
246 returns: 'Optionally the text of the sent message, if specified in the "return" argument',
245 namedArgumentList: [247 namedArgumentList: [
246 SlashCommandNamedArgument.fromProps({248 SlashCommandNamedArgument.fromProps({
247 name: 'name',249 name: 'name',
@@ -269,6 +271,14 @@ export function initDefaultSlashCommands() {
269 typeList: [ARGUMENT_TYPE.NUMBER],271 typeList: [ARGUMENT_TYPE.NUMBER],
270 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),272 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
271 }),273 }),
274 SlashCommandNamedArgument.fromProps({
275 name: 'return',
276 description: 'The way how you want the return value to be provided',
277 typeList: [ARGUMENT_TYPE.STRING],
278 defaultValue: 'none',
279 enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
280 forceEnum: true,
281 }),
272 ],282 ],
273 unnamedArgumentList: [283 unnamedArgumentList: [
274 new SlashCommandArgument(284 new SlashCommandArgument(
@@ -301,6 +311,7 @@ export function initDefaultSlashCommands() {
301 name: 'sys',311 name: 'sys',
302 callback: sendNarratorMessage,312 callback: sendNarratorMessage,
303 aliases: ['nar'],313 aliases: ['nar'],
314 returns: 'Optionally the text of the sent message, if specified in the "return" argument',
304 namedArgumentList: [315 namedArgumentList: [
305 new SlashCommandNamedArgument(316 new SlashCommandNamedArgument(
306 'compact',317 'compact',
@@ -316,6 +327,14 @@ export function initDefaultSlashCommands() {
316 typeList: [ARGUMENT_TYPE.NUMBER],327 typeList: [ARGUMENT_TYPE.NUMBER],
317 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),328 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
318 }),329 }),
330 SlashCommandNamedArgument.fromProps({
331 name: 'return',
332 description: 'The way how you want the return value to be provided',
333 typeList: [ARGUMENT_TYPE.STRING],
334 defaultValue: 'none',
335 enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
336 forceEnum: true,
337 }),
319 ],338 ],
320 unnamedArgumentList: [339 unnamedArgumentList: [
321 new SlashCommandArgument(340 new SlashCommandArgument(
@@ -355,6 +374,7 @@ export function initDefaultSlashCommands() {
355 SlashCommandParser.addCommandObject(SlashCommand.fromProps({374 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
356 name: 'comment',375 name: 'comment',
357 callback: sendCommentMessage,376 callback: sendCommentMessage,
377 returns: 'Optionally the text of the sent message, if specified in the "return" argument',
358 namedArgumentList: [378 namedArgumentList: [
359 new SlashCommandNamedArgument(379 new SlashCommandNamedArgument(
360 'compact',380 'compact',
@@ -370,6 +390,14 @@ export function initDefaultSlashCommands() {
370 typeList: [ARGUMENT_TYPE.NUMBER],390 typeList: [ARGUMENT_TYPE.NUMBER],
371 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),391 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
372 }),392 }),
393 SlashCommandNamedArgument.fromProps({
394 name: 'return',
395 description: 'The way how you want the return value to be provided',
396 typeList: [ARGUMENT_TYPE.STRING],
397 defaultValue: 'none',
398 enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
399 forceEnum: true,
400 }),
373 ],401 ],
374 unnamedArgumentList: [402 unnamedArgumentList: [
375 new SlashCommandArgument(403 new SlashCommandArgument(
@@ -509,7 +537,7 @@ export function initDefaultSlashCommands() {
509 SlashCommandParser.addCommandObject(SlashCommand.fromProps({537 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
510 name: 'ask',538 name: 'ask',
511 callback: askCharacter,539 callback: askCharacter,
512 returns: 'the generated text',540 returns: 'Optionally the text of the sent message, if specified in the "return" argument',
513 namedArgumentList: [541 namedArgumentList: [
514 SlashCommandNamedArgument.fromProps({542 SlashCommandNamedArgument.fromProps({
515 name: 'name',543 name: 'name',
@@ -518,6 +546,14 @@ export function initDefaultSlashCommands() {
518 isRequired: true,546 isRequired: true,
519 enumProvider: commonEnumProviders.characters('character'),547 enumProvider: commonEnumProviders.characters('character'),
520 }),548 }),
549 SlashCommandNamedArgument.fromProps({
550 name: 'return',
551 description: 'The way how you want the return value to be provided',
552 typeList: [ARGUMENT_TYPE.STRING],
553 defaultValue: 'pipe',
554 enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
555 forceEnum: true,
556 }),
521 ],557 ],
522 unnamedArgumentList: [558 unnamedArgumentList: [
523 new SlashCommandArgument(559 new SlashCommandArgument(
@@ -556,6 +592,7 @@ export function initDefaultSlashCommands() {
556 SlashCommandParser.addCommandObject(SlashCommand.fromProps({592 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
557 name: 'send',593 name: 'send',
558 callback: sendUserMessageCallback,594 callback: sendUserMessageCallback,
595 returns: 'Optionally the text of the sent message, if specified in the "return" argument',
559 namedArgumentList: [596 namedArgumentList: [
560 new SlashCommandNamedArgument(597 new SlashCommandNamedArgument(
561 'compact',598 'compact',
@@ -578,6 +615,14 @@ export function initDefaultSlashCommands() {
578 defaultValue: '{{user}}',615 defaultValue: '{{user}}',
579 enumProvider: commonEnumProviders.personas,616 enumProvider: commonEnumProviders.personas,
580 }),617 }),
618 SlashCommandNamedArgument.fromProps({
619 name: 'return',
620 description: 'The way how you want the return value to be provided',
621 typeList: [ARGUMENT_TYPE.STRING],
622 defaultValue: 'none',
623 enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
624 forceEnum: true,
625 }),
581 ],626 ],
582 unnamedArgumentList: [627 unnamedArgumentList: [
583 new SlashCommandArgument(628 new SlashCommandArgument(
@@ -1568,12 +1613,21 @@ export function initDefaultSlashCommands() {
1568 SlashCommandParser.addCommandObject(SlashCommand.fromProps({1613 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1569 name: 'listinjects',1614 name: 'listinjects',
1570 callback: listInjectsCallback,1615 callback: listInjectsCallback,
1571 helpString: 'Lists all script injections for the current chat. Displays injects in a popup by default. Use the <code>format</code> argument to change the output format.',1616 helpString: 'Lists all script injections for the current chat. Displays injects in a popup by default. Use the <code>return</code> argument to change the return type.',
1572 returns: 'JSON object of script injections',1617 returns: 'Optionalls the JSON object of script injections',
1573 namedArgumentList: [1618 namedArgumentList: [
1574 SlashCommandNamedArgument.fromProps({1619 SlashCommandNamedArgument.fromProps({
1620 name: 'return',
1621 description: 'The way how you want the return value to be provided',
1622 typeList: [ARGUMENT_TYPE.STRING],
1623 defaultValue: 'popup-html',
1624 enumList: slashCommandReturnHelper.enumList({ allowPipe: false, allowObject: true, allowChat: true, allowPopup: true, allowTextVersion: false }),
1625 forceEnum: true,
1626 }),
1627 // TODO remove some day
1628 SlashCommandNamedArgument.fromProps({
1575 name: 'format',1629 name: 'format',
1576 description: 'output format',1630 description: '!!! DEPRECATED - use "return" instead !!! output format',
1577 typeList: [ARGUMENT_TYPE.STRING],1631 typeList: [ARGUMENT_TYPE.STRING],
1578 isRequired: true,1632 isRequired: true,
1579 forceEnum: true,1633 forceEnum: true,
@@ -1842,37 +1896,43 @@ function injectCallback(args, value) {
1842}1896}
18431897
1844async function listInjectsCallback(args) {1898async function listInjectsCallback(args) {
1845 const type = String(args?.format).toLowerCase().trim();1899 /** @type {import('./slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */
1846 if (!chat_metadata.script_injects || !Object.keys(chat_metadata.script_injects).length) {1900 let returnType = args.return;
1847 type !== 'none' && toastr.info('No script injections for the current chat');1901
1848 return JSON.stringify({});1902 // Old legacy return type handling
1849 }1903 if (args.format) {
18501904 toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning');
1851 const injects = Object.entries(chat_metadata.script_injects)1905 const type = String(args?.format).toLowerCase().trim();
1852 .map(([id, inject]) => {1906 if (!chat_metadata.script_injects || !Object.keys(chat_metadata.script_injects).length) {
1853 const position = Object.entries(extension_prompt_types);1907 type !== 'none' && toastr.info('No script injections for the current chat');
1854 const positionName = position.find(([_, value]) => value === inject.position)?.[0] ?? 'unknown';1908 }
1855 return `* **${id}**: <code>${inject.value}</code> (${positionName}, depth: ${inject.depth}, scan: ${inject.scan ?? false}, role: ${inject.role ?? extension_prompt_roles.SYSTEM})`;1909 switch (type) {
1856 })1910 case 'none':
1857 .join('\n');1911 returnType = 'none';
18581912 break;
1859 const converter = new showdown.Converter();1913 case 'chat':
1860 const messageText = `### Script injections:\n${injects}`;1914 returnType = 'chat-html';
1861 const htmlMessage = DOMPurify.sanitize(converter.makeHtml(messageText));1915 break;
18621916 case 'popup':
1863 switch (type) {1917 default:
1864 case 'none':1918 returnType = 'popup-html';
1865 break;1919 break;
1866 case 'chat':1920 }
1867 sendSystemMessage(system_message_types.GENERIC, htmlMessage);
1868 break;
1869 case 'popup':
1870 default:
1871 await callGenericPopup(htmlMessage, POPUP_TYPE.TEXT);
1872 break;
1873 }1921 }
18741922
1875 return JSON.stringify(chat_metadata.script_injects);1923 // Now the actual new return type handling
1924 const buildTextValue = (injects) => {
1925 const injectsStr = Object.entries(injects)
1926 .map(([id, inject]) => {
1927 const position = Object.entries(extension_prompt_types);
1928 const positionName = position.find(([_, value]) => value === inject.position)?.[0] ?? 'unknown';
1929 return `* **${id}**: <code>${inject.value}</code> (${positionName}, depth: ${inject.depth}, scan: ${inject.scan ?? false}, role: ${inject.role ?? extension_prompt_roles.SYSTEM})`;
1930 })
1931 .join('\n');
1932 return `### Script injections:\n${injectsStr || 'No script injections for the current chat'}`;
1933 };
1934
1935 return await slashCommandReturnHelper.doReturn(returnType ?? 'popup-html', chat_metadata.script_injects ?? {}, { objectToStringFunc: buildTextValue });
1876}1936}
18771937
1878/**1938/**
@@ -2559,7 +2619,7 @@ async function askCharacter(args, text) {
2559 // Not supported in group chats2619 // Not supported in group chats
2560 // TODO: Maybe support group chats?2620 // TODO: Maybe support group chats?
2561 if (selected_group) {2621 if (selected_group) {
2562 toastr.error('Cannot run /ask command in a group chat!');2622 toastr.warning('Cannot run /ask command in a group chat!');
2563 return '';2623 return '';
2564 }2624 }
25652625
@@ -2633,7 +2693,9 @@ async function askCharacter(args, text) {
2633 }2693 }
2634 }2694 }
26352695
2636 return askResult;2696 const message = askResult ? chat[chat.length - 1] : null;
2697
2698 return await slashCommandReturnHelper.doReturn(args.return ?? 'pipe', message, { objectToStringFunc: x => x.mes });
2637}2699}
26382700
2639async function hideMessageCallback(_, arg) {2701async function hideMessageCallback(_, arg) {
@@ -2908,7 +2970,7 @@ function findPersonaByName(name) {
29082970
2909async function sendUserMessageCallback(args, text) {2971async function sendUserMessageCallback(args, text) {
2910 if (!text) {2972 if (!text) {
2911 console.warn('WARN: No text provided for /send command');2973 toastr.warning('You must specify text to send');
2912 return;2974 return;
2913 }2975 }
29142976
@@ -2924,16 +2986,17 @@ async function sendUserMessageCallback(args, text) {
2924 insertAt = chat.length + insertAt;2986 insertAt = chat.length + insertAt;
2925 }2987 }
29262988
2989 let message;
2927 if ('name' in args) {2990 if ('name' in args) {
2928 const name = args.name || '';2991 const name = args.name || '';
2929 const avatar = findPersonaByName(name) || user_avatar;2992 const avatar = findPersonaByName(name) || user_avatar;
2930 await sendMessageAsUser(text, bias, insertAt, compact, name, avatar);2993 message = await sendMessageAsUser(text, bias, insertAt, compact, name, avatar);
2931 }2994 }
2932 else {2995 else {
2933 await sendMessageAsUser(text, bias, insertAt, compact);2996 message = await sendMessageAsUser(text, bias, insertAt, compact);
2934 }2997 }
29352998
2936 return '';2999 return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
2937}3000}
29383001
2939async function deleteMessagesByNameCallback(_, name) {3002async function deleteMessagesByNameCallback(_, name) {
@@ -3027,7 +3090,7 @@ async function continueChatCallback(args, prompt) {
3027 resolve();3090 resolve();
3028 } catch (error) {3091 } catch (error) {
3029 console.error('Error running /continue command:', error);3092 console.error('Error running /continue command:', error);
3030 reject();3093 reject(error);
3031 }3094 }
3032 });3095 });
30333096
@@ -3221,30 +3284,20 @@ export function getNameAndAvatarForMessage(character, name = null) {
32213284
3222export async function sendMessageAs(args, text) {3285export async function sendMessageAs(args, text) {
3223 if (!text) {3286 if (!text) {
3287 toastr.warning('You must specify text to send as');
3224 return '';3288 return '';
3225 }3289 }
32263290
3227 let name;3291 let name = args.name?.trim();
3228 let mesText;3292 let mesText;
32293293
3230 if (args.name) {3294 if (!name) {
3231 name = args.name.trim();
3232
3233 if (!name && !text) {
3234 toastr.warning('You must specify a name and text to send as');
3235 return '';
3236 }
3237 } else {
3238 const namelessWarningKey = 'sendAsNamelessWarningShown';3295 const namelessWarningKey = 'sendAsNamelessWarningShown';
3239 if (localStorage.getItem(namelessWarningKey) !== 'true') {3296 if (localStorage.getItem(namelessWarningKey) !== 'true') {
3240 toastr.warning('To avoid confusion, please use /sendas name="Character Name"', 'Name defaulted to {{char}}', { timeOut: 10000 });3297 toastr.warning('To avoid confusion, please use /sendas name="Character Name"', 'Name defaulted to {{char}}', { timeOut: 10000 });
3241 localStorage.setItem(namelessWarningKey, 'true');3298 localStorage.setItem(namelessWarningKey, 'true');
3242 }3299 }
3243 name = name2;3300 name = name2;
3244 if (!text) {
3245 toastr.warning('You must specify text to send as');
3246 return '';
3247 }
3248 }3301 }
32493302
3250 mesText = text.trim();3303 mesText = text.trim();
@@ -3321,11 +3374,12 @@ export async function sendMessageAs(args, text) {
3321 await saveChatConditional();3374 await saveChatConditional();
3322 }3375 }
33233376
3324 return '';3377 return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
3325}3378}
33263379
3327export async function sendNarratorMessage(args, text) {3380export async function sendNarratorMessage(args, text) {
3328 if (!text) {3381 if (!text) {
3382 toastr.warning('You must specify text to send');
3329 return '';3383 return '';
3330 }3384 }
33313385
@@ -3374,7 +3428,7 @@ export async function sendNarratorMessage(args, text) {
3374 await saveChatConditional();3428 await saveChatConditional();
3375 }3429 }
33763430
3377 return '';3431 return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
3378}3432}
33793433
3380export async function promptQuietForLoudResponse(who, text) {3434export async function promptQuietForLoudResponse(who, text) {
@@ -3420,6 +3474,7 @@ export async function promptQuietForLoudResponse(who, text) {
34203474
3421async function sendCommentMessage(args, text) {3475async function sendCommentMessage(args, text) {
3422 if (!text) {3476 if (!text) {
3477 toastr.warning('You must specify text to send');
3423 return '';3478 return '';
3424 }3479 }
34253480
@@ -3462,7 +3517,7 @@ async function sendCommentMessage(args, text) {
3462 await saveChatConditional();3517 await saveChatConditional();
3463 }3518 }
34643519
3465 return '';3520 return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
3466}3521}
34673522
3468/**3523/**
public/scripts/slash-commands/SlashCommandClosure.js+8 -0
@@ -508,6 +508,14 @@ export class SlashCommandClosure {
508 return v;508 return v;
509 });509 });
510 }510 }
511
512 value ??= '';
513
514 // Make sure that if unnamed args are split, it should always return an array
515 if (executor.command.splitUnnamedArgument && !Array.isArray(value)) {
516 value = [value];
517 }
518
511 return value;519 return value;
512 }520 }
513521
public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js+30 -0
@@ -36,6 +36,7 @@ export const enumIcons = {
36 message: '💬',36 message: '💬',
37 voice: '🎤',37 voice: '🎤',
38 server: '🖥️',38 server: '🖥️',
39 popup: '🗔',
3940
40 true: '✔️',41 true: '✔️',
41 false: '❌',42 false: '❌',
@@ -153,6 +154,35 @@ export const commonEnumProviders = {
153 },154 },
154155
155 /**156 /**
157 * Enum values for numbers and variable names
158 *
159 * Includes all variable names and the ability to specify any number
160 *
161 * @param {SlashCommandExecutor} executor - The executor of the slash command
162 * @param {SlashCommandScope} scope - The scope of the slash command
163 * @returns {SlashCommandEnumValue[]} The enum values
164 */
165 numbersAndVariables: (executor, scope) => [
166 ...commonEnumProviders.variables('all')(executor, scope),
167 new SlashCommandEnumValue(
168 'any variable name',
169 null,
170 enumTypes.variable,
171 enumIcons.variable,
172 (input) => /^\w*$/.test(input),
173 (input) => input,
174 ),
175 new SlashCommandEnumValue(
176 'any number',
177 null,
178 enumTypes.number,
179 enumIcons.number,
180 (input) => input == '' || !Number.isNaN(Number(input)),
181 (input) => input,
182 ),
183 ],
184
185 /**
156 * All possible char entities, like characters and groups. Can be filtered down to just one type.186 * All possible char entities, like characters and groups. Can be filtered down to just one type.
157 *187 *
158 * @param {('all' | 'character' | 'group')?} [mode='all'] - Which type to return188 * @param {('all' | 'character' | 'group')?} [mode='all'] - Which type to return
public/scripts/slash-commands/SlashCommandReturnHelper.js+80 -0
@@ -0,0 +1,80 @@
1import { sendSystemMessage, system_message_types } from '../../script.js';
2import { callGenericPopup, POPUP_TYPE } from '../popup.js';
3import { escapeHtml } from '../utils.js';
4import { enumIcons } from './SlashCommandCommonEnumsProvider.js';
5import { enumTypes, SlashCommandEnumValue } from './SlashCommandEnumValue.js';
6
7/** @typedef {'pipe'|'object'|'chat-html'|'chat-text'|'popup-html'|'popup-text'|'toast-html'|'toast-text'|'console'|'none'} SlashCommandReturnType */
8
9export const slashCommandReturnHelper = {
10 // Without this, VSCode formatter fucks up JS docs. Don't ask me why.
11 _: false,
12
13 /**
14 * Gets/creates the enum list of types of return relevant for a slash command
15 *
16 * @param {object} [options={}] Options
17 * @param {boolean} [options.allowPipe=true] Allow option to pipe the return value
18 * @param {boolean} [options.allowObject=false] Allow option to return the value as an object
19 * @param {boolean} [options.allowChat=false] Allow option to return the value as a chat message
20 * @param {boolean} [options.allowPopup=false] Allow option to return the value as a popup
21 * @param {boolean}[options.allowTextVersion=true] Used in combination with chat/popup/toast, some of them do not make sense for text versions, e.g.if you are building a HTML string anyway
22 * @returns {SlashCommandEnumValue[]} The enum list
23 */
24 enumList: ({ allowPipe = true, allowObject = false, allowChat = false, allowPopup = false, allowTextVersion = true } = {}) => [
25 allowPipe && new SlashCommandEnumValue('pipe', 'Return to the pipe for the next command', enumTypes.name, '|'),
26 allowObject && new SlashCommandEnumValue('object', 'Return as an object (or array) to the pipe for the next command', enumTypes.variable, enumIcons.dictionary),
27 allowChat && new SlashCommandEnumValue('chat-html', 'Sending a chat message with the return value - Can display HTML', enumTypes.command, enumIcons.message),
28 allowChat && allowTextVersion && new SlashCommandEnumValue('chat-text', 'Sending a chat message with the return value - Will only display as text', enumTypes.qr, enumIcons.message),
29 allowPopup && new SlashCommandEnumValue('popup-html', 'Showing as a popup with the return value - Can display HTML', enumTypes.command, enumIcons.popup),
30 allowPopup && allowTextVersion && new SlashCommandEnumValue('popup-text', 'Showing as a popup with the return value - Will only display as text', enumTypes.qr, enumIcons.popup),
31 new SlashCommandEnumValue('toast-html', 'Show the return value as a toast notification - Can display HTML', enumTypes.command, 'ℹ️'),
32 allowTextVersion && new SlashCommandEnumValue('toast-text', 'Show the return value as a toast notification - Will only display as text', enumTypes.qr, 'ℹ️'),
33 new SlashCommandEnumValue('console', 'Log the return value (object, if it can be one) to the console', enumTypes.enum, '>'),
34 new SlashCommandEnumValue('none', 'No return value'),
35 ].filter(x => !!x),
36
37 /**
38 * Handles the return value based on the specified type
39 *
40 * @param {SlashCommandReturnType} type The type of return
41 * @param {object|number|string} value The value to return
42 * @param {object} [options={}] Options
43 * @param {(o: object) => string} [options.objectToStringFunc=null] Function to convert the object to a string, if object was provided and 'object' was not the chosen return type
44 * @param {(o: object) => string} [options.objectToHtmlFunc=null] Analog to 'objectToStringFunc', which will be used here if not provided - but can do a different string layout if HTML is requested
45 * @returns {Promise<*>} The processed return value
46 */
47 async doReturn(type, value, { objectToStringFunc = o => o?.toString(), objectToHtmlFunc = null } = {}) {
48 const shouldHtml = type.endsWith('html');
49 const actualConverterFunc = shouldHtml && objectToHtmlFunc ? objectToHtmlFunc : objectToStringFunc;
50 const stringValue = typeof value !== 'string' ? actualConverterFunc(value) : value;
51
52 switch (type) {
53 case 'popup-html':
54 case 'popup-text':
55 case 'chat-text':
56 case 'chat-html':
57 case 'toast-text':
58 case 'toast-html': {
59 const htmlOrNotHtml = shouldHtml ? DOMPurify.sanitize((new showdown.Converter()).makeHtml(stringValue)) : escapeHtml(stringValue);
60
61 if (type.startsWith('popup')) await callGenericPopup(htmlOrNotHtml, POPUP_TYPE.TEXT);
62 if (type.startsWith('chat')) sendSystemMessage(system_message_types.GENERIC, htmlOrNotHtml);
63 if (type.startsWith('toast')) toastr.info(htmlOrNotHtml, null, { escapeHtml: !shouldHtml });
64
65 return '';
66 }
67 case 'pipe':
68 return stringValue ?? '';
69 case 'object':
70 return JSON.stringify(value);
71 case 'console':
72 console.info(value);
73 return '';
74 case 'none':
75 return '';
76 default:
77 throw new Error(`Unknown return type: ${type}`);
78 }
79 },
80};
public/scripts/variables.js+124 -85
@@ -11,6 +11,7 @@ import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureR
11import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';11import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
12import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';12import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
13import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';13import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
14import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';
14import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';15import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
15import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js';16import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js';
1617
@@ -305,7 +306,28 @@ export function replaceVariableMacros(input) {
305}306}
306307
307async function listVariablesCallback(args) {308async function listVariablesCallback(args) {
308 const type = String(args?.format || '').toLowerCase().trim() || 'popup';309 /** @type {import('./slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */
310 let returnType = args.return;
311
312 // Old legacy return type handling
313 if (args.format) {
314 toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning');
315 const type = String(args?.format).toLowerCase().trim();
316 switch (type) {
317 case 'none':
318 returnType = 'none';
319 break;
320 case 'chat':
321 returnType = 'chat-html';
322 break;
323 case 'popup':
324 default:
325 returnType = 'popup-html';
326 break;
327 }
328 }
329
330 // Now the actual new return type handling
309 const scope = String(args?.scope || '').toLowerCase().trim() || 'all';331 const scope = String(args?.scope || '').toLowerCase().trim() || 'all';
310 if (!chat_metadata.variables) {332 if (!chat_metadata.variables) {
311 chat_metadata.variables = {};333 chat_metadata.variables = {};
@@ -317,35 +339,24 @@ async function listVariablesCallback(args) {
317 const localVariables = includeLocalVariables ? Object.entries(chat_metadata.variables).map(([name, value]) => `${name}: ${value}`) : [];339 const localVariables = includeLocalVariables ? Object.entries(chat_metadata.variables).map(([name, value]) => `${name}: ${value}`) : [];
318 const globalVariables = includeGlobalVariables ? Object.entries(extension_settings.variables.global).map(([name, value]) => `${name}: ${value}`) : [];340 const globalVariables = includeGlobalVariables ? Object.entries(extension_settings.variables.global).map(([name, value]) => `${name}: ${value}`) : [];
319341
342 const buildTextValue = (_) => {
343 const localVariablesString = localVariables.length > 0 ? localVariables.join('\n\n') : 'No local variables';
344 const globalVariablesString = globalVariables.length > 0 ? globalVariables.join('\n\n') : 'No global variables';
345 const chatName = getCurrentChatId();
346
347 const message = [
348 includeLocalVariables ? `### Local variables (${chatName}):\n${localVariablesString}` : '',
349 includeGlobalVariables ? `### Global variables:\n${globalVariablesString}` : '',
350 ].filter(x => x).join('\n\n');
351 return message;
352 };
353
320 const jsonVariables = [354 const jsonVariables = [
321 ...Object.entries(chat_metadata.variables).map(x => ({ key: x[0], value: x[1], scope: 'local' })),355 ...Object.entries(chat_metadata.variables).map(x => ({ key: x[0], value: x[1], scope: 'local' })),
322 ...Object.entries(extension_settings.variables.global).map(x => ({ key: x[0], value: x[1], scope: 'global' })),356 ...Object.entries(extension_settings.variables.global).map(x => ({ key: x[0], value: x[1], scope: 'global' })),
323 ];357 ];
324358
325 const localVariablesString = localVariables.length > 0 ? localVariables.join('\n\n') : 'No local variables';359 return await slashCommandReturnHelper.doReturn(returnType ?? 'popup-html', jsonVariables, { objectToStringFunc: buildTextValue });
326 const globalVariablesString = globalVariables.length > 0 ? globalVariables.join('\n\n') : 'No global variables';
327 const chatName = getCurrentChatId();
328
329 const converter = new showdown.Converter();
330 const message = [
331 includeLocalVariables ? `### Local variables (${chatName}):\n${localVariablesString}` : '',
332 includeGlobalVariables ? `### Global variables:\n${globalVariablesString}` : '',
333 ].filter(x => x).join('\n\n');
334 const htmlMessage = DOMPurify.sanitize(converter.makeHtml(message));
335
336 switch (type) {
337 case 'none':
338 break;
339 case 'chat':
340 sendSystemMessage(system_message_types.GENERIC, htmlMessage);
341 break;
342 case 'popup':
343 default:
344 await callGenericPopup(htmlMessage, POPUP_TYPE.TEXT);
345 break;
346 }
347
348 return JSON.stringify(jsonVariables);
349}360}
350361
351/**362/**
@@ -669,8 +680,8 @@ function deleteGlobalVariable(name) {
669}680}
670681
671/**682/**
672 * Parses a series of numeric values from a string.683 * Parses a series of numeric values from a string or a string array.
673 * @param {string} value A space-separated list of numeric values or variable names684 * @param {string|string[]} value A space-separated list of numeric values or variable names
674 * @param {SlashCommandScope} scope Scope685 * @param {SlashCommandScope} scope Scope
675 * @returns {number[]} An array of numeric values686 * @returns {number[]} An array of numeric values
676 */687 */
@@ -679,11 +690,17 @@ function parseNumericSeries(value, scope = null) {
679 return [value];690 return [value];
680 }691 }
681692
682 const array = value693 /** @type {(string|number)[]} */
683 .split(' ')694 let values = Array.isArray(value) ? value : value.split(' ');
684 .map(i => i.trim())695
696 // If a JSON array was provided as the only value, convert it to an array
697 if (values.length === 1 && typeof values[0] === 'string' && values[0].startsWith('[')) {
698 values = convertValueType(values[0], 'array');
699 }
700
701 const array = values.map(i => typeof i === 'string' ? i.trim() : i)
685 .filter(i => i !== '')702 .filter(i => i !== '')
686 .map(i => isNaN(Number(i)) ? Number(resolveVariable(i, scope)) : Number(i))703 .map(i => isNaN(Number(i)) ? Number(resolveVariable(String(i), scope)) : Number(i))
687 .filter(i => !isNaN(i));704 .filter(i => !isNaN(i));
688705
689 return array;706 return array;
@@ -703,7 +720,7 @@ function performOperation(value, operation, singleOperand = false, scope = null)
703720
704 const result = singleOperand ? operation(array[0]) : operation(array);721 const result = singleOperand ? operation(array[0]) : operation(array);
705722
706 if (isNaN(result) || !isFinite(result)) {723 if (isNaN(result)) {
707 return 0;724 return 0;
708 }725 }
709726
@@ -731,7 +748,7 @@ function maxValuesCallback(args, value) {
731}748}
732749
733function subValuesCallback(args, value) {750function subValuesCallback(args, value) {
734 return performOperation(value, (array) => array[0] - array[1], false, args._scope);751 return performOperation(value, (array) => array.reduce((a, b) => a - b, array.shift() ?? 0), false, args._scope);
735}752}
736753
737function divValuesCallback(args, value) {754function divValuesCallback(args, value) {
@@ -910,7 +927,7 @@ export function registerVariableCommands() {
910 name: 'listvar',927 name: 'listvar',
911 callback: listVariablesCallback,928 callback: listVariablesCallback,
912 aliases: ['listchatvar'],929 aliases: ['listchatvar'],
913 helpString: 'List registered chat variables. Displays variables in a popup by default. Use the <code>format</code> argument to change the output format.',930 helpString: 'List registered chat variables. Displays variables in a popup by default. Use the <code>return</code> argument to change the return type.',
914 returns: 'JSON list of local variables',931 returns: 'JSON list of local variables',
915 namedArgumentList: [932 namedArgumentList: [
916 SlashCommandNamedArgument.fromProps({933 SlashCommandNamedArgument.fromProps({
@@ -927,8 +944,17 @@ export function registerVariableCommands() {
927 ],944 ],
928 }),945 }),
929 SlashCommandNamedArgument.fromProps({946 SlashCommandNamedArgument.fromProps({
947 name: 'return',
948 description: 'The way how you want the return value to be provided',
949 typeList: [ARGUMENT_TYPE.STRING],
950 defaultValue: 'popup-html',
951 enumList: slashCommandReturnHelper.enumList({ allowPipe: false, allowObject: true, allowChat: true, allowPopup: true, allowTextVersion: false }),
952 forceEnum: true,
953 }),
954 // TODO remove some day
955 SlashCommandNamedArgument.fromProps({
930 name: 'format',956 name: 'format',
931 description: 'output format',957 description: '!!! DEPRECATED - use "return" instead !!! output format',
932 typeList: [ARGUMENT_TYPE.STRING],958 typeList: [ARGUMENT_TYPE.STRING],
933 isRequired: true,959 isRequired: true,
934 forceEnum: true,960 forceEnum: true,
@@ -1595,36 +1621,15 @@ export function registerVariableCommands() {
1595 }));1621 }));
1596 SlashCommandParser.addCommandObject(SlashCommand.fromProps({1622 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1597 name: 'add',1623 name: 'add',
1598 callback: (args, /**@type {string[]}*/value) => addValuesCallback(args, value.join(' ')),1624 callback: (args, value) => addValuesCallback(args, value),
1599 returns: 'sum of the provided values',1625 returns: 'sum of the provided values',
1600 unnamedArgumentList: [1626 unnamedArgumentList: [
1601 SlashCommandArgument.fromProps({1627 SlashCommandArgument.fromProps({
1602 description: 'values to sum',1628 description: 'values to sum',
1603 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1629 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
1604 isRequired: true,1630 isRequired: true,
1605 acceptsMultiple: true,1631 acceptsMultiple: true,
1606 enumProvider: (executor, scope) => {1632 enumProvider: commonEnumProviders.numbersAndVariables,
1607 const vars = commonEnumProviders.variables('all')(executor, scope);
1608 vars.push(
1609 new SlashCommandEnumValue(
1610 'any variable name',
1611 null,
1612 enumTypes.variable,
1613 enumIcons.variable,
1614 (input) => /^\w*$/.test(input),
1615 (input) => input,
1616 ),
1617 new SlashCommandEnumValue(
1618 'any number',
1619 null,
1620 enumTypes.number,
1621 enumIcons.number,
1622 (input) => input == '' || !Number.isNaN(Number(input)),
1623 (input) => input,
1624 ),
1625 );
1626 return vars;
1627 },
1628 forceEnum: false,1633 forceEnum: false,
1629 }),1634 }),
1630 ],1635 ],
@@ -1632,7 +1637,9 @@ export function registerVariableCommands() {
1632 helpString: `1637 helpString: `
1633 <div>1638 <div>
1634 Performs an addition of the set of values and passes the result down the pipe.1639 Performs an addition of the set of values and passes the result down the pipe.
1635 Can use variable names.1640 </div>
1641 <div>
1642 Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
1636 </div>1643 </div>
1637 <div>1644 <div>
1638 <strong>Example:</strong>1645 <strong>Example:</strong>
@@ -1640,6 +1647,9 @@ export function registerVariableCommands() {
1640 <li>1647 <li>
1641 <pre><code class="language-stscript">/add 10 i 30 j</code></pre>1648 <pre><code class="language-stscript">/add 10 i 30 j</code></pre>
1642 </li>1649 </li>
1650 <li>
1651 <pre><code class="language-stscript">/add ["count", 15, 2, "i"]</code></pre>
1652 </li>
1643 </ul>1653 </ul>
1644 </div>1654 </div>
1645 `,1655 `,
@@ -1651,16 +1661,20 @@ export function registerVariableCommands() {
1651 unnamedArgumentList: [1661 unnamedArgumentList: [
1652 SlashCommandArgument.fromProps({1662 SlashCommandArgument.fromProps({
1653 description: 'values to multiply',1663 description: 'values to multiply',
1654 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1664 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
1655 isRequired: true,1665 isRequired: true,
1656 acceptsMultiple: true,1666 acceptsMultiple: true,
1657 enumProvider: commonEnumProviders.variables('all'),1667 enumProvider: commonEnumProviders.numbersAndVariables,
1658 forceEnum: false,1668 forceEnum: false,
1659 }),1669 }),
1660 ],1670 ],
1671 splitUnnamedArgument: true,
1661 helpString: `1672 helpString: `
1662 <div>1673 <div>
1663 Performs a multiplication of the set of values and passes the result down the pipe. Can use variable names.1674 Performs a multiplication of the set of values and passes the result down the pipe.
1675 </div>
1676 <div>
1677 Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
1664 </div>1678 </div>
1665 <div>1679 <div>
1666 <strong>Examples:</strong>1680 <strong>Examples:</strong>
@@ -1668,6 +1682,9 @@ export function registerVariableCommands() {
1668 <li>1682 <li>
1669 <pre><code class="language-stscript">/mul 10 i 30 j</code></pre>1683 <pre><code class="language-stscript">/mul 10 i 30 j</code></pre>
1670 </li>1684 </li>
1685 <li>
1686 <pre><code class="language-stscript">/mul ["count", 15, 2, "i"]</code></pre>
1687 </li>
1671 </ul>1688 </ul>
1672 </div>1689 </div>
1673 `,1690 `,
@@ -1679,16 +1696,20 @@ export function registerVariableCommands() {
1679 unnamedArgumentList: [1696 unnamedArgumentList: [
1680 SlashCommandArgument.fromProps({1697 SlashCommandArgument.fromProps({
1681 description: 'values to find the max',1698 description: 'values to find the max',
1682 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1699 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
1683 isRequired: true,1700 isRequired: true,
1684 acceptsMultiple: true,1701 acceptsMultiple: true,
1685 enumProvider: commonEnumProviders.variables('all'),1702 enumProvider: commonEnumProviders.numbersAndVariables,
1686 forceEnum: false,1703 forceEnum: false,
1687 }),1704 }),
1688 ],1705 ],
1706 splitUnnamedArgument: true,
1689 helpString: `1707 helpString: `
1690 <div>1708 <div>
1691 Returns the maximum value of the set of values and passes the result down the pipe. Can use variable names.1709 Returns the maximum value of the set of values and passes the result down the pipe.
1710 </div>
1711 <div>
1712 Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
1692 </div>1713 </div>
1693 <div>1714 <div>
1694 <strong>Examples:</strong>1715 <strong>Examples:</strong>
@@ -1696,6 +1717,9 @@ export function registerVariableCommands() {
1696 <li>1717 <li>
1697 <pre><code class="language-stscript">/max 10 i 30 j</code></pre>1718 <pre><code class="language-stscript">/max 10 i 30 j</code></pre>
1698 </li>1719 </li>
1720 <li>
1721 <pre><code class="language-stscript">/max ["count", 15, 2, "i"]</code></pre>
1722 </li>
1699 </ul>1723 </ul>
1700 </div>1724 </div>
1701 `,1725 `,
@@ -1707,17 +1731,20 @@ export function registerVariableCommands() {
1707 unnamedArgumentList: [1731 unnamedArgumentList: [
1708 SlashCommandArgument.fromProps({1732 SlashCommandArgument.fromProps({
1709 description: 'values to find the min',1733 description: 'values to find the min',
1710 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1734 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
1711 isRequired: true,1735 isRequired: true,
1712 acceptsMultiple: true,1736 acceptsMultiple: true,
1713 enumProvider: commonEnumProviders.variables('all'),1737 enumProvider: commonEnumProviders.numbersAndVariables,
1714 forceEnum: false,1738 forceEnum: false,
1715 }),1739 }),
1716 ],1740 ],
1741 splitUnnamedArgument: true,
1717 helpString: `1742 helpString: `
1718 <div>1743 <div>
1719 Returns the minimum value of the set of values and passes the result down the pipe.1744 Returns the minimum value of the set of values and passes the result down the pipe.
1720 Can use variable names.1745 </div>
1746 <div>
1747 Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
1721 </div>1748 </div>
1722 <div>1749 <div>
1723 <strong>Example:</strong>1750 <strong>Example:</strong>
@@ -1725,6 +1752,9 @@ export function registerVariableCommands() {
1725 <li>1752 <li>
1726 <pre><code class="language-stscript">/min 10 i 30 j</code></pre>1753 <pre><code class="language-stscript">/min 10 i 30 j</code></pre>
1727 </li>1754 </li>
1755 <li>
1756 <pre><code class="language-stscript">/min ["count", 15, 2, "i"]</code></pre>
1757 </li>
1728 </ul>1758 </ul>
1729 </div>1759 </div>
1730 `,1760 `,
@@ -1735,18 +1765,21 @@ export function registerVariableCommands() {
1735 returns: 'difference of the provided values',1765 returns: 'difference of the provided values',
1736 unnamedArgumentList: [1766 unnamedArgumentList: [
1737 SlashCommandArgument.fromProps({1767 SlashCommandArgument.fromProps({
1738 description: 'values to find the difference',1768 description: 'values to subtract, starting form the first provided value',
1739 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1769 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
1740 isRequired: true,1770 isRequired: true,
1741 acceptsMultiple: true,1771 acceptsMultiple: true,
1742 enumProvider: commonEnumProviders.variables('all'),1772 enumProvider: commonEnumProviders.numbersAndVariables,
1743 forceEnum: false,1773 forceEnum: false,
1744 }),1774 }),
1745 ],1775 ],
1776 splitUnnamedArgument: true,
1746 helpString: `1777 helpString: `
1747 <div>1778 <div>
1748 Performs a subtraction of the set of values and passes the result down the pipe.1779 Performs a subtraction of the set of values and passes the result down the pipe.
1749 Can use variable names.1780 </div>
1781 <div>
1782 Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
1750 </div>1783 </div>
1751 <div>1784 <div>
1752 <strong>Example:</strong>1785 <strong>Example:</strong>
@@ -1754,6 +1787,9 @@ export function registerVariableCommands() {
1754 <li>1787 <li>
1755 <pre><code class="language-stscript">/sub i 5</code></pre>1788 <pre><code class="language-stscript">/sub i 5</code></pre>
1756 </li>1789 </li>
1790 <li>
1791 <pre><code class="language-stscript">/sub ["count", 4, "i"]</code></pre>
1792 </li>
1757 </ul>1793 </ul>
1758 </div>1794 </div>
1759 `,1795 `,
@@ -1767,17 +1803,18 @@ export function registerVariableCommands() {
1767 description: 'dividend',1803 description: 'dividend',
1768 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1804 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1769 isRequired: true,1805 isRequired: true,
1770 enumProvider: commonEnumProviders.variables('all'),1806 enumProvider: commonEnumProviders.numbersAndVariables,
1771 forceEnum: false,1807 forceEnum: false,
1772 }),1808 }),
1773 SlashCommandArgument.fromProps({1809 SlashCommandArgument.fromProps({
1774 description: 'divisor',1810 description: 'divisor',
1775 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1811 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1776 isRequired: true,1812 isRequired: true,
1777 enumProvider: commonEnumProviders.variables('all'),1813 enumProvider: commonEnumProviders.numbersAndVariables,
1778 forceEnum: false,1814 forceEnum: false,
1779 }),1815 }),
1780 ],1816 ],
1817 splitUnnamedArgument: true,
1781 helpString: `1818 helpString: `
1782 <div>1819 <div>
1783 Performs a division of two values and passes the result down the pipe.1820 Performs a division of two values and passes the result down the pipe.
@@ -1802,17 +1839,18 @@ export function registerVariableCommands() {
1802 description: 'dividend',1839 description: 'dividend',
1803 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1840 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1804 isRequired: true,1841 isRequired: true,
1805 enumProvider: commonEnumProviders.variables('all'),1842 enumProvider: commonEnumProviders.numbersAndVariables,
1806 forceEnum: false,1843 forceEnum: false,
1807 }),1844 }),
1808 SlashCommandArgument.fromProps({1845 SlashCommandArgument.fromProps({
1809 description: 'divisor',1846 description: 'divisor',
1810 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1847 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1811 isRequired: true,1848 isRequired: true,
1812 enumProvider: commonEnumProviders.variables('all'),1849 enumProvider: commonEnumProviders.numbersAndVariables,
1813 forceEnum: false,1850 forceEnum: false,
1814 }),1851 }),
1815 ],1852 ],
1853 splitUnnamedArgument: true,
1816 helpString: `1854 helpString: `
1817 <div>1855 <div>
1818 Performs a modulo operation of two values and passes the result down the pipe.1856 Performs a modulo operation of two values and passes the result down the pipe.
@@ -1837,17 +1875,18 @@ export function registerVariableCommands() {
1837 description: 'base',1875 description: 'base',
1838 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1876 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1839 isRequired: true,1877 isRequired: true,
1840 enumProvider: commonEnumProviders.variables('all'),1878 enumProvider: commonEnumProviders.numbersAndVariables,
1841 forceEnum: false,1879 forceEnum: false,
1842 }),1880 }),
1843 SlashCommandArgument.fromProps({1881 SlashCommandArgument.fromProps({
1844 description: 'exponent',1882 description: 'exponent',
1845 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1883 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1846 isRequired: true,1884 isRequired: true,
1847 enumProvider: commonEnumProviders.variables('all'),1885 enumProvider: commonEnumProviders.numbersAndVariables,
1848 forceEnum: false,1886 forceEnum: false,
1849 }),1887 }),
1850 ],1888 ],
1889 splitUnnamedArgument: true,
1851 helpString: `1890 helpString: `
1852 <div>1891 <div>
1853 Performs a power operation of two values and passes the result down the pipe.1892 Performs a power operation of two values and passes the result down the pipe.
@@ -1872,7 +1911,7 @@ export function registerVariableCommands() {
1872 description: 'value',1911 description: 'value',
1873 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1912 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1874 isRequired: true,1913 isRequired: true,
1875 enumProvider: commonEnumProviders.variables('all'),1914 enumProvider: commonEnumProviders.numbersAndVariables,
1876 forceEnum: false,1915 forceEnum: false,
1877 }),1916 }),
1878 ],1917 ],
@@ -1900,7 +1939,7 @@ export function registerVariableCommands() {
1900 description: 'value',1939 description: 'value',
1901 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1940 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1902 isRequired: true,1941 isRequired: true,
1903 enumProvider: commonEnumProviders.variables('all'),1942 enumProvider: commonEnumProviders.numbersAndVariables,
1904 forceEnum: false,1943 forceEnum: false,
1905 }),1944 }),
1906 ],1945 ],
@@ -1929,7 +1968,7 @@ export function registerVariableCommands() {
1929 description: 'value',1968 description: 'value',
1930 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1969 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1931 isRequired: true,1970 isRequired: true,
1932 enumProvider: commonEnumProviders.variables('all'),1971 enumProvider: commonEnumProviders.numbersAndVariables,
1933 forceEnum: false,1972 forceEnum: false,
1934 }),1973 }),
1935 ],1974 ],
@@ -1957,7 +1996,7 @@ export function registerVariableCommands() {
1957 description: 'value',1996 description: 'value',
1958 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1997 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1959 isRequired: true,1998 isRequired: true,
1960 enumProvider: commonEnumProviders.variables('all'),1999 enumProvider: commonEnumProviders.numbersAndVariables,
1961 forceEnum: false,2000 forceEnum: false,
1962 }),2001 }),
1963 ],2002 ],
@@ -1985,7 +2024,7 @@ export function registerVariableCommands() {
1985 description: 'value',2024 description: 'value',
1986 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],2025 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1987 isRequired: true,2026 isRequired: true,
1988 enumProvider: commonEnumProviders.variables('all'),2027 enumProvider: commonEnumProviders.numbersAndVariables,
1989 forceEnum: false,2028 forceEnum: false,
1990 }),2029 }),
1991 ],2030 ],
@@ -2013,7 +2052,7 @@ export function registerVariableCommands() {
2013 description: 'value',2052 description: 'value',
2014 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],2053 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
2015 isRequired: true,2054 isRequired: true,
2016 enumProvider: commonEnumProviders.variables('all'),2055 enumProvider: commonEnumProviders.numbersAndVariables,
2017 forceEnum: false,2056 forceEnum: false,
2018 }),2057 }),
2019 ],2058 ],
src/endpoints/backends/chat-completions.js+1 -1
@@ -323,7 +323,7 @@ async function sendMakerSuiteRequest(request, response) {
323 ? (stream ? 'streamGenerateContent' : 'generateContent')323 ? (stream ? 'streamGenerateContent' : 'generateContent')
324 : (isText ? 'generateText' : 'generateMessage');324 : (isText ? 'generateText' : 'generateMessage');
325325
326 const generateResponse = await fetch(`${apiUrl.origin}/${apiVersion}/models/${model}:${responseType}?key=${apiKey}${stream ? '&alt=sse' : ''}`, {326 const generateResponse = await fetch(`${apiUrl}/${apiVersion}/models/${model}:${responseType}?key=${apiKey}${stream ? '&alt=sse' : ''}`, {
327 body: JSON.stringify(body),327 body: JSON.stringify(body),
328 method: 'POST',328 method: 'POST',
329 headers: {329 headers: {
src/endpoints/stable-diffusion.js+0 -69
@@ -10,41 +10,6 @@ const { readSecret, SECRET_KEYS } = require('./secrets.js');
10const FormData = require('form-data');10const FormData = require('form-data');
1111
12/**12/**
13 * Sanitizes a string.
14 * @param {string} x String to sanitize
15 * @returns {string} Sanitized string
16 */
17function safeStr(x) {
18 x = String(x);
19 x = x.replace(/ +/g, ' ');
20 x = x.trim();
21 x = x.replace(/^[\s,.]+|[\s,.]+$/g, '');
22 return x;
23}
24
25const splitStrings = [
26 ', extremely',
27 ', intricate,',
28];
29
30const dangerousPatterns = '[]【】()()|::';
31
32/**
33 * Removes patterns from a string.
34 * @param {string} x String to sanitize
35 * @param {string} pattern Pattern to remove
36 * @returns {string} Sanitized string
37 */
38function removePattern(x, pattern) {
39 for (let i = 0; i < pattern.length; i++) {
40 let p = pattern[i];
41 let regex = new RegExp('\\' + p, 'g');
42 x = x.replace(regex, '');
43 }
44 return x;
45}
46
47/**
48 * Gets the comfy workflows.13 * Gets the comfy workflows.
49 * @param {import('../users.js').UserDirectoryList} directories14 * @param {import('../users.js').UserDirectoryList} directories
50 * @returns {string[]} List of comfy workflows15 * @returns {string[]} List of comfy workflows
@@ -391,40 +356,6 @@ router.post('/sd-next/upscalers', jsonParser, async (request, response) => {
391 }356 }
392});357});
393358
394/**
395 * SD prompt expansion using GPT-2 text generation model.
396 * Adapted from: https://github.com/lllyasviel/Fooocus/blob/main/modules/expansion.py
397 */
398router.post('/expand', jsonParser, async (request, response) => {
399 const originalPrompt = request.body.prompt;
400
401 if (!originalPrompt) {
402 console.warn('No prompt provided for SD expansion.');
403 return response.send({ prompt: '' });
404 }
405
406 console.log('Refine prompt input:', originalPrompt);
407 const splitString = splitStrings[Math.floor(Math.random() * splitStrings.length)];
408 let prompt = safeStr(originalPrompt) + splitString;
409
410 try {
411 const task = 'text-generation';
412 const module = await import('../transformers.mjs');
413 const pipe = await module.default.getPipeline(task);
414
415 const result = await pipe(prompt, { num_beams: 1, max_new_tokens: 256, do_sample: true });
416
417 const newText = result[0].generated_text;
418 const newPrompt = safeStr(removePattern(newText, dangerousPatterns));
419 console.log('Refine prompt output:', newPrompt);
420
421 return response.send({ prompt: newPrompt });
422 } catch {
423 console.warn('Failed to load transformers.js pipeline.');
424 return response.send({ prompt: originalPrompt });
425 }
426});
427
428const comfy = express.Router();359const comfy = express.Router();
429360
430comfy.post('/ping', jsonParser, async (request, response) => {361comfy.post('/ping', jsonParser, async (request, response) => {
src/transformers.mjs+0 -6
@@ -31,12 +31,6 @@ const tasks = {
31 configField: 'extras.embeddingModel',31 configField: 'extras.embeddingModel',
32 quantized: true,32 quantized: true,
33 },33 },
34 'text-generation': {
35 defaultModel: 'Cohee/fooocus_expansion-onnx',
36 pipeline: null,
37 configField: 'extras.promptExpansionModel',
38 quantized: false,
39 },
40 'automatic-speech-recognition': {34 'automatic-speech-recognition': {
41 defaultModel: 'Xenova/whisper-small',35 defaultModel: 'Xenova/whisper-small',
42 pipeline: null,36 pipeline: null,