diff --git a/default/config.yaml b/default/config.yaml index 585988686b..a7e7a747e9 100644 --- a/default/config.yaml +++ b/default/config.yaml @@ -118,7 +118,6 @@ extras: classificationModel: Cohee/distilbert-base-uncased-go-emotions-onnx captioningModel: Xenova/vit-gpt2-image-captioning embeddingModel: Cohee/jina-embeddings-v2-base-en - promptExpansionModel: Cohee/fooocus_expansion-onnx speechToTextModel: Xenova/whisper-small textToSpeechModel: Xenova/speecht5_tts # -- OPENAI CONFIGURATION -- diff --git a/public/index.html b/public/index.html index 741871eddf..c6117f665a 100644 --- a/public/index.html +++ b/public/index.html @@ -1709,9 +1709,9 @@

-
+
Character Names Behavior - + ()
@@ -1720,25 +1720,22 @@

-
+
Continue Postfix - + ()
@@ -4013,6 +4010,10 @@

Compact Input Area +

- +
+ +
+
diff --git a/public/script.js b/public/script.js index 8d085b6d28..35bfcbab0b 100644 --- a/public/script.js +++ b/public/script.js @@ -83,6 +83,7 @@ import { resetMovableStyles, forceCharacterEditorTokenize, applyPowerUserSettings, + switchSwipeNumAllMessages, } from './scripts/power-user.js'; import { @@ -2369,6 +2370,13 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll addCopyToCodeBlocks(newMessage); + // Set the swipes counter for past messages, only visible if 'Show Swipes on All Message' is enabled + if (!params.isUser && newMessageId !== 0 && newMessageId !== chat.length - 1) { + const swipesNum = chat[newMessageId].swipes?.length; + const swipeId = chat[newMessageId].swipe_id + 1; + newMessage.find('.swipes-counter').text(`${swipeId}\u200B/\u200b${swipesNum}`); + } + if (showSwipes) { $('#chat .mes').last().addClass('last_mes'); $('#chat .mes').eq(-2).removeClass('last_mes'); @@ -4785,7 +4793,7 @@ export function removeMacros(str) { * @param {boolean} [compact] Send as a compact display message. * @param {string} [name] Name of the user sending the message. Defaults to name1. * @param {string} [avatar] Avatar of the user sending the message. Defaults to user_avatar. - * @returns {Promise} A promise that resolves when the message is inserted. + * @returns {Promise} A promise that resolves to the message when it is inserted. */ export async function sendMessageAsUser(messageText, messageBias, insertAt = null, compact = false, name = name1, avatar = user_avatar) { messageText = getRegexedString(messageText, regex_placement.USER_INPUT); @@ -4832,6 +4840,8 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul await eventSource.emit(event_types.USER_MESSAGE_RENDERED, chat_id); await saveChatConditional(); } + + return message; } /** @@ -7469,7 +7479,6 @@ export function showSwipeButtons() { //had to add this to make the swipe counter work //(copied from the onclick functions for swipe buttons.. //don't know why the array isn't set for non-swipe messsages in Generate or addOneMessage..) - if (chat[chat.length - 1]['swipe_id'] === undefined) { // if there is no swipe-message in the last spot of the chat array chat[chat.length - 1]['swipe_id'] = 0; // set it to id 0 chat[chat.length - 1]['swipes'] = []; // empty the array @@ -7479,32 +7488,36 @@ export function showSwipeButtons() { const currentMessage = $('#chat').children().filter(`[mesid="${chat.length - 1}"]`); const swipeId = chat[chat.length - 1].swipe_id; const swipeCounterText = (`${(swipeId + 1)}\u200B/\u200b${(chat[chat.length - 1].swipes.length)}`); + const swipeRight = currentMessage.find('.swipe_right'); + const swipeLeft = currentMessage.find('.swipe_left'); + const swipeCounter = currentMessage.find('.swipes-counter'); if (swipeId !== undefined && (chat[chat.length - 1].swipes.length > 1 || swipeId > 0)) { - currentMessage.children('.swipe_left').css('display', 'flex'); + swipeLeft.css('display', 'flex'); } //only show right when generate is off, or when next right swipe would not make a generate happen if (is_send_press === false || chat[chat.length - 1].swipes.length >= swipeId) { - currentMessage.children('.swipe_right').css('display', 'flex'); - currentMessage.children('.swipe_right').css('opacity', '0.3'); + swipeRight.css('display', 'flex').css('opacity', '0.3'); + swipeCounter.css('opacity', '0.3'); } - //console.log((chat[chat.length - 1])); if ((chat[chat.length - 1].swipes.length - swipeId) === 1) { - //console.log('highlighting R swipe'); - currentMessage.children('.swipe_right').css('opacity', '0.7'); + //chevron was moved out of hardcode in HTML to class toggle dependent on last_mes or not + //necessary for 'swipe_right' div in past messages to have no chevron if 'show swipes for all messages' is turned on + swipeRight.css('opacity', '0.7'); + swipeCounter.css('opacity', '0.7'); } - //console.log(swipesCounterHTML); - $('.swipes-counter').text(swipeCounterText); + //allows for writing individual swipe counters for past messages + const lastSwipeCounter = $('.last_mes .swipes-counter'); + lastSwipeCounter.text(swipeCounterText).show(); - //console.log(swipeId); - //console.log(chat[chat.length - 1].swipes.length); + switchSwipeNumAllMessages(); } export function hideSwipeButtons() { - //console.log('hideswipebuttons entered'); - $('#chat').find('.swipe_right').css('display', 'none'); - $('#chat').find('.swipe_left').css('display', 'none'); + $('#chat').find('.swipe_right').hide(); + $('#chat').find('.last_mes .swipes-counter').hide(); + $('#chat').find('.swipe_left').hide(); } /** @@ -8339,8 +8352,8 @@ const swipe_right = () => { } const currentMessage = $('#chat').children().filter(`[mesid="${chat.length - 1}"]`); - let this_div = currentMessage.children('.swipe_right'); - let this_mes_div = this_div.parent(); + let this_div = currentMessage.find('.swipe_right'); + let this_mes_div = this_div.parent().parent(); if (chat[chat.length - 1]['swipe_id'] > chat[chat.length - 1]['swipes'].length) { //if we swipe right while generating (the swipe ID is greater than what we are viewing now) chat[chat.length - 1]['swipe_id'] = chat[chat.length - 1]['swipes'].length; //show that message slot (will be '...' while generating) @@ -8350,7 +8363,7 @@ const swipe_right = () => { } // handles animated transitions when swipe right, specifically height transitions between messages if (run_generate || run_swipe_right) { - let this_mes_block = this_mes_div.children('.mes_block').children('.mes_text'); + let this_mes_block = this_mes_div.find('.mes_block .mes_text'); const this_mes_div_height = this_mes_div[0].scrollHeight; const this_mes_block_height = this_mes_block[0].scrollHeight; @@ -9407,9 +9420,9 @@ jQuery(async function () { ///// SWIPE BUTTON CLICKS /////// - $(document).on('click', '.swipe_right', swipe_right); - - $(document).on('click', '.swipe_left', swipe_left); + //limit swiping to only last message clicks + $(document).on('click', '.last_mes .swipe_right', swipe_right); + $(document).on('click', '.last_mes .swipe_left', swipe_left); const debouncedCharacterSearch = debounce((searchQuery) => { entitiesFilter.setFilterData(FILTER_TYPES.SEARCH, searchQuery); diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index edb19ff052..4a09e3aeb6 100644 --- a/public/scripts/extensions/expressions/index.js +++ b/public/scripts/extensions/expressions/index.js @@ -12,6 +12,8 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from ' import { isFunctionCallingSupported } from '../../openai.js'; import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js'; import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; +import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js'; +import { SlashCommandClosure } from '../../slash-commands/SlashCommandClosure.js'; export { MODULE_NAME }; const MODULE_NAME = 'expressions'; @@ -2134,18 +2136,42 @@ function migrateSettings() { name: 'classify-expressions', aliases: ['expressions'], callback: async (args) => { - const list = await getExpressionsList(); - switch (String(args.format).toLowerCase()) { - case 'json': - return JSON.stringify(list); - default: - return list.join(', '); + /** @type {import('../../slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */ + // @ts-ignore + let returnType = args.return; + + // Old legacy return type handling + if (args.format) { + toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning'); + const type = String(args?.format).toLowerCase().trim(); + switch (type) { + case 'json': + returnType = 'object'; + break; + default: + returnType = 'pipe'; + break; + } } + + // Now the actual new return type handling + const list = await getExpressionsList(); + + return await slashCommandReturnHelper.doReturn(returnType ?? 'pipe', list, { objectToStringFunc: list => list.join(', ') }); }, namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'return', + description: 'The way how you want the return value to be provided', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: 'pipe', + enumList: slashCommandReturnHelper.enumList({ allowObject: true }), + forceEnum: true, + }), + // TODO remove some day SlashCommandNamedArgument.fromProps({ name: 'format', - description: 'The format to return the list in: comma-separated plain text or JSON array. Default is plain text.', + description: '!!! DEPRECATED - use "return" instead !!! The format to return the list in: comma-separated plain text or JSON array. Default is plain text.', typeList: [ARGUMENT_TYPE.STRING], enumList: [ new SlashCommandEnumValue('plain', null, enumTypes.enum, ', '), diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index ab0b1bcb53..e96575d56c 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -20,7 +20,7 @@ import { } from '../../../script.js'; import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules, renderExtensionTemplateAsync, writeExtensionField } from '../../extensions.js'; import { selected_group } from '../../group-chats.js'; -import { stringFormat, initScrollHeight, resetScrollHeight, getCharaFilename, saveBase64AsFile, getBase64Async, delay, isTrueBoolean, debounce } from '../../utils.js'; +import { stringFormat, initScrollHeight, resetScrollHeight, getCharaFilename, saveBase64AsFile, getBase64Async, delay, isTrueBoolean, debounce, isFalseBoolean } from '../../utils.js'; import { getMessageTimeStamp, humanizedDateTime } from '../../RossAscends-mods.js'; import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js'; import { getNovelUnlimitedImageGeneration, getNovelAnlas, loadNovelSubscriptionData } from '../../nai-settings.js'; @@ -31,6 +31,7 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from ' import { debounce_timeout } from '../../constants.js'; import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js'; import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js'; +import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; export { MODULE_NAME }; const MODULE_NAME = 'sd'; @@ -221,7 +222,6 @@ const defaultSettings = { // Refine mode refine_mode: false, - expand: false, interactive_mode: false, multimodal_captioning: false, snap: false, @@ -240,7 +240,7 @@ const defaultSettings = { drawthings_auth: '', hr_upscaler: 'Latent', - hr_scale: 2.0, + hr_scale: 1.0, hr_scale_min: 1.0, hr_scale_max: 4.0, hr_scale_step: 0.1, @@ -260,10 +260,6 @@ const defaultSettings = { clip_skip: 1, // NovelAI settings - novel_upscale_ratio_min: 1.0, - novel_upscale_ratio_max: 4.0, - novel_upscale_ratio_step: 0.1, - novel_upscale_ratio: 1.0, novel_anlas_guard: false, novel_sm: false, novel_sm_dyn: false, @@ -416,7 +412,6 @@ async function loadSettings() { $('#sd_hr_scale').val(extension_settings.sd.hr_scale).trigger('input'); $('#sd_denoising_strength').val(extension_settings.sd.denoising_strength).trigger('input'); $('#sd_hr_second_pass_steps').val(extension_settings.sd.hr_second_pass_steps).trigger('input'); - $('#sd_novel_upscale_ratio').val(extension_settings.sd.novel_upscale_ratio).trigger('input'); $('#sd_novel_anlas_guard').prop('checked', extension_settings.sd.novel_anlas_guard); $('#sd_novel_sm').prop('checked', extension_settings.sd.novel_sm); $('#sd_novel_sm_dyn').prop('checked', extension_settings.sd.novel_sm_dyn); @@ -430,7 +425,6 @@ async function loadSettings() { $('#sd_restore_faces').prop('checked', extension_settings.sd.restore_faces); $('#sd_enable_hr').prop('checked', extension_settings.sd.enable_hr); $('#sd_refine_mode').prop('checked', extension_settings.sd.refine_mode); - $('#sd_expand').prop('checked', extension_settings.sd.expand); $('#sd_multimodal_captioning').prop('checked', extension_settings.sd.multimodal_captioning); $('#sd_auto_url').val(extension_settings.sd.auto_url); $('#sd_auto_auth').val(extension_settings.sd.auto_auth); @@ -644,37 +638,13 @@ async function onSaveStyleClick() { saveSettingsDebounced(); } -async function expandPrompt(prompt) { - try { - const response = await fetch('/api/sd/expand', { - method: 'POST', - headers: getRequestHeaders(), - body: JSON.stringify({ prompt: prompt }), - }); - - if (!response.ok) { - throw new Error('API returned an error.'); - } - - const data = await response.json(); - return data.prompt; - } catch { - return prompt; - } -} - /** - * Modifies prompt based on auto-expansion and user inputs. + * Modifies prompt based on user inputs. * @param {string} prompt Prompt to refine - * @param {boolean} allowExpand Whether to allow auto-expansion * @param {boolean} isNegative Whether the prompt is a negative one * @returns {Promise} Refined prompt */ -async function refinePrompt(prompt, allowExpand, isNegative = false) { - if (allowExpand && extension_settings.sd.expand) { - prompt = await expandPrompt(prompt); - } - +async function refinePrompt(prompt, isNegative) { if (extension_settings.sd.refine_mode) { const text = isNegative ? '

Review and edit the negative prompt:

' : '

Review and edit the prompt:

'; 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 = '') { return process(result); } -function onExpandInput() { - extension_settings.sd.expand = !!$(this).prop('checked'); - saveSettingsDebounced(); -} - function onRefineModeInput() { extension_settings.sd.refine_mode = !!$('#sd_refine_mode').prop('checked'); saveSettingsDebounced(); @@ -969,12 +934,6 @@ async function onViewAnlasClick() { toastr.info(`Free image generation: ${unlimitedGeneration ? 'Yes' : 'No'}`, `Anlas: ${anlas}`); } -function onNovelUpscaleRatioInput() { - extension_settings.sd.novel_upscale_ratio = Number($('#sd_novel_upscale_ratio').val()); - $('#sd_novel_upscale_ratio_value').val(extension_settings.sd.novel_upscale_ratio.toFixed(1)); - saveSettingsDebounced(); -} - function onNovelAnlasGuardInput() { extension_settings.sd.novel_anlas_guard = !!$('#sd_novel_anlas_guard').prop('checked'); saveSettingsDebounced(); @@ -2272,6 +2231,25 @@ function getRawLastMessage() { return `((${processReply(lastMessage.mes)})), (${processReply(character.scenario)}:0.7), (${processReply(character.description)}:0.5)`; } +/** + * Ensure that the selected option exists in the dropdown. + * @param {string} setting Setting key + * @param {string} selector Dropdown selector + * @returns {void} + */ +function ensureSelectionExists(setting, selector) { + /** @type {HTMLSelectElement} */ + const selectElement = document.querySelector(selector); + if (!selectElement) { + return; + } + const options = Array.from(selectElement.options); + const value = extension_settings.sd[setting]; + if (selectElement.selectedOptions.length && !options.some(option => option.value === value)) { + extension_settings.sd[setting] = selectElement.selectedOptions[0].value; + } +} + /** * Generates an image based on the given trigger word. * @param {string} initiator The initiator of the image generation @@ -2292,8 +2270,8 @@ async function generatePicture(initiator, args, trigger, message, callback) { return; } - extension_settings.sd.sampler = $('#sd_sampler').find(':selected').val(); - extension_settings.sd.model = $('#sd_model').find(':selected').val(); + ensureSelectionExists('sampler', '#sd_sampler'); + ensureSelectionExists('model', '#sd_model'); trigger = trigger.trim(); const generationType = getGenerationType(trigger); @@ -2441,7 +2419,7 @@ async function getPrompt(generationType, message, trigger, quietPrompt, combineN } if (generationType !== generationMode.FREE) { - prompt = await refinePrompt(prompt, true); + prompt = await refinePrompt(prompt, false); } return prompt; @@ -2469,7 +2447,7 @@ function generateFreeModePrompt(trigger, combineNegatives) { return message.original_avatar.replace(/\.[^/.]+$/, ''); } } - throw new Error('No usable messages found.'); + return ''; }; const key = getLastCharacterKey(); @@ -3031,7 +3009,7 @@ async function generateNovelImage(prompt, negativePrompt, signal) { width: width, height: height, negative_prompt: negativePrompt, - upscale_ratio: extension_settings.sd.novel_upscale_ratio, + upscale_ratio: extension_settings.sd.hr_scale, decrisper: extension_settings.sd.novel_decrisper, sm: sm, sm_dyn: sm_dyn, @@ -3613,8 +3591,8 @@ async function sdMessageButton(e) { try { setBusyIcon(true); if (hasSavedImage) { - const prompt = await refinePrompt(message.extra.title, false, false); - const negative = hasSavedNegative ? await refinePrompt(message.extra.negative, false, true) : ''; + const prompt = await refinePrompt(message.extra.title, false); + const negative = hasSavedNegative ? await refinePrompt(message.extra.negative, true) : ''; message.extra.title = prompt; const generationType = message?.extra?.generationType ?? generationMode.FREE; @@ -3756,8 +3734,8 @@ async function onImageSwiped({ message, element, direction }) { eventSource.once(CUSTOM_STOP_EVENT, stopListener); const callback = () => { }; const hasNegative = message.extra.negative; - const prompt = await refinePrompt(message.extra.title, false, false); - const negativePromptPrefix = hasNegative ? await refinePrompt(message.extra.negative, false, true) : ''; + const prompt = await refinePrompt(message.extra.title, false); + const negativePromptPrefix = hasNegative ? await refinePrompt(message.extra.negative, true) : ''; const characterName = context.groupId ? context.groups[Object.keys(context.groups).filter(x => context.groups[x].id === context.groupId)[0]]?.id?.toString() : context.characters[context.characterId]?.name; @@ -3788,12 +3766,85 @@ async function onImageSwiped({ message, element, direction }) { await context.saveChat(); } +/** + * Applies the command arguments to the extension settings. + * @typedef {import('../../slash-commands/SlashCommand.js').NamedArguments} NamedArguments + * @typedef {import('../../slash-commands/SlashCommand.js').NamedArgumentsCapture} NamedArgumentsCapture + * @param {NamedArguments | NamedArgumentsCapture} args - Command arguments + * @returns {Record} - Current settings before applying the command arguments + */ +function applyCommandArguments(args) { + const overrideSettings = {}; + const currentSettings = {}; + const settingMap = { + 'edit': 'refine_mode', + 'extend': 'free_extend', + 'multimodal': 'multimodal_captioning', + 'seed': 'seed', + 'width': 'width', + 'height': 'height', + 'steps': 'steps', + 'cfg': 'scale', + 'skip': 'clip_skip', + 'model': 'model', + 'sampler': 'sampler', + 'scheduler': 'scheduler', + 'vae': 'vae', + 'upscaler': 'hr_upscaler', + 'scale': 'hr_scale', + 'hires': 'enable_hr', + 'denoise': 'denoising_strength', + '2ndpass': 'hr_second_pass_steps', + 'faces': 'restore_faces', + }; + + for (const [param, setting] of Object.entries(settingMap)) { + if (args[param] === undefined || defaultSettings[setting] === undefined) { + continue; + } + currentSettings[setting] = extension_settings.sd[setting]; + const value = String(args[param]); + const type = typeof defaultSettings[setting]; + switch (type) { + case 'boolean': + overrideSettings[setting] = isTrueBoolean(value) || !isFalseBoolean(value); + break; + case 'number': + overrideSettings[setting] = Number(value); + break; + default: + overrideSettings[setting] = value; + break; + } + } + + Object.assign(extension_settings.sd, overrideSettings); + return currentSettings; +} + jQuery(async () => { await addSDGenButtons(); + const getSelectEnumProvider = (id, text) => () => Array.from(document.querySelectorAll(`#${id} > [value]`)).map(x => new SlashCommandEnumValue(x.getAttribute('value'), text ? x.textContent : null)); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'imagine', - callback: (args, trigger) => generatePicture(initiators.command, args, String(trigger)), + returns: 'URL of the generated image, or an empty string if the generation failed', + callback: async (args, trigger) => { + const currentSettings = applyCommandArguments(args); + + try { + return await generatePicture(initiators.command, args, String(trigger)); + } catch (error) { + console.error('Failed to generate image:', error); + return ''; + } finally { + if (Object.keys(currentSettings).length) { + Object.assign(extension_settings.sd, currentSettings); + saveSettingsDebounced(); + } + } + }, aliases: ['sd', 'img', 'image'], namedArgumentList: [ new SlashCommandNamedArgument( @@ -3803,6 +3854,164 @@ jQuery(async () => { name: 'negative', description: 'negative prompt prefix', typeList: [ARGUMENT_TYPE.STRING], + isRequired: false, + acceptsMultiple: false, + }), + SlashCommandNamedArgument.fromProps({ + name: 'extend', + description: 'auto-extend free mode prompts with the LLM', + typeList: [ARGUMENT_TYPE.BOOLEAN], + enumProvider: commonEnumProviders.boolean('trueFalse'), + isRequired: false, + acceptsMultiple: false, + }), + SlashCommandNamedArgument.fromProps({ + name: 'edit', + description: 'edit the prompt before generation', + typeList: [ARGUMENT_TYPE.BOOLEAN], + enumProvider: commonEnumProviders.boolean('trueFalse'), + isRequired: false, + acceptsMultiple: false, + }), + SlashCommandNamedArgument.fromProps({ + name: 'multimodal', + description: 'use multimodal captioning (for portraits only)', + typeList: [ARGUMENT_TYPE.BOOLEAN], + enumProvider: commonEnumProviders.boolean('trueFalse'), + isRequired: false, + acceptsMultiple: false, + }), + SlashCommandNamedArgument.fromProps({ + name: 'snap', + description: 'snap auto-adjusted dimensions to the nearest known resolution (portraits and backgrounds only)', + typeList: [ARGUMENT_TYPE.BOOLEAN], + enumProvider: commonEnumProviders.boolean('trueFalse'), + isRequired: false, + acceptsMultiple: false, + }), + SlashCommandNamedArgument.fromProps({ + name: 'seed', + description: 'random seed', + isRequired: false, + typeList: [ARGUMENT_TYPE.NUMBER], + acceptsMultiple: false, + }), + SlashCommandNamedArgument.fromProps({ + name: 'width', + description: 'image width', + isRequired: false, + typeList: [ARGUMENT_TYPE.NUMBER], + acceptsMultiple: false, + }), + SlashCommandNamedArgument.fromProps({ + name: 'height', + description: 'image height', + isRequired: false, + typeList: [ARGUMENT_TYPE.NUMBER], + acceptsMultiple: false, + }), + SlashCommandNamedArgument.fromProps({ + name: 'steps', + description: 'number of steps', + isRequired: false, + typeList: [ARGUMENT_TYPE.NUMBER], + acceptsMultiple: false, + }), + SlashCommandNamedArgument.fromProps({ + name: 'cfg', + description: 'CFG scale', + isRequired: false, + typeList: [ARGUMENT_TYPE.NUMBER], + acceptsMultiple: false, + }), + SlashCommandNamedArgument.fromProps({ + name: 'skip', + description: 'CLIP skip layers', + isRequired: false, + typeList: [ARGUMENT_TYPE.NUMBER], + acceptsMultiple: false, + }), + SlashCommandNamedArgument.fromProps({ + name: 'model', + description: 'model override', + isRequired: false, + typeList: [ARGUMENT_TYPE.STRING], + acceptsMultiple: false, + forceEnum: true, + enumProvider: getSelectEnumProvider('sd_model', true), + }), + SlashCommandNamedArgument.fromProps({ + name: 'sampler', + description: 'sampler override', + isRequired: false, + typeList: [ARGUMENT_TYPE.STRING], + acceptsMultiple: false, + forceEnum: true, + enumProvider: getSelectEnumProvider('sd_sampler', false), + }), + SlashCommandNamedArgument.fromProps({ + name: 'scheduler', + description: 'scheduler override', + isRequired: false, + typeList: [ARGUMENT_TYPE.STRING], + acceptsMultiple: false, + forceEnum: true, + enumProvider: getSelectEnumProvider('sd_scheduler', false), + }), + SlashCommandNamedArgument.fromProps({ + name: 'vae', + description: 'VAE name override', + isRequired: false, + typeList: [ARGUMENT_TYPE.STRING], + acceptsMultiple: false, + forceEnum: true, + enumProvider: getSelectEnumProvider('sd_vae', false), + }), + SlashCommandNamedArgument.fromProps({ + name: 'upscaler', + description: 'upscaler override', + isRequired: false, + typeList: [ARGUMENT_TYPE.STRING], + acceptsMultiple: false, + forceEnum: true, + enumProvider: getSelectEnumProvider('sd_hr_upscaler', false), + }), + SlashCommandNamedArgument.fromProps({ + name: 'hires', + description: 'enable high-res fix', + isRequired: false, + typeList: [ARGUMENT_TYPE.BOOLEAN], + acceptsMultiple: false, + enumProvider: commonEnumProviders.boolean('trueFalse'), + }), + SlashCommandNamedArgument.fromProps({ + name: 'scale', + description: 'upscale amount', + isRequired: false, + typeList: [ARGUMENT_TYPE.NUMBER], + acceptsMultiple: false, + }), + SlashCommandNamedArgument.fromProps({ + name: 'denoise', + description: 'denoising strength', + isRequired: false, + typeList: [ARGUMENT_TYPE.NUMBER], + acceptsMultiple: false, + }), + SlashCommandNamedArgument.fromProps({ + name: '2ndpass', + description: 'second pass steps', + isRequired: false, + typeList: [ARGUMENT_TYPE.NUMBER], + acceptsMultiple: false, + }), + SlashCommandNamedArgument.fromProps({ + name: 'faces', + description: 'restore faces', + isRequired: false, + typeList: [ARGUMENT_TYPE.BOOLEAN], + acceptsMultiple: false, + enumProvider: commonEnumProviders.boolean('trueFalse'), }), ], unnamedArgumentList: [ @@ -3823,6 +4032,66 @@ jQuery(async () => { `, })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'imagine-source', + aliases: ['sd-source', 'img-source'], + returns: 'a name of the current generation source', + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'source name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: false, + forceEnum: true, + enumProvider: getSelectEnumProvider('sd_source', true), + }), + ], + helpString: 'If an argument is provided, change the source of the image generation, e.g. /imagine-source comfy. Returns the current source.', + callback: async (_args, name) => { + if (!name) { + return extension_settings.sd.source; + } + const isKnownSource = Object.keys(sources).includes(String(name)); + if (!isKnownSource) { + throw new Error('The value provided is not a valid image generation source.'); + } + const option = document.querySelector(`#sd_source [value="${name}"]`); + if (!(option instanceof HTMLOptionElement)) { + throw new Error('Could not find the source option in the dropdown.'); + } + option.selected = true; + await onSourceChange(); + return extension_settings.sd.source; + }, + })); + + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'imagine-style', + aliases: ['sd-style', 'img-style'], + returns: 'a name of the current style', + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'style name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: false, + forceEnum: true, + enumProvider: getSelectEnumProvider('sd_style', false), + }), + ], + helpString: 'If an argument is provided, change the style of the image generation, e.g. /imagine-style MyStyle. Returns the current style.', + callback: async (_args, name) => { + if (!name) { + return extension_settings.sd.style; + } + const option = document.querySelector(`#sd_style [value="${name}"]`); + if (!(option instanceof HTMLOptionElement)) { + throw new Error('Could not find the style option in the dropdown.'); + } + option.selected = true; + onStyleSelect(); + return extension_settings.sd.style; + }, + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'imagine-comfy-workflow', callback: changeComfyWorkflow, @@ -3832,7 +4101,7 @@ jQuery(async () => { description: 'workflow name', typeList: [ARGUMENT_TYPE.STRING], isRequired: true, - enumProvider: () => Array.from(document.querySelectorAll('#sd_comfy_workflow > [value]')).map(x => x.getAttribute('value')).map(workflow => new SlashCommandEnumValue(workflow)), + enumProvider: getSelectEnumProvider('sd_comfy_workflow', false), }), ], helpString: '(workflowName) - change the workflow to be used for image generation with ComfyUI, e.g.
/imagine-comfy-workflow MyWorkflow
', @@ -3874,7 +4143,6 @@ jQuery(async () => { $('#sd_hr_scale').on('input', onHrScaleInput); $('#sd_denoising_strength').on('input', onDenoisingStrengthInput); $('#sd_hr_second_pass_steps').on('input', onHrSecondPassStepsInput); - $('#sd_novel_upscale_ratio').on('input', onNovelUpscaleRatioInput); $('#sd_novel_anlas_guard').on('input', onNovelAnlasGuardInput); $('#sd_novel_view_anlas').on('click', onViewAnlasClick); $('#sd_novel_sm').on('input', onNovelSmInput); @@ -3887,7 +4155,6 @@ jQuery(async () => { $('#sd_comfy_open_workflow_editor').on('click', onComfyOpenWorkflowEditorClick); $('#sd_comfy_new_workflow').on('click', onComfyNewWorkflowClick); $('#sd_comfy_delete_workflow').on('click', onComfyDeleteWorkflowClick); - $('#sd_expand').on('input', onExpandInput); $('#sd_style').on('change', onStyleSelect); $('#sd_save_style').on('click', onSaveStyleClick); $('#sd_delete_style').on('click', onDeleteStyleClick); diff --git a/public/scripts/extensions/stable-diffusion/settings.html b/public/scripts/extensions/stable-diffusion/settings.html index f43ed7246d..2efa74d838 100644 --- a/public/scripts/extensions/stable-diffusion/settings.html +++ b/public/scripts/extensions/stable-diffusion/settings.html @@ -27,11 +27,6 @@ Extend free mode prompts (interactive/commands) -
-
+
Upscale by @@ -332,14 +327,6 @@
-
- - Upscale by - - - -
-
CLIP Skip diff --git a/public/scripts/openai.js b/public/scripts/openai.js index df798132bc..20c1413169 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -724,6 +724,12 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul if (chatCompletion.canAfford(chatMessage)) { if (type === 'continue' && oai_settings.continue_prefill && chatPrompt === firstNonInjected) { + // in case we are using continue_prefill and the latest message is an assistant message, we want to prepend the users assistant prefill on the message + if (chatPrompt.role === 'assistant') { + const collection = new MessageCollection('continuePrefill', new Message(chatMessage.role, substituteParams(oai_settings.assistant_prefill + '\n\n') + chatMessage.content, chatMessage.identifier)); + chatCompletion.add(collection, -1); + continue; + } const collection = new MessageCollection('continuePrefill', chatMessage); chatCompletion.add(collection, -1); continue; @@ -1770,8 +1776,8 @@ async function sendOpenAIRequest(type, messages, signal) { generate_data['claude_use_sysprompt'] = oai_settings.claude_use_sysprompt; generate_data['stop'] = getCustomStoppingStrings(); // Claude shouldn't have limits on stop strings. generate_data['human_sysprompt_message'] = substituteParams(oai_settings.human_sysprompt_message); - // Don't add a prefill on quiet gens (summarization) - if (!isQuiet) { + // Don't add a prefill on quiet gens (summarization) and when using continue prefill. + if (!isQuiet && !(isContinue && oai_settings.continue_prefill)) { generate_data['assistant_prefill'] = isImpersonate ? substituteParams(oai_settings.assistant_impersonation) : substituteParams(oai_settings.assistant_prefill); } } diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index 8b2a81a167..bd618bad02 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -290,6 +290,7 @@ let power_user = { restore_user_input: true, reduced_motion: false, compact_input_area: true, + show_swipe_num_all_messages: false, auto_connect: false, auto_load_chat: false, forbid_external_media: true, @@ -469,6 +470,11 @@ function switchCompactInputArea() { $('#compact_input_area').prop('checked', power_user.compact_input_area); } +export function switchSwipeNumAllMessages() { + $('#show_swipe_num_all_messages').prop('checked', power_user.show_swipe_num_all_messages); + $('.mes:not(.last_mes) .swipes-counter').css('opacity', '').toggle(power_user.show_swipe_num_all_messages); +} + var originalSliderValues = []; async function switchLabMode() { @@ -1283,6 +1289,13 @@ function applyTheme(name) { switchCompactInputArea(); }, }, + { + key: 'show_swipe_num_all_messages', + action: () => { + $('#show_swipe_num_all_messages').prop('checked', power_user.show_swipe_num_all_messages); + switchSwipeNumAllMessages(); + }, + }, ]; for (const { key, selector, type, action } of themeProperties) { @@ -1352,6 +1365,7 @@ function applyPowerUserSettings() { switchHideChatAvatars(); switchTokenCount(); switchMessageActions(); + switchSwipeNumAllMessages(); } function getExampleMessagesBehavior() { @@ -2296,6 +2310,7 @@ function getThemeObject(name) { zoomed_avatar_magnification: power_user.zoomed_avatar_magnification, reduced_motion: power_user.reduced_motion, compact_input_area: power_user.compact_input_area, + show_swipe_num_all_messages: power_user.show_swipe_num_all_messages, }; } @@ -3755,6 +3770,12 @@ $(document).ready(() => { saveSettingsDebounced(); }); + $('#show_swipe_num_all_messages').on('input', function () { + power_user.show_swipe_num_all_messages = !!$(this).prop('checked'); + switchSwipeNumAllMessages(); + saveSettingsDebounced(); + }); + $('#auto-connect-checkbox').on('input', function () { power_user.auto_connect = !!$(this).prop('checked'); saveSettingsDebounced(); diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 1e0413b7ba..bc0941968e 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -70,6 +70,7 @@ import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js'; import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js'; +import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js'; export { executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand, }; @@ -242,6 +243,7 @@ export function initDefaultSlashCommands() { SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'sendas', callback: sendMessageAs, + returns: 'Optionally the text of the sent message, if specified in the "return" argument', namedArgumentList: [ SlashCommandNamedArgument.fromProps({ name: 'name', @@ -269,6 +271,14 @@ export function initDefaultSlashCommands() { typeList: [ARGUMENT_TYPE.NUMBER], enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), }), + SlashCommandNamedArgument.fromProps({ + name: 'return', + description: 'The way how you want the return value to be provided', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: 'none', + enumList: slashCommandReturnHelper.enumList({ allowObject: true }), + forceEnum: true, + }), ], unnamedArgumentList: [ new SlashCommandArgument( @@ -301,6 +311,7 @@ export function initDefaultSlashCommands() { name: 'sys', callback: sendNarratorMessage, aliases: ['nar'], + returns: 'Optionally the text of the sent message, if specified in the "return" argument', namedArgumentList: [ new SlashCommandNamedArgument( 'compact', @@ -316,6 +327,14 @@ export function initDefaultSlashCommands() { typeList: [ARGUMENT_TYPE.NUMBER], enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), }), + SlashCommandNamedArgument.fromProps({ + name: 'return', + description: 'The way how you want the return value to be provided', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: 'none', + enumList: slashCommandReturnHelper.enumList({ allowObject: true }), + forceEnum: true, + }), ], unnamedArgumentList: [ new SlashCommandArgument( @@ -355,6 +374,7 @@ export function initDefaultSlashCommands() { SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'comment', callback: sendCommentMessage, + returns: 'Optionally the text of the sent message, if specified in the "return" argument', namedArgumentList: [ new SlashCommandNamedArgument( 'compact', @@ -370,6 +390,14 @@ export function initDefaultSlashCommands() { typeList: [ARGUMENT_TYPE.NUMBER], enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), }), + SlashCommandNamedArgument.fromProps({ + name: 'return', + description: 'The way how you want the return value to be provided', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: 'none', + enumList: slashCommandReturnHelper.enumList({ allowObject: true }), + forceEnum: true, + }), ], unnamedArgumentList: [ new SlashCommandArgument( @@ -509,7 +537,7 @@ export function initDefaultSlashCommands() { SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'ask', callback: askCharacter, - returns: 'the generated text', + returns: 'Optionally the text of the sent message, if specified in the "return" argument', namedArgumentList: [ SlashCommandNamedArgument.fromProps({ name: 'name', @@ -518,6 +546,14 @@ export function initDefaultSlashCommands() { isRequired: true, enumProvider: commonEnumProviders.characters('character'), }), + SlashCommandNamedArgument.fromProps({ + name: 'return', + description: 'The way how you want the return value to be provided', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: 'pipe', + enumList: slashCommandReturnHelper.enumList({ allowObject: true }), + forceEnum: true, + }), ], unnamedArgumentList: [ new SlashCommandArgument( @@ -556,6 +592,7 @@ export function initDefaultSlashCommands() { SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'send', callback: sendUserMessageCallback, + returns: 'Optionally the text of the sent message, if specified in the "return" argument', namedArgumentList: [ new SlashCommandNamedArgument( 'compact', @@ -578,6 +615,14 @@ export function initDefaultSlashCommands() { defaultValue: '{{user}}', enumProvider: commonEnumProviders.personas, }), + SlashCommandNamedArgument.fromProps({ + name: 'return', + description: 'The way how you want the return value to be provided', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: 'none', + enumList: slashCommandReturnHelper.enumList({ allowObject: true }), + forceEnum: true, + }), ], unnamedArgumentList: [ new SlashCommandArgument( @@ -1568,12 +1613,21 @@ export function initDefaultSlashCommands() { SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'listinjects', callback: listInjectsCallback, - helpString: 'Lists all script injections for the current chat. Displays injects in a popup by default. Use the format argument to change the output format.', - returns: 'JSON object of script injections', + helpString: 'Lists all script injections for the current chat. Displays injects in a popup by default. Use the return argument to change the return type.', + returns: 'Optionalls the JSON object of script injections', namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'return', + description: 'The way how you want the return value to be provided', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: 'popup-html', + enumList: slashCommandReturnHelper.enumList({ allowPipe: false, allowObject: true, allowChat: true, allowPopup: true, allowTextVersion: false }), + forceEnum: true, + }), + // TODO remove some day SlashCommandNamedArgument.fromProps({ name: 'format', - description: 'output format', + description: '!!! DEPRECATED - use "return" instead !!! output format', typeList: [ARGUMENT_TYPE.STRING], isRequired: true, forceEnum: true, @@ -1842,37 +1896,43 @@ function injectCallback(args, value) { } async function listInjectsCallback(args) { - const type = String(args?.format).toLowerCase().trim(); - if (!chat_metadata.script_injects || !Object.keys(chat_metadata.script_injects).length) { - type !== 'none' && toastr.info('No script injections for the current chat'); - return JSON.stringify({}); - } - - const injects = Object.entries(chat_metadata.script_injects) - .map(([id, inject]) => { - const position = Object.entries(extension_prompt_types); - const positionName = position.find(([_, value]) => value === inject.position)?.[0] ?? 'unknown'; - return `* **${id}**: ${inject.value} (${positionName}, depth: ${inject.depth}, scan: ${inject.scan ?? false}, role: ${inject.role ?? extension_prompt_roles.SYSTEM})`; - }) - .join('\n'); - - const converter = new showdown.Converter(); - const messageText = `### Script injections:\n${injects}`; - const htmlMessage = DOMPurify.sanitize(converter.makeHtml(messageText)); - - switch (type) { - case 'none': - break; - case 'chat': - sendSystemMessage(system_message_types.GENERIC, htmlMessage); - break; - case 'popup': - default: - await callGenericPopup(htmlMessage, POPUP_TYPE.TEXT); - break; + /** @type {import('./slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */ + let returnType = args.return; + + // Old legacy return type handling + if (args.format) { + toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning'); + const type = String(args?.format).toLowerCase().trim(); + if (!chat_metadata.script_injects || !Object.keys(chat_metadata.script_injects).length) { + type !== 'none' && toastr.info('No script injections for the current chat'); + } + switch (type) { + case 'none': + returnType = 'none'; + break; + case 'chat': + returnType = 'chat-html'; + break; + case 'popup': + default: + returnType = 'popup-html'; + break; + } } - return JSON.stringify(chat_metadata.script_injects); + // Now the actual new return type handling + const buildTextValue = (injects) => { + const injectsStr = Object.entries(injects) + .map(([id, inject]) => { + const position = Object.entries(extension_prompt_types); + const positionName = position.find(([_, value]) => value === inject.position)?.[0] ?? 'unknown'; + return `* **${id}**: ${inject.value} (${positionName}, depth: ${inject.depth}, scan: ${inject.scan ?? false}, role: ${inject.role ?? extension_prompt_roles.SYSTEM})`; + }) + .join('\n'); + return `### Script injections:\n${injectsStr || 'No script injections for the current chat'}`; + }; + + return await slashCommandReturnHelper.doReturn(returnType ?? 'popup-html', chat_metadata.script_injects ?? {}, { objectToStringFunc: buildTextValue }); } /** @@ -2559,7 +2619,7 @@ async function askCharacter(args, text) { // Not supported in group chats // TODO: Maybe support group chats? if (selected_group) { - toastr.error('Cannot run /ask command in a group chat!'); + toastr.warning('Cannot run /ask command in a group chat!'); return ''; } @@ -2633,7 +2693,9 @@ async function askCharacter(args, text) { } } - return askResult; + const message = askResult ? chat[chat.length - 1] : null; + + return await slashCommandReturnHelper.doReturn(args.return ?? 'pipe', message, { objectToStringFunc: x => x.mes }); } async function hideMessageCallback(_, arg) { @@ -2908,7 +2970,7 @@ function findPersonaByName(name) { async function sendUserMessageCallback(args, text) { if (!text) { - console.warn('WARN: No text provided for /send command'); + toastr.warning('You must specify text to send'); return; } @@ -2924,16 +2986,17 @@ async function sendUserMessageCallback(args, text) { insertAt = chat.length + insertAt; } + let message; if ('name' in args) { const name = args.name || ''; const avatar = findPersonaByName(name) || user_avatar; - await sendMessageAsUser(text, bias, insertAt, compact, name, avatar); + message = await sendMessageAsUser(text, bias, insertAt, compact, name, avatar); } else { - await sendMessageAsUser(text, bias, insertAt, compact); + message = await sendMessageAsUser(text, bias, insertAt, compact); } - return ''; + return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes }); } async function deleteMessagesByNameCallback(_, name) { @@ -3221,30 +3284,20 @@ export function getNameAndAvatarForMessage(character, name = null) { export async function sendMessageAs(args, text) { if (!text) { + toastr.warning('You must specify text to send as'); return ''; } - let name; + let name = args.name?.trim(); let mesText; - if (args.name) { - name = args.name.trim(); - - if (!name && !text) { - toastr.warning('You must specify a name and text to send as'); - return ''; - } - } else { + if (!name) { const namelessWarningKey = 'sendAsNamelessWarningShown'; if (localStorage.getItem(namelessWarningKey) !== 'true') { toastr.warning('To avoid confusion, please use /sendas name="Character Name"', 'Name defaulted to {{char}}', { timeOut: 10000 }); localStorage.setItem(namelessWarningKey, 'true'); } name = name2; - if (!text) { - toastr.warning('You must specify text to send as'); - return ''; - } } mesText = text.trim(); @@ -3321,11 +3374,12 @@ export async function sendMessageAs(args, text) { await saveChatConditional(); } - return ''; + return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes }); } export async function sendNarratorMessage(args, text) { if (!text) { + toastr.warning('You must specify text to send'); return ''; } @@ -3374,7 +3428,7 @@ export async function sendNarratorMessage(args, text) { await saveChatConditional(); } - return ''; + return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes }); } export async function promptQuietForLoudResponse(who, text) { @@ -3420,6 +3474,7 @@ export async function promptQuietForLoudResponse(who, text) { async function sendCommentMessage(args, text) { if (!text) { + toastr.warning('You must specify text to send'); return ''; } @@ -3462,7 +3517,7 @@ async function sendCommentMessage(args, text) { await saveChatConditional(); } - return ''; + return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes }); } /** diff --git a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js index 5a40965c13..b96b83a53b 100644 --- a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js +++ b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js @@ -36,6 +36,7 @@ export const enumIcons = { message: 'đŸ’Ŧ', voice: '🎤', server: 'đŸ–Ĩī¸', + popup: '🗔', true: '✔ī¸', false: '❌', diff --git a/public/scripts/slash-commands/SlashCommandReturnHelper.js b/public/scripts/slash-commands/SlashCommandReturnHelper.js new file mode 100644 index 0000000000..3601c30cd1 --- /dev/null +++ b/public/scripts/slash-commands/SlashCommandReturnHelper.js @@ -0,0 +1,80 @@ +import { sendSystemMessage, system_message_types } from '../../script.js'; +import { callGenericPopup, POPUP_TYPE } from '../popup.js'; +import { escapeHtml } from '../utils.js'; +import { enumIcons } from './SlashCommandCommonEnumsProvider.js'; +import { enumTypes, SlashCommandEnumValue } from './SlashCommandEnumValue.js'; + +/** @typedef {'pipe'|'object'|'chat-html'|'chat-text'|'popup-html'|'popup-text'|'toast-html'|'toast-text'|'console'|'none'} SlashCommandReturnType */ + +export const slashCommandReturnHelper = { + // Without this, VSCode formatter fucks up JS docs. Don't ask me why. + _: false, + + /** + * Gets/creates the enum list of types of return relevant for a slash command + * + * @param {object} [options={}] Options + * @param {boolean} [options.allowPipe=true] Allow option to pipe the return value + * @param {boolean} [options.allowObject=false] Allow option to return the value as an object + * @param {boolean} [options.allowChat=false] Allow option to return the value as a chat message + * @param {boolean} [options.allowPopup=false] Allow option to return the value as a popup + * @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 + * @returns {SlashCommandEnumValue[]} The enum list + */ + enumList: ({ allowPipe = true, allowObject = false, allowChat = false, allowPopup = false, allowTextVersion = true } = {}) => [ + allowPipe && new SlashCommandEnumValue('pipe', 'Return to the pipe for the next command', enumTypes.name, '|'), + allowObject && new SlashCommandEnumValue('object', 'Return as an object (or array) to the pipe for the next command', enumTypes.variable, enumIcons.dictionary), + allowChat && new SlashCommandEnumValue('chat-html', 'Sending a chat message with the return value - Can display HTML', enumTypes.command, enumIcons.message), + allowChat && allowTextVersion && new SlashCommandEnumValue('chat-text', 'Sending a chat message with the return value - Will only display as text', enumTypes.qr, enumIcons.message), + allowPopup && new SlashCommandEnumValue('popup-html', 'Showing as a popup with the return value - Can display HTML', enumTypes.command, enumIcons.popup), + allowPopup && allowTextVersion && new SlashCommandEnumValue('popup-text', 'Showing as a popup with the return value - Will only display as text', enumTypes.qr, enumIcons.popup), + new SlashCommandEnumValue('toast-html', 'Show the return value as a toast notification - Can display HTML', enumTypes.command, 'ℹī¸'), + allowTextVersion && new SlashCommandEnumValue('toast-text', 'Show the return value as a toast notification - Will only display as text', enumTypes.qr, 'ℹī¸'), + new SlashCommandEnumValue('console', 'Log the return value (object, if it can be one) to the console', enumTypes.enum, '>'), + new SlashCommandEnumValue('none', 'No return value'), + ].filter(x => !!x), + + /** + * Handles the return value based on the specified type + * + * @param {SlashCommandReturnType} type The type of return + * @param {object|number|string} value The value to return + * @param {object} [options={}] Options + * @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 + * @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 + * @returns {Promise<*>} The processed return value + */ + async doReturn(type, value, { objectToStringFunc = o => o?.toString(), objectToHtmlFunc = null } = {}) { + const shouldHtml = type.endsWith('html'); + const actualConverterFunc = shouldHtml && objectToHtmlFunc ? objectToHtmlFunc : objectToStringFunc; + const stringValue = typeof value !== 'string' ? actualConverterFunc(value) : value; + + switch (type) { + case 'popup-html': + case 'popup-text': + case 'chat-text': + case 'chat-html': + case 'toast-text': + case 'toast-html': { + const htmlOrNotHtml = shouldHtml ? DOMPurify.sanitize((new showdown.Converter()).makeHtml(stringValue)) : escapeHtml(stringValue); + + if (type.startsWith('popup')) await callGenericPopup(htmlOrNotHtml, POPUP_TYPE.TEXT); + if (type.startsWith('chat')) sendSystemMessage(system_message_types.GENERIC, htmlOrNotHtml); + if (type.startsWith('toast')) toastr.info(htmlOrNotHtml, null, { escapeHtml: !shouldHtml }); + + return ''; + } + case 'pipe': + return stringValue ?? ''; + case 'object': + return JSON.stringify(value); + case 'console': + console.info(value); + return ''; + case 'none': + return ''; + default: + throw new Error(`Unknown return type: ${type}`); + } + }, +}; diff --git a/public/scripts/variables.js b/public/scripts/variables.js index 7c9830a365..c768c4f5e8 100644 --- a/public/scripts/variables.js +++ b/public/scripts/variables.js @@ -11,6 +11,7 @@ import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureR import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js'; import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js'; +import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js'; import { SlashCommandScope } from './slash-commands/SlashCommandScope.js'; import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js'; @@ -305,7 +306,28 @@ export function replaceVariableMacros(input) { } async function listVariablesCallback(args) { - const type = String(args?.format || '').toLowerCase().trim() || 'popup'; + /** @type {import('./slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */ + let returnType = args.return; + + // Old legacy return type handling + if (args.format) { + toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning'); + const type = String(args?.format).toLowerCase().trim(); + switch (type) { + case 'none': + returnType = 'none'; + break; + case 'chat': + returnType = 'chat-html'; + break; + case 'popup': + default: + returnType = 'popup-html'; + break; + } + } + + // Now the actual new return type handling const scope = String(args?.scope || '').toLowerCase().trim() || 'all'; if (!chat_metadata.variables) { chat_metadata.variables = {}; @@ -317,35 +339,24 @@ async function listVariablesCallback(args) { const localVariables = includeLocalVariables ? Object.entries(chat_metadata.variables).map(([name, value]) => `${name}: ${value}`) : []; const globalVariables = includeGlobalVariables ? Object.entries(extension_settings.variables.global).map(([name, value]) => `${name}: ${value}`) : []; + const buildTextValue = (_) => { + const localVariablesString = localVariables.length > 0 ? localVariables.join('\n\n') : 'No local variables'; + const globalVariablesString = globalVariables.length > 0 ? globalVariables.join('\n\n') : 'No global variables'; + const chatName = getCurrentChatId(); + + const message = [ + includeLocalVariables ? `### Local variables (${chatName}):\n${localVariablesString}` : '', + includeGlobalVariables ? `### Global variables:\n${globalVariablesString}` : '', + ].filter(x => x).join('\n\n'); + return message; + }; + const jsonVariables = [ ...Object.entries(chat_metadata.variables).map(x => ({ key: x[0], value: x[1], scope: 'local' })), ...Object.entries(extension_settings.variables.global).map(x => ({ key: x[0], value: x[1], scope: 'global' })), ]; - const localVariablesString = localVariables.length > 0 ? localVariables.join('\n\n') : 'No local variables'; - const globalVariablesString = globalVariables.length > 0 ? globalVariables.join('\n\n') : 'No global variables'; - const chatName = getCurrentChatId(); - - const converter = new showdown.Converter(); - const message = [ - includeLocalVariables ? `### Local variables (${chatName}):\n${localVariablesString}` : '', - includeGlobalVariables ? `### Global variables:\n${globalVariablesString}` : '', - ].filter(x => x).join('\n\n'); - const htmlMessage = DOMPurify.sanitize(converter.makeHtml(message)); - - switch (type) { - case 'none': - break; - case 'chat': - sendSystemMessage(system_message_types.GENERIC, htmlMessage); - break; - case 'popup': - default: - await callGenericPopup(htmlMessage, POPUP_TYPE.TEXT); - break; - } - - return JSON.stringify(jsonVariables); + return await slashCommandReturnHelper.doReturn(returnType ?? 'popup-html', jsonVariables, { objectToStringFunc: buildTextValue }); } /** @@ -916,7 +927,7 @@ export function registerVariableCommands() { name: 'listvar', callback: listVariablesCallback, aliases: ['listchatvar'], - helpString: 'List registered chat variables. Displays variables in a popup by default. Use the format argument to change the output format.', + helpString: 'List registered chat variables. Displays variables in a popup by default. Use the return argument to change the return type.', returns: 'JSON list of local variables', namedArgumentList: [ SlashCommandNamedArgument.fromProps({ @@ -932,9 +943,18 @@ export function registerVariableCommands() { new SlashCommandEnumValue('global', 'Global variables', enumTypes.enum, enumIcons.globalVariable), ], }), + SlashCommandNamedArgument.fromProps({ + name: 'return', + description: 'The way how you want the return value to be provided', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: 'popup-html', + enumList: slashCommandReturnHelper.enumList({ allowPipe: false, allowObject: true, allowChat: true, allowPopup: true, allowTextVersion: false }), + forceEnum: true, + }), + // TODO remove some day SlashCommandNamedArgument.fromProps({ name: 'format', - description: 'output format', + description: '!!! DEPRECATED - use "return" instead !!! output format', typeList: [ARGUMENT_TYPE.STRING], isRequired: true, forceEnum: true, diff --git a/public/style.css b/public/style.css index 7d55cf8325..01a49ab9f7 100644 --- a/public/style.css +++ b/public/style.css @@ -969,34 +969,53 @@ body .panelControlBar { /* SWIPE RELATED STYLES*/ +.mes { + --swipeCounterHeight: 15px; + --swipeCounterMargin: 5px; +} + .swipe_right, .swipe_left { - height: 40px; - width: 40px; + width: 25px; + height: 25px; opacity: 0.3; align-items: center; justify-content: center; z-index: 9999; grid-row-start: 2; - font-size: 30px; + font-size: 20px; cursor: pointer; align-self: center; +} + +.swipe_left { position: absolute; -bottom: 15px; -flex-flow: column; + bottom: calc(var(--swipeCounterHeight) + var(--swipeCounterMargin)); + flex-flow: column; } +.swipeRightBlock { + position: absolute; + right: 0; + bottom: 0; +} .swipes-counter { color: var(--SmartThemeBodyColor); font-size: 12px; - padding: 0; + padding: 0 5px; font-family: var(--mainFontFamily); font-weight: 400; align-self: center; min-width: 40px; display: flex; justify-content: center; + margin-bottom: var(--swipeCounterMargin); + height: var(--swipeCounterHeight); +} + +.mes:not(.last_mes) .swipes-counter { + opacity: 0.3; } .swipe_left { @@ -1006,7 +1025,7 @@ flex-flow: column; .swipe_right { right: 5px; - align-self:end; + align-self: center; } .ui-settings { @@ -2640,7 +2659,7 @@ select option:not(:checked) { #instruct_enabled_label .menu_button:not(.toggleEnabled), #sysprompt_enabled_label .menu_button:not(.toggleEnabled) { - color: Red; + color: Red; } .displayBlock { diff --git a/src/endpoints/stable-diffusion.js b/src/endpoints/stable-diffusion.js index 9eff948f19..b6348b8b2f 100644 --- a/src/endpoints/stable-diffusion.js +++ b/src/endpoints/stable-diffusion.js @@ -9,41 +9,6 @@ const { jsonParser } = require('../express-common'); const { readSecret, SECRET_KEYS } = require('./secrets.js'); const FormData = require('form-data'); -/** - * Sanitizes a string. - * @param {string} x String to sanitize - * @returns {string} Sanitized string - */ -function safeStr(x) { - x = String(x); - x = x.replace(/ +/g, ' '); - x = x.trim(); - x = x.replace(/^[\s,.]+|[\s,.]+$/g, ''); - return x; -} - -const splitStrings = [ - ', extremely', - ', intricate,', -]; - -const dangerousPatterns = '[]【】()īŧˆīŧ‰|:īŧš'; - -/** - * Removes patterns from a string. - * @param {string} x String to sanitize - * @param {string} pattern Pattern to remove - * @returns {string} Sanitized string - */ -function removePattern(x, pattern) { - for (let i = 0; i < pattern.length; i++) { - let p = pattern[i]; - let regex = new RegExp('\\' + p, 'g'); - x = x.replace(regex, ''); - } - return x; -} - /** * Gets the comfy workflows. * @param {import('../users.js').UserDirectoryList} directories @@ -391,40 +356,6 @@ router.post('/sd-next/upscalers', jsonParser, async (request, response) => { } }); -/** - * SD prompt expansion using GPT-2 text generation model. - * Adapted from: https://github.com/lllyasviel/Fooocus/blob/main/modules/expansion.py - */ -router.post('/expand', jsonParser, async (request, response) => { - const originalPrompt = request.body.prompt; - - if (!originalPrompt) { - console.warn('No prompt provided for SD expansion.'); - return response.send({ prompt: '' }); - } - - console.log('Refine prompt input:', originalPrompt); - const splitString = splitStrings[Math.floor(Math.random() * splitStrings.length)]; - let prompt = safeStr(originalPrompt) + splitString; - - try { - const task = 'text-generation'; - const module = await import('../transformers.mjs'); - const pipe = await module.default.getPipeline(task); - - const result = await pipe(prompt, { num_beams: 1, max_new_tokens: 256, do_sample: true }); - - const newText = result[0].generated_text; - const newPrompt = safeStr(removePattern(newText, dangerousPatterns)); - console.log('Refine prompt output:', newPrompt); - - return response.send({ prompt: newPrompt }); - } catch { - console.warn('Failed to load transformers.js pipeline.'); - return response.send({ prompt: originalPrompt }); - } -}); - const comfy = express.Router(); comfy.post('/ping', jsonParser, async (request, response) => { diff --git a/src/transformers.mjs b/src/transformers.mjs index fe947123a5..09714e3bb1 100644 --- a/src/transformers.mjs +++ b/src/transformers.mjs @@ -31,12 +31,6 @@ const tasks = { configField: 'extras.embeddingModel', quantized: true, }, - 'text-generation': { - defaultModel: 'Cohee/fooocus_expansion-onnx', - pipeline: null, - configField: 'extras.promptExpansionModel', - quantized: false, - }, 'automatic-speech-recognition': { defaultModel: 'Xenova/whisper-small', pipeline: null,