From 8dab4ecb06ab2e5c1d16a479db4bbca4445db6d2 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 25 Jun 2024 21:53:10 +0300 Subject: [PATCH 001/181] Add simple custom macros registration --- public/script.js | 3 ++- public/scripts/macros.js | 58 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/public/script.js b/public/script.js index 93b1bbc01..70eb2726e 100644 --- a/public/script.js +++ b/public/script.js @@ -226,7 +226,7 @@ import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay import { loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels } from './scripts/textgen-models.js'; import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId } from './scripts/chats.js'; import { initPresetManager } from './scripts/preset-manager.js'; -import { evaluateMacros } from './scripts/macros.js'; +import { MacrosParser, evaluateMacros } from './scripts/macros.js'; import { currentUser, setUserControls } from './scripts/user.js'; import { POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js'; import { renderTemplate, renderTemplateAsync } from './scripts/templates.js'; @@ -7763,6 +7763,7 @@ window['SillyTavern'].getContext = function () { * @deprecated Handlebars for extensions are no longer supported. */ registerHelper: () => { }, + registerMacro: MacrosParser.registerMacro.bind(MacrosParser), registedDebugFunction: registerDebugFunction, /** * @deprecated Use renderExtensionTemplateAsync instead. diff --git a/public/scripts/macros.js b/public/scripts/macros.js index 2b4ea17a7..23722aaa2 100644 --- a/public/scripts/macros.js +++ b/public/scripts/macros.js @@ -1,5 +1,5 @@ import { chat, chat_metadata, main_api, getMaxContextSize, getCurrentChatId, substituteParams } from '../script.js'; -import { timestampToMoment, isDigitsOnly, getStringHash } from './utils.js'; +import { timestampToMoment, isDigitsOnly, getStringHash, escapeRegex } from './utils.js'; import { textgenerationwebui_banned_in_macros } from './textgen-settings.js'; import { replaceInstructMacros } from './instruct-mode.js'; import { replaceVariableMacros } from './variables.js'; @@ -13,6 +13,56 @@ Handlebars.registerHelper('helperMissing', function () { return substituteParams(`{{${macroName}}}`); }); +export class MacrosParser { + /** + * A map of registered macros. + * @type {Map string)>} + */ + static #macros = new Map(); + + /** + * Registers a global macro that can be used anywhere where substitution is allowed. + * @param {string} key Macro name (key) + * @param {string|(() => string)} value A string or a function that returns a string + */ + static registerMacro(key, value) { + if (typeof key !== 'string') { + throw new Error('Macro key must be a string'); + } + + if (this.#macros.has(key)) { + console.warn(`Macro ${key} is already registered`); + } + + if (typeof value !== 'string' && typeof value !== 'function') { + throw new Error('Macro value must be a string or a function that returns a string'); + } + + this.#macros.set(key, value); + } + + /** + * Populate the env object with macro values from the current context. + * @param {Object} env Env object for the current evaluation context + * @returns {void} + */ + static populateEnv(env) { + if (!env || typeof env !== 'object') { + console.warn('Env object is not provided'); + return; + } + + // No macros are registered + if (this.#macros.size === 0) { + return; + } + + for (const [key, value] of this.#macros) { + env[key] = value; + } + } +} + /** * Gets a hashed id of the current chat from the metadata. * If no metadata exists, creates a new hash and saves it. @@ -315,12 +365,16 @@ export function evaluateMacros(content, env) { content = content.replace(/{{noop}}/gi, ''); content = content.replace(/{{input}}/gi, () => String($('#send_textarea').val())); + // Add all registered macros to the env object + MacrosParser.populateEnv(env); + // Substitute passed-in variables for (const varName in env) { if (!Object.hasOwn(env, varName)) continue; const param = env[varName]; - content = content.replace(new RegExp(`{{${varName}}}`, 'gi'), param); + const value = typeof param === 'function' ? param() : param; + content = content.replace(new RegExp(`{{${escapeRegex(varName)}}}`, 'gi'), value); } content = content.replace(/{{maxPrompt}}/gi, () => String(getMaxContextSize())); From 01d38f9218d3ed52aac21f107ee8993d847346aa Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 25 Jun 2024 22:02:05 +0300 Subject: [PATCH 002/181] Additional validation of custom macro keys --- public/scripts/macros.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/public/scripts/macros.js b/public/scripts/macros.js index 23722aaa2..ef6ffec71 100644 --- a/public/scripts/macros.js +++ b/public/scripts/macros.js @@ -30,14 +30,25 @@ export class MacrosParser { throw new Error('Macro key must be a string'); } - if (this.#macros.has(key)) { - console.warn(`Macro ${key} is already registered`); + // Allowing surrounding whitespace would just create more confusion... + key = key.trim(); + + if (!key) { + throw new Error('Macro key must not be empty or whitespace only'); + } + + if (key.startsWith('{{') || key.endsWith('}}')) { + throw new Error('Macro key must not include the surrounding braces'); } if (typeof value !== 'string' && typeof value !== 'function') { throw new Error('Macro value must be a string or a function that returns a string'); } + if (this.#macros.has(key)) { + console.warn(`Macro ${key} is already registered`); + } + this.#macros.set(key, value); } From 24ae2b6fa63af38fdedffa77b04a5a68dc7ce0f5 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 25 Jun 2024 22:15:40 +0300 Subject: [PATCH 003/181] Add sanitation of macro values --- public/scripts/macros.js | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/public/scripts/macros.js b/public/scripts/macros.js index ef6ffec71..c3c35232e 100644 --- a/public/scripts/macros.js +++ b/public/scripts/macros.js @@ -42,7 +42,8 @@ export class MacrosParser { } if (typeof value !== 'string' && typeof value !== 'function') { - throw new Error('Macro value must be a string or a function that returns a string'); + console.warn(`Macro value for "${key}" will be converted to a string`); + value = this.sanitizeMacroValue(value); } if (this.#macros.has(key)) { @@ -72,6 +73,27 @@ export class MacrosParser { env[key] = value; } } + + /** + * Performs a type-check on the macro value and returns a sanitized version of it. + * @param {any} value Value returned by a macro + * @returns {string} Sanitized value + */ + static sanitizeMacroValue(value) { + if (typeof value === 'string') { + return value; + } + + if (value === null || value === undefined) { + return ''; + } + + if (typeof value === 'object') { + return JSON.stringify(value); + } + + return String(value); + } } /** @@ -384,7 +406,7 @@ export function evaluateMacros(content, env) { if (!Object.hasOwn(env, varName)) continue; const param = env[varName]; - const value = typeof param === 'function' ? param() : param; + const value = MacrosParser.sanitizeMacroValue(typeof param === 'function' ? param() : param); content = content.replace(new RegExp(`{{${escapeRegex(varName)}}}`, 'gi'), value); } From a030237641e547beb5e84d560b396137b6450a7d Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 25 Jun 2024 22:44:00 +0300 Subject: [PATCH 004/181] Evaluate macro functions for every instance --- public/scripts/macros.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/public/scripts/macros.js b/public/scripts/macros.js index c3c35232e..0465eda2c 100644 --- a/public/scripts/macros.js +++ b/public/scripts/macros.js @@ -1,5 +1,5 @@ import { chat, chat_metadata, main_api, getMaxContextSize, getCurrentChatId, substituteParams } from '../script.js'; -import { timestampToMoment, isDigitsOnly, getStringHash, escapeRegex } from './utils.js'; +import { timestampToMoment, isDigitsOnly, getStringHash, escapeRegex, uuidv4 } from './utils.js'; import { textgenerationwebui_banned_in_macros } from './textgen-settings.js'; import { replaceInstructMacros } from './instruct-mode.js'; import { replaceVariableMacros } from './variables.js'; @@ -16,14 +16,14 @@ Handlebars.registerHelper('helperMissing', function () { export class MacrosParser { /** * A map of registered macros. - * @type {Map string)>} + * @type {Map string)>} */ static #macros = new Map(); /** * Registers a global macro that can be used anywhere where substitution is allowed. * @param {string} key Macro name (key) - * @param {string|(() => string)} value A string or a function that returns a string + * @param {string|((nonce: string) => string)} value A string or a function that returns a string */ static registerMacro(key, value) { if (typeof key !== 'string') { @@ -399,15 +399,18 @@ export function evaluateMacros(content, env) { content = content.replace(/{{input}}/gi, () => String($('#send_textarea').val())); // Add all registered macros to the env object + const nonce = uuidv4(); MacrosParser.populateEnv(env); // Substitute passed-in variables for (const varName in env) { if (!Object.hasOwn(env, varName)) continue; - const param = env[varName]; - const value = MacrosParser.sanitizeMacroValue(typeof param === 'function' ? param() : param); - content = content.replace(new RegExp(`{{${escapeRegex(varName)}}}`, 'gi'), value); + content = content.replace(new RegExp(`{{${escapeRegex(varName)}}}`, 'gi'), () => { + const param = env[varName]; + const value = MacrosParser.sanitizeMacroValue(typeof param === 'function' ? param(nonce) : param); + return value; + }); } content = content.replace(/{{maxPrompt}}/gi, () => String(getMaxContextSize())); From 112e26a0ffb1a7a331e43a7f5a2f962883524542 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 26 Jun 2024 19:48:45 +0300 Subject: [PATCH 005/181] Model icon for slash command messages --- public/img/manual.svg | 6 ++++++ public/scripts/slash-commands.js | 9 +++++++++ 2 files changed, 15 insertions(+) create mode 100644 public/img/manual.svg diff --git a/public/img/manual.svg b/public/img/manual.svg new file mode 100644 index 000000000..e313ab59c --- /dev/null +++ b/public/img/manual.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 8f3a1baa7..5b2dd0739 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -2703,6 +2703,8 @@ export async function sendMessageAs(args, text) { bias: bias.trim().length ? bias : null, gen_id: Date.now(), isSmallSys: compact, + api: 'manual', + model: 'slash command', }, }; @@ -2715,6 +2717,7 @@ export async function sendMessageAs(args, text) { extra: { bias: message.extra.bias, gen_id: message.extra.gen_id, + isSmallSys: compact, api: 'manual', model: 'slash command', }, @@ -2762,6 +2765,8 @@ export async function sendNarratorMessage(args, text) { bias: bias.trim().length ? bias : null, gen_id: Date.now(), isSmallSys: compact, + api: 'manual', + model: 'slash command', }, }; @@ -2812,6 +2817,8 @@ export async function promptQuietForLoudResponse(who, text) { extra: { type: system_message_types.COMMENT, gen_id: Date.now(), + api: 'manual', + model: 'slash command', }, }; @@ -2840,6 +2847,8 @@ async function sendCommentMessage(args, text) { type: system_message_types.COMMENT, gen_id: Date.now(), isSmallSys: compact, + api: 'manual', + model: 'slash command', }, }; From d9536ae3a8c194638b82f395e6dd824bb56f9bf3 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 26 Jun 2024 20:18:05 +0300 Subject: [PATCH 006/181] Add tri-state argument for /lock command --- public/scripts/personas.js | 41 +++++++++++++++++++++++++++++--- public/scripts/slash-commands.js | 32 +++++++++++++++++++++---- 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/public/scripts/personas.js b/public/scripts/personas.js index 0ea2d5486..3ed28b95e 100644 --- a/public/scripts/personas.js +++ b/public/scripts/personas.js @@ -590,7 +590,37 @@ function selectCurrentPersona() { } } -async function lockUserNameToChat() { +/** + * Checks if the persona is locked for the current chat. + * @returns {boolean} Whether the persona is locked + */ +function isPersonaLocked() { + return !!chat_metadata['persona']; +} + +/** + * Locks or unlocks the persona for the current chat. + * @param {boolean} state Desired lock state + * @returns {Promise} + */ +export async function setPersonaLockState(state) { + return state ? await lockPersona() : await unlockPersona(); +} + +/** + * Toggle the persona lock state for the current chat. + * @returns {Promise} + */ +export async function togglePersonaLock() { + return isPersonaLocked() + ? await unlockPersona() + : await lockPersona(); +} + +/** + * Unlock the persona for the current chat. + */ +async function unlockPersona() { if (chat_metadata['persona']) { console.log(`Unlocking persona for this chat ${chat_metadata['persona']}`); delete chat_metadata['persona']; @@ -599,9 +629,13 @@ async function lockUserNameToChat() { toastr.info('User persona is now unlocked for this chat. Click the "Lock" again to revert.', 'Persona unlocked'); } updateUserLockIcon(); - return; } +} +/** + * Lock the persona for the current chat. + */ +async function lockPersona() { if (!(user_avatar in power_user.personas)) { console.log(`Creating a new persona ${user_avatar}`); if (power_user.persona_show_notifications) { @@ -625,6 +659,7 @@ async function lockUserNameToChat() { updateUserLockIcon(); } + async function deleteUserAvatar(e) { e?.stopPropagation(); const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile'); @@ -973,7 +1008,7 @@ export function initPersonas() { $(document).on('click', '.bind_user_name', bindUserNameToPersona); $(document).on('click', '.set_default_persona', setDefaultPersona); $(document).on('click', '.delete_avatar', deleteUserAvatar); - $('#lock_user_name').on('click', lockUserNameToChat); + $('#lock_user_name').on('click', togglePersonaLock); $('#create_dummy_persona').on('click', createDummyPersona); $('#persona_description').on('input', onPersonaDescriptionInput); $('#persona_description_position').on('input', onPersonaDescriptionPositionInput); diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 5b2dd0739..4c87c469e 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -46,7 +46,7 @@ import { extension_settings, getContext, saveMetadataDebounced } from './extensi import { getRegexedString, regex_placement } from './extensions/regex/engine.js'; import { findGroupMemberId, getGroupMembers, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group } from './group-chats.js'; import { chat_completion_sources, oai_settings, setupChatCompletionPromptManager } from './openai.js'; -import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, user_avatar } from './personas.js'; +import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockState, togglePersonaLock, user_avatar } from './personas.js'; import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js'; import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js'; import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync } from './tokenizers.js'; @@ -118,9 +118,18 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'lock', - callback: bindCallback, + callback: lockPersonaCallback, aliases: ['bind'], helpString: 'Locks/unlocks a persona (name and avatar) to the current chat', + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'state', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + defaultValue: 'toggle', + enumProvider: commonEnumProviders.boolean('onOffToggle'), + }), + ], })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'bg', @@ -2576,8 +2585,23 @@ function syncCallback() { return ''; } -function bindCallback() { - $('#lock_user_name').trigger('click'); +async function lockPersonaCallback(_args, value) { + if (['toggle', 't', ''].includes(value.trim().toLowerCase())) { + await togglePersonaLock(); + return ''; + } + + if (isTrueBoolean(value)) { + await setPersonaLockState(true); + return ''; + } + + if (isFalseBoolean(value)) { + await setPersonaLockState(false); + return ''; + + } + return ''; } From b1fa4d3038fca5b0a2309068b4cc2aa1e36510d7 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 26 Jun 2024 20:37:29 +0300 Subject: [PATCH 007/181] Remove font-family we don't vendor --- public/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/style.css b/public/style.css index b097aac3c..08a87f25f 100644 --- a/public/style.css +++ b/public/style.css @@ -82,7 +82,7 @@ /*base variable calculated in rems*/ --fontScale: 1; --mainFontSize: calc(var(--fontScale) * 15px); - --mainFontFamily: "Noto Sans", "Noto Color Emoji", sans-serif; + --mainFontFamily: "Noto Sans", sans-serif; --monoFontFamily: 'Noto Sans Mono', 'Courier New', Consolas, monospace; /* base variable for blur strength slider calculations */ From 8b1492a2d94fdf58a4928f4295b789b28ee853a5 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 26 Jun 2024 20:39:39 +0300 Subject: [PATCH 008/181] Mono font family for kbd --- public/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/style.css b/public/style.css index 08a87f25f..a93b5160b 100644 --- a/public/style.css +++ b/public/style.css @@ -458,7 +458,7 @@ code { kbd { display: inline-block; padding: 2px 4px; - font-family: Consolas, monospace; + font-family: var(--monoFontFamily); white-space: nowrap; /* background-color: #eeeeee; */ background-color: rgba(255, 255, 255, 0.9); From 719539c2abfd7e14fb338b8d32e627fd2d7170f6 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 26 Jun 2024 21:58:57 +0300 Subject: [PATCH 009/181] Improve types and sanitation of macro values --- public/scripts/macros.js | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/public/scripts/macros.js b/public/scripts/macros.js index 0465eda2c..992150894 100644 --- a/public/scripts/macros.js +++ b/public/scripts/macros.js @@ -13,17 +13,22 @@ Handlebars.registerHelper('helperMissing', function () { return substituteParams(`{{${macroName}}}`); }); +/** + * @typedef {Object} EnvObject + * @typedef {(nonce: string) => string} MacroFunction + */ + export class MacrosParser { /** * A map of registered macros. - * @type {Map string)>} + * @type {Map} */ static #macros = new Map(); /** * Registers a global macro that can be used anywhere where substitution is allowed. * @param {string} key Macro name (key) - * @param {string|((nonce: string) => string)} value A string or a function that returns a string + * @param {string|MacroFunction} value A string or a function that returns a string */ static registerMacro(key, value) { if (typeof key !== 'string') { @@ -55,7 +60,7 @@ export class MacrosParser { /** * Populate the env object with macro values from the current context. - * @param {Object} env Env object for the current evaluation context + * @param {EnvObject} env Env object for the current evaluation context * @returns {void} */ static populateEnv(env) { @@ -88,6 +93,20 @@ export class MacrosParser { return ''; } + if (value instanceof Promise) { + console.warn('Promises are not supported as macro values'); + return ''; + } + + if (typeof value === 'function') { + console.warn('Functions are not supported as macro values'); + return ''; + } + + if (value instanceof Date) { + return value.toISOString(); + } + if (typeof value === 'object') { return JSON.stringify(value); } @@ -367,7 +386,7 @@ function timeDiffReplace(input) { /** * Substitutes {{macro}} parameters in a string. * @param {string} content - The string to substitute parameters in. - * @param {Object} env - Map of macro names to the values they'll be substituted with. If the param + * @param {EnvObject} env - Map of macro names to the values they'll be substituted with. If the param * values are functions, those functions will be called and their return values are used. * @returns {string} The string with substituted parameters. */ From 54fb7a9030c3485b80e27453084a5303c55ce3d2 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 26 Jun 2024 22:11:22 +0300 Subject: [PATCH 010/181] Add 'online_status_changed' event --- public/script.js | 51 ++++++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/public/script.js b/public/script.js index 6f2d55236..6e303a2f7 100644 --- a/public/script.js +++ b/public/script.js @@ -454,6 +454,7 @@ export const event_types = { OPEN_CHARACTER_LIBRARY: 'open_character_library', LLM_FUNCTION_TOOL_REGISTER: 'llm_function_tool_register', LLM_FUNCTION_TOOL_CALL: 'llm_function_tool_call', + ONLINE_STATUS_CHANGED: 'online_status_changed', }; export const eventSource = new EventEmitter(); @@ -1050,10 +1051,10 @@ export async function clearItemizedPrompts() { async function getStatusHorde() { try { const hordeStatus = await checkHordeStatus(); - online_status = hordeStatus ? 'Connected' : 'no_connection'; + setOnlineStatus(hordeStatus ? 'Connected' : 'no_connection'); } catch { - online_status = 'no_connection'; + setOnlineStatus('no_connection'); } return resultCheckStatus(); @@ -1064,7 +1065,7 @@ async function getStatusKobold() { if (!endpoint) { console.warn('No endpoint for status check'); - online_status = 'no_connection'; + setOnlineStatus('no_connection'); return resultCheckStatus(); } @@ -1081,7 +1082,7 @@ async function getStatusKobold() { const data = await response.json(); - online_status = data?.model ?? 'no_connection'; + setOnlineStatus(data?.model ?? 'no_connection'); if (!data.koboldUnitedVersion) { throw new Error('Missing mandatory Kobold version in data:', data); @@ -1099,7 +1100,7 @@ async function getStatusKobold() { } } catch (err) { console.error('Error getting status', err); - online_status = 'no_connection'; + setOnlineStatus('no_connection'); } return resultCheckStatus(); @@ -1112,12 +1113,12 @@ async function getStatusTextgen() { if (!endpoint) { console.warn('No endpoint for status check'); - online_status = 'no_connection'; + setOnlineStatus('no_connection'); return resultCheckStatus(); } if (textgen_settings.type == OOBA && textgen_settings.bypass_status_check) { - online_status = 'Status check bypassed'; + setOnlineStatus('Status check bypassed'); return resultCheckStatus(); } @@ -1137,34 +1138,34 @@ async function getStatusTextgen() { if (textgen_settings.type === MANCER) { loadMancerModels(data?.data); - online_status = textgen_settings.mancer_model; + setOnlineStatus(textgen_settings.mancer_model); } else if (textgen_settings.type === TOGETHERAI) { loadTogetherAIModels(data?.data); - online_status = textgen_settings.togetherai_model; + setOnlineStatus(textgen_settings.togetherai_model); } else if (textgen_settings.type === OLLAMA) { loadOllamaModels(data?.data); - online_status = textgen_settings.ollama_model || 'Connected'; + setOnlineStatus(textgen_settings.ollama_model || 'Connected'); } else if (textgen_settings.type === INFERMATICAI) { loadInfermaticAIModels(data?.data); - online_status = textgen_settings.infermaticai_model; + setOnlineStatus(textgen_settings.infermaticai_model); } else if (textgen_settings.type === DREAMGEN) { loadDreamGenModels(data?.data); - online_status = textgen_settings.dreamgen_model; + setOnlineStatus(textgen_settings.dreamgen_model); } else if (textgen_settings.type === OPENROUTER) { loadOpenRouterModels(data?.data); - online_status = textgen_settings.openrouter_model; + setOnlineStatus(textgen_settings.openrouter_model); } else if (textgen_settings.type === VLLM) { loadVllmModels(data?.data); - online_status = textgen_settings.vllm_model; + setOnlineStatus(textgen_settings.vllm_model); } else if (textgen_settings.type === APHRODITE) { loadAphroditeModels(data?.data); - online_status = textgen_settings.aphrodite_model; + setOnlineStatus(textgen_settings.aphrodite_model); } else { - online_status = data?.result; + setOnlineStatus(data?.result); } if (!online_status) { - online_status = 'no_connection'; + setOnlineStatus('no_connection'); } // Determine instruct mode preset @@ -1176,7 +1177,7 @@ async function getStatusTextgen() { } } catch (err) { console.error('Error getting status', err); - online_status = 'no_connection'; + setOnlineStatus('no_connection'); } return resultCheckStatus(); @@ -1190,9 +1191,9 @@ async function getStatusNovel() { throw new Error('Could not load subscription data'); } - online_status = getNovelTier(); + setOnlineStatus(getNovelTier()); } catch { - online_status = 'no_connection'; + setOnlineStatus('no_connection'); } resultCheckStatus(); @@ -5526,9 +5527,17 @@ export function setCharacterName(value) { name2 = value; } +/** + * Sets the API connection status of the application + * @param {string|'no_connection'} value Connection status value + */ export function setOnlineStatus(value) { + const previousStatus = online_status; online_status = value; displayOnlineStatus(); + if (previousStatus !== online_status) { + eventSource.emitAndWait(event_types.ONLINE_STATUS_CHANGED, online_status); + } } export function setEditedMessageId(value) { @@ -6096,7 +6105,7 @@ export function changeMainAPI() { } main_api = selectedVal; - online_status = 'no_connection'; + setOnlineStatus('no_connection'); if (main_api == 'openai' && oai_settings.chat_completion_source == chat_completion_sources.WINDOWAI) { $('#api_button_openai').trigger('click'); From 886b6fee64d1aaa641435dc70401181bdb36c532 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 26 Jun 2024 22:43:30 +0300 Subject: [PATCH 011/181] Add WI entry delay --- public/index.html | 17 +++++++++-- public/scripts/world-info.js | 56 +++++++++++++++++++++++++++++++++--- src/endpoints/characters.js | 1 + 3 files changed, 68 insertions(+), 6 deletions(-) diff --git a/public/index.html b/public/index.html index 3b619ccaf..d815e2300 100644 --- a/public/index.html +++ b/public/index.html @@ -5425,7 +5425,7 @@
-
+
Inclusion Group @@ -5447,7 +5447,7 @@
-
+
Group Weight @@ -5483,6 +5483,19 @@
+
+
+ + + Delay + + + +
+
+ +
+
diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index 23db6945e..507395e4c 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -99,6 +99,7 @@ const MAX_SCAN_DEPTH = 1000; * @property {number} [selectiveLogic] The logic to use for selective activation * @property {number} [sticky] The sticky value of the entry * @property {number} [cooldown] The cooldown of the entry + * @property {number} [delay] The delay of the entry */ /** @@ -111,7 +112,7 @@ const MAX_SCAN_DEPTH = 1000; /** * @typedef TimedEffectType Type of timed effect - * @type {'sticky'|'cooldown'} + * @type {'sticky'|'cooldown'|'delay'} */ // End typedef area @@ -371,6 +372,7 @@ class WorldInfoTimedEffects { #buffer = { 'sticky': [], 'cooldown': [], + 'delay': [], }; /** @@ -404,6 +406,8 @@ class WorldInfoTimedEffects { 'cooldown': (entry) => { console.debug('Cooldown ended for entry', entry.uid); }, + + 'delay': () => {}, }; /** @@ -529,12 +533,31 @@ class WorldInfoTimedEffects { } } + /** + * Processes entries for the "delay" timed effect. + * @param {WIScanEntry[]} buffer Buffer to store the entries + */ + #checkDelayEffect(buffer) { + for (const entry of this.#entries) { + if (!entry.delay) { + continue; + } + + if (this.#chat.length < entry.delay) { + buffer.push(entry); + console.log('Timed effect "delay" applied to entry', entry); + } + } + + } + /** * Checks for timed effects on chat messages. */ checkTimedEffects() { this.#checkTimedEffectOfType('sticky', this.#buffer.sticky, this.#onEnded.sticky.bind(this)); this.#checkTimedEffectOfType('cooldown', this.#buffer.cooldown, this.#onEnded.cooldown.bind(this)); + this.#checkDelayEffect(this.#buffer.delay); } /** @@ -611,7 +634,7 @@ class WorldInfoTimedEffects { * @returns {boolean} Is recognized type */ isValidEffectType(type) { - return typeof type === 'string' && ['sticky', 'cooldown'].includes(type.trim().toLowerCase()); + return typeof type === 'string' && ['sticky', 'cooldown', 'delay'].includes(type.trim().toLowerCase()); } /** @@ -1685,7 +1708,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl // Regardless of whether success is displayed or not. Make sure the delete button is available. // Do not put this code behind. $('#world_popup_delete').off('click').on('click', async () => { - const confirmation = await Popup.show.confirm(`Delete the World/Lorebook: "${name}"?`, `This action is irreversible!`); + const confirmation = await Popup.show.confirm(`Delete the World/Lorebook: "${name}"?`, 'This action is irreversible!'); if (!confirmation) { return; } @@ -1941,6 +1964,7 @@ const originalDataKeyMap = { 'groupWeight': 'extensions.group_weight', 'sticky': 'extensions.sticky', 'cooldown': 'extensions.cooldown', + 'delay': 'extensions.delay', }; /** Checks the state of the current search, and adds/removes the search sorting option accordingly */ @@ -2589,6 +2613,19 @@ function getWorldEntry(name, data, entry) { }); cooldown.val(entry.cooldown > 0 ? entry.cooldown : '').trigger('input'); + // delay + const delay = template.find('input[name="delay"]'); + delay.data('uid', entry.uid); + delay.on('input', function () { + const uid = $(this).data('uid'); + const value = Number($(this).val()); + data.entries[uid].delay = !isNaN(value) ? value : null; + + setOriginalDataValue(data, uid, 'extensions.delay', data.entries[uid].delay); + saveWorldInfo(name, data); + }); + delay.val(entry.delay > 0 ? entry.delay : '').trigger('input'); + // probability if (entry.probability === undefined) { entry.probability = null; @@ -3139,6 +3176,7 @@ const newEntryDefinition = { role: { default: 0, type: 'enum' }, sticky: { default: null, type: 'number?' }, cooldown: { default: null, type: 'number?' }, + delay: { default: null, type: 'number?' }, }; const newEntryTemplate = Object.fromEntries( @@ -3533,9 +3571,15 @@ async function checkWorldInfo(chat, maxContext, isDryRun) { const isSticky = timedEffects.isEffectActive('sticky', entry); const isCooldown = timedEffects.isEffectActive('cooldown', entry); + const isDelay = timedEffects.isEffectActive('delay', entry); + + if (isDelay) { + console.debug(`WI entry ${entry.uid} suppressed by delay`, entry); + continue; + } if (isCooldown && !isSticky) { - console.debug(`WI entry ${entry.uid} suppressed by cooldown`); + console.debug(`WI entry ${entry.uid} suppressed by cooldown`, entry); continue; } @@ -3933,6 +3977,7 @@ function convertAgnaiMemoryBook(inputObj) { role: extension_prompt_roles.SYSTEM, sticky: null, cooldown: null, + delay: null, }; }); @@ -3974,6 +4019,7 @@ function convertRisuLorebook(inputObj) { role: extension_prompt_roles.SYSTEM, sticky: null, cooldown: null, + delay: null, }; }); @@ -4020,6 +4066,7 @@ function convertNovelLorebook(inputObj) { role: extension_prompt_roles.SYSTEM, sticky: null, cooldown: null, + delay: null, }; }); @@ -4068,6 +4115,7 @@ function convertCharacterBook(characterBook) { vectorized: entry.extensions?.vectorized ?? false, sticky: entry.extensions?.sticky ?? null, cooldown: entry.extensions?.cooldown ?? null, + delay: entry.extensions?.delay ?? null, }; }); diff --git a/src/endpoints/characters.js b/src/endpoints/characters.js index 17dcaa6b8..59bdd41df 100644 --- a/src/endpoints/characters.js +++ b/src/endpoints/characters.js @@ -485,6 +485,7 @@ function convertWorldInfoToCharacterBook(name, entries) { vectorized: entry.vectorized ?? false, sticky: entry.sticky ?? null, cooldown: entry.cooldown ?? null, + delay: entry.delay ?? null, }, }; From 5b002c6e46dcc638d2c2b86d394952d3dfc9da04 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 26 Jun 2024 23:22:42 +0300 Subject: [PATCH 012/181] #2422 Move uploads under the data root --- .dockerignore | 2 ++ .npmignore | 2 ++ server.js | 10 +++++----- src/constants.js | 7 +++++-- src/endpoints/avatars.js | 4 ++-- src/endpoints/backgrounds.js | 3 +-- src/endpoints/characters.js | 8 ++++---- src/endpoints/chats.js | 7 +++---- src/endpoints/sprites.js | 5 ++--- src/endpoints/worldinfo.js | 3 +-- 10 files changed, 27 insertions(+), 24 deletions(-) diff --git a/.dockerignore b/.dockerignore index e4995fe58..9f6dc7953 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,3 +8,5 @@ Start.bat cloudflared.exe access.log /data +/cache +.DS_Store diff --git a/.npmignore b/.npmignore index 10082773f..ccee41fb6 100644 --- a/.npmignore +++ b/.npmignore @@ -5,4 +5,6 @@ node_modules/ secrets.json /dist /backups/ +/data +/cache access.log diff --git a/server.js b/server.js index ab0460ac0..9f1bd81b4 100644 --- a/server.js +++ b/server.js @@ -136,7 +136,7 @@ const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProte const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH); const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS); -const { UPLOADS_PATH } = require('./src/constants'); +const uploadsPath = path.join(dataRoot, require('./src/constants').UPLOADS_DIRECTORY); // CORS Settings // const CORS = cors({ @@ -286,7 +286,7 @@ app.use(userModule.requireLoginMiddleware); app.get('/api/ping', (_, response) => response.sendStatus(204)); // File uploads -app.use(multer({ dest: UPLOADS_PATH, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar')); +app.use(multer({ dest: uploadsPath, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar')); app.use(require('./src/middleware/multerMonkeyPatch')); // User data mount @@ -303,8 +303,8 @@ app.get('/version', async function (_, response) { function cleanUploads() { try { - if (fs.existsSync(UPLOADS_PATH)) { - const uploads = fs.readdirSync(UPLOADS_PATH); + if (fs.existsSync(uploadsPath)) { + const uploads = fs.readdirSync(uploadsPath); if (!uploads.length) { return; @@ -312,7 +312,7 @@ function cleanUploads() { console.debug(`Cleaning uploads folder (${uploads.length} files)`); uploads.forEach(file => { - const pathToFile = path.join(UPLOADS_PATH, file); + const pathToFile = path.join(uploadsPath, file); fs.unlinkSync(pathToFile); }); } diff --git a/src/constants.js b/src/constants.js index bb992530b..89af4f739 100644 --- a/src/constants.js +++ b/src/constants.js @@ -196,7 +196,10 @@ const CHAT_COMPLETION_SOURCES = { GROQ: 'groq', }; -const UPLOADS_PATH = './uploads'; +/** + * Path to multer file uploads under the data root. + */ +const UPLOADS_DIRECTORY = '_uploads'; // TODO: this is copied from the client code; there should be a way to de-duplicate it eventually const TEXTGEN_TYPES = { @@ -364,7 +367,7 @@ module.exports = { PUBLIC_DIRECTORIES, USER_DIRECTORY_TEMPLATE, UNSAFE_EXTENSIONS, - UPLOADS_PATH, + UPLOADS_DIRECTORY, GEMINI_SAFETY, BISON_SAFETY, TEXTGEN_TYPES, diff --git a/src/endpoints/avatars.js b/src/endpoints/avatars.js index 58571bae9..5f8509f0f 100644 --- a/src/endpoints/avatars.js +++ b/src/endpoints/avatars.js @@ -4,7 +4,7 @@ const fs = require('fs'); const sanitize = require('sanitize-filename'); const writeFileAtomicSync = require('write-file-atomic').sync; const { jsonParser, urlencodedParser } = require('../express-common'); -const { AVATAR_WIDTH, AVATAR_HEIGHT, UPLOADS_PATH } = require('../constants'); +const { AVATAR_WIDTH, AVATAR_HEIGHT } = require('../constants'); const { getImages, tryParse } = require('../util'); // image processing related library imports @@ -39,7 +39,7 @@ router.post('/upload', urlencodedParser, async (request, response) => { if (!request.file) return response.sendStatus(400); try { - const pathToUpload = path.join(UPLOADS_PATH, request.file.filename); + const pathToUpload = path.join(request.file.destination, request.file.filename); const crop = tryParse(request.query.crop); let rawImg = await jimp.read(pathToUpload); diff --git a/src/endpoints/backgrounds.js b/src/endpoints/backgrounds.js index b8965ab5f..6ef59f923 100644 --- a/src/endpoints/backgrounds.js +++ b/src/endpoints/backgrounds.js @@ -4,7 +4,6 @@ const express = require('express'); const sanitize = require('sanitize-filename'); const { jsonParser, urlencodedParser } = require('../express-common'); -const { UPLOADS_PATH } = require('../constants'); const { invalidateThumbnail } = require('./thumbnails'); const { getImages } = require('../util'); @@ -60,7 +59,7 @@ router.post('/rename', jsonParser, function (request, response) { router.post('/upload', urlencodedParser, function (request, response) { if (!request.body || !request.file) return response.sendStatus(400); - const img_path = path.join(UPLOADS_PATH, request.file.filename); + const img_path = path.join(request.file.destination, request.file.filename); const filename = request.file.originalname; try { diff --git a/src/endpoints/characters.js b/src/endpoints/characters.js index 17dcaa6b8..5b6246f52 100644 --- a/src/endpoints/characters.js +++ b/src/endpoints/characters.js @@ -11,7 +11,7 @@ const mime = require('mime-types'); const jimp = require('jimp'); -const { UPLOADS_PATH, AVATAR_WIDTH, AVATAR_HEIGHT } = require('../constants'); +const { AVATAR_WIDTH, AVATAR_HEIGHT } = require('../constants'); const { jsonParser, urlencodedParser } = require('../express-common'); const { deepMerge, humanizedISO8601DateTime, tryParse, extractFileFromZipBuffer } = require('../util'); const { TavernCardValidator } = require('../validator/TavernCardValidator'); @@ -729,7 +729,7 @@ router.post('/create', urlencodedParser, async function (request, response) { return response.send(avatarName); } else { const crop = tryParse(request.query.crop); - const uploadPath = path.join(UPLOADS_PATH, request.file.filename); + const uploadPath = path.join(request.file.destination, request.file.filename); await writeCharacterData(uploadPath, char, internalName, request, crop); fs.unlinkSync(uploadPath); return response.send(avatarName); @@ -812,7 +812,7 @@ router.post('/edit', urlencodedParser, async function (request, response) { await writeCharacterData(avatarPath, char, targetFile, request); } else { const crop = tryParse(request.query.crop); - const newAvatarPath = path.join(UPLOADS_PATH, request.file.filename); + const newAvatarPath = path.join(request.file.destination, request.file.filename); invalidateThumbnail(request.user.directories, 'avatar', request.body.avatar_url); await writeCharacterData(newAvatarPath, char, targetFile, request, crop); fs.unlinkSync(newAvatarPath); @@ -1096,7 +1096,7 @@ function getPreservedName(request) { router.post('/import', urlencodedParser, async function (request, response) { if (!request.body || !request.file) return response.sendStatus(400); - const uploadPath = path.join(UPLOADS_PATH, request.file.filename); + const uploadPath = path.join(request.file.destination, request.file.filename); const format = request.body.file_type; const preservedFileName = getPreservedName(request); diff --git a/src/endpoints/chats.js b/src/endpoints/chats.js index d88b27fa3..b6f675c04 100644 --- a/src/endpoints/chats.js +++ b/src/endpoints/chats.js @@ -6,7 +6,6 @@ const sanitize = require('sanitize-filename'); const writeFileAtomicSync = require('write-file-atomic').sync; const { jsonParser, urlencodedParser } = require('../express-common'); -const { UPLOADS_PATH } = require('../constants'); const { getConfigValue, humanizedISO8601DateTime, tryParse, generateTimestamp, removeOldBackups } = require('../util'); /** @@ -323,7 +322,7 @@ router.post('/group/import', urlencodedParser, function (request, response) { } const chatname = humanizedISO8601DateTime(); - const pathToUpload = path.join(UPLOADS_PATH, filedata.filename); + const pathToUpload = path.join(filedata.destination, filedata.filename); const pathToNewFile = path.join(request.user.directories.groupChats, `${chatname}.jsonl`); fs.copyFileSync(pathToUpload, pathToNewFile); fs.unlinkSync(pathToUpload); @@ -347,7 +346,7 @@ router.post('/import', urlencodedParser, function (request, response) { } try { - const data = fs.readFileSync(path.join(UPLOADS_PATH, request.file.filename), 'utf8'); + const data = fs.readFileSync(path.join(request.file.destination, request.file.filename), 'utf8'); if (format === 'json') { const jsonData = JSON.parse(data); @@ -388,7 +387,7 @@ router.post('/import', urlencodedParser, function (request, response) { if (jsonData.user_name !== undefined || jsonData.name !== undefined) { const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`; const filePath = path.join(request.user.directories.chats, avatarUrl, fileName); - fs.copyFileSync(path.join(UPLOADS_PATH, request.file.filename), filePath); + fs.copyFileSync(path.join(request.file.destination, request.file.filename), filePath); response.send({ res: true }); } else { console.log('Incorrect chat format .jsonl'); diff --git a/src/endpoints/sprites.js b/src/endpoints/sprites.js index 88a577b3b..43bfdb145 100644 --- a/src/endpoints/sprites.js +++ b/src/endpoints/sprites.js @@ -5,7 +5,6 @@ const express = require('express'); const mime = require('mime-types'); const sanitize = require('sanitize-filename'); const writeFileAtomicSync = require('write-file-atomic').sync; -const { UPLOADS_PATH } = require('../constants'); const { getImageBuffers } = require('../util'); const { jsonParser, urlencodedParser } = require('../express-common'); @@ -190,7 +189,7 @@ router.post('/upload-zip', urlencodedParser, async (request, response) => { return response.sendStatus(404); } - const spritePackPath = path.join(UPLOADS_PATH, file.filename); + const spritePackPath = path.join(file.destination, file.filename); const sprites = await getImageBuffers(spritePackPath); const files = fs.readdirSync(spritesPath); @@ -248,7 +247,7 @@ router.post('/upload', urlencodedParser, async (request, response) => { } const filename = label + path.parse(file.originalname).ext; - const spritePath = path.join(UPLOADS_PATH, file.filename); + const spritePath = path.join(file.destination, file.filename); const pathToFile = path.join(spritesPath, filename); // Copy uploaded file to sprites folder fs.cpSync(spritePath, pathToFile); diff --git a/src/endpoints/worldinfo.js b/src/endpoints/worldinfo.js index f8ab2d498..4f125bcc4 100644 --- a/src/endpoints/worldinfo.js +++ b/src/endpoints/worldinfo.js @@ -5,7 +5,6 @@ const sanitize = require('sanitize-filename'); const writeFileAtomicSync = require('write-file-atomic').sync; const { jsonParser, urlencodedParser } = require('../express-common'); -const { UPLOADS_PATH } = require('../constants'); /** * Reads a World Info file and returns its contents @@ -74,7 +73,7 @@ router.post('/import', urlencodedParser, (request, response) => { if (request.body.convertedData) { fileContents = request.body.convertedData; } else { - const pathToUpload = path.join(UPLOADS_PATH, request.file.filename); + const pathToUpload = path.join(request.file.destination, request.file.filename); fileContents = fs.readFileSync(pathToUpload, 'utf8'); fs.unlinkSync(pathToUpload); } From b80b2d9a748a435edf3d3a5732884f8a395a51ef Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 26 Jun 2024 23:25:00 +0300 Subject: [PATCH 013/181] Fix imported chats not deleting itself after upload --- src/endpoints/chats.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/endpoints/chats.js b/src/endpoints/chats.js index b6f675c04..2be1eb4e0 100644 --- a/src/endpoints/chats.js +++ b/src/endpoints/chats.js @@ -346,9 +346,11 @@ router.post('/import', urlencodedParser, function (request, response) { } try { - const data = fs.readFileSync(path.join(request.file.destination, request.file.filename), 'utf8'); + const pathToUpload = path.join(request.file.destination, request.file.filename); + const data = fs.readFileSync(pathToUpload, 'utf8'); if (format === 'json') { + fs.unlinkSync(pathToUpload); const jsonData = JSON.parse(data); if (jsonData.histories !== undefined) { // CAI Tools format @@ -387,7 +389,8 @@ router.post('/import', urlencodedParser, function (request, response) { if (jsonData.user_name !== undefined || jsonData.name !== undefined) { const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`; const filePath = path.join(request.user.directories.chats, avatarUrl, fileName); - fs.copyFileSync(path.join(request.file.destination, request.file.filename), filePath); + fs.copyFileSync(pathToUpload, filePath); + fs.unlinkSync(pathToUpload); response.send({ res: true }); } else { console.log('Incorrect chat format .jsonl'); From d64d16bdf2425991fb725c2e5ade1ce3116667b1 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Thu, 27 Jun 2024 00:27:55 +0200 Subject: [PATCH 014/181] Fix popup onClose executing after resolver --- public/scripts/popup.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/scripts/popup.js b/public/scripts/popup.js index ddd7dd5d0..c31824f0b 100644 --- a/public/scripts/popup.js +++ b/public/scripts/popup.js @@ -451,9 +451,9 @@ export class Popup { else popup.setAutoFocus(); } } - }); - this.resolver(this.value); + this.resolver(this.value); + }); } /** From 360c2985f50e21495e91dc1d2d6064a67726f32b Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Thu, 27 Jun 2024 00:29:25 +0200 Subject: [PATCH 015/181] Switch char deletion to new popup - New popup - Move char CHARACTER_DELETED to after char deleting, and inside the correct function --- public/script.js | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/public/script.js b/public/script.js index 6e303a2f7..e69466d62 100644 --- a/public/script.js +++ b/public/script.js @@ -8659,13 +8659,11 @@ function doCloseChat() { * it proceeds to delete character from UI and saves settings. * In case of error during the fetch request, it logs the error details. * - * @param {string} popup_type - The type of popup currently active. * @param {string} this_chid - The character ID to be deleted. * @param {boolean} delete_chats - Whether to delete chats or not. */ -export async function handleDeleteCharacter(popup_type, this_chid, delete_chats) { - if (popup_type !== 'del_ch' || - !characters[this_chid]) { +export async function handleDeleteCharacter(this_chid, delete_chats) { + if (!characters[this_chid]) { return; } @@ -8711,6 +8709,8 @@ export async function deleteCharacter(characterKey, { deleteChats = true } = {}) await eventSource.emit(event_types.CHAT_DELETED, name); } } + + eventSource.emit(event_types.CHARACTER_DELETED, { id: this_chid, character: characters[this_chid] }); } /** @@ -9212,11 +9212,6 @@ jQuery(async function () { }, 2000); } } - if (popup_type == 'del_ch') { - const deleteChats = !!$('#del_char_checkbox').prop('checked'); - eventSource.emit(event_types.CHARACTER_DELETED, { id: this_chid, character: characters[this_chid] }); - await handleDeleteCharacter(popup_type, this_chid, deleteChats); - } if (popup_type == 'alternate_greeting' && menu_type !== 'create') { createOrEditCharacter(); } @@ -9297,15 +9292,27 @@ jQuery(async function () { $('#form_create').submit(createOrEditCharacter); - $('#delete_button').on('click', function () { - callPopup(` -

Delete the character?

- THIS IS PERMANENT!

+ $('#delete_button').on('click', async function () { + if (!this_chid) { + toastr.warning('No character selected.'); + return; + } + + let deleteChats = false; + + const confirm = await Popup.show.confirm('Delete the character?', ` + THIS IS PERMANENT!


`, 'del_ch', '', - ); +
`, { + onClose: () => deleteChats = !!$('#del_char_checkbox').prop('checked'), + }); + if (!confirm) { + return; + } + + await deleteCharacter(characters[this_chid].avatar, { deleteChats: deleteChats }); }); //////// OPTIMIZED ALL CHAR CREATION/EDITING TEXTAREA LISTENERS /////////////// From efb9fbcc7eb44c486736ffc6be4c01f94b710403 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Thu, 27 Jun 2024 00:45:26 +0200 Subject: [PATCH 016/181] Refactor new chat to new popup --- public/script.js | 79 ++++++++++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 36 deletions(-) diff --git a/public/script.js b/public/script.js index e69466d62..507791c19 100644 --- a/public/script.js +++ b/public/script.js @@ -8551,6 +8551,39 @@ async function doImpersonate(args, prompt) { return ''; } +async function doNewChat({ deleteCurrentChat = false } = {}) { + //Make a new chat for selected character + if ((!selected_group && this_chid == undefined) || menu_type != 'create') { + return; + } + + //Fix it; New chat doesn't create while open create character menu + await clearChat(); + chat.length = 0; + + chat_file_for_del = getCurrentChatDetails()?.sessionName; + + // Make it easier to find in backups + if (deleteCurrentChat) { + await saveChatConditional(); + } + + if (selected_group) { + await createNewGroupChat(selected_group); + if (deleteCurrentChat) await deleteGroupChat(selected_group, chat_file_for_del); + } + else { + //RossAscends: added character name to new chat filenames and replaced Date.now() with humanizedDateTime; + chat_metadata = {}; + characters[this_chid].chat = `${name2} - ${humanizedDateTime()}`; + $('#selected_chat_pole').val(characters[this_chid].chat); + await getChat(); + await createOrEditCharacter(new CustomEvent('newChat')); + if (deleteCurrentChat) await delChat(chat_file_for_del + '.jsonl'); + } + +} + async function doDeleteChat() { await displayPastChats(); let currentChatDeleteButton = $('.select_chat_block[highlight=\'true\']').parent().find('.PastChat_cross'); @@ -9215,38 +9248,6 @@ jQuery(async function () { if (popup_type == 'alternate_greeting' && menu_type !== 'create') { createOrEditCharacter(); } - //Make a new chat for selected character - if ( - popup_type == 'new_chat' && - (selected_group || this_chid !== undefined) && - menu_type != 'create' - ) { - //Fix it; New chat doesn't create while open create character menu - await clearChat(); - chat.length = 0; - - chat_file_for_del = getCurrentChatDetails()?.sessionName; - const isDelChatCheckbox = document.getElementById('del_chat_checkbox')?.checked; - - // Make it easier to find in backups - if (isDelChatCheckbox) { - await saveChatConditional(); - } - - if (selected_group) { - await createNewGroupChat(selected_group); - if (isDelChatCheckbox) await deleteGroupChat(selected_group, chat_file_for_del); - } - else { - //RossAscends: added character name to new chat filenames and replaced Date.now() with humanizedDateTime; - chat_metadata = {}; - characters[this_chid].chat = `${name2} - ${humanizedDateTime()}`; - $('#selected_chat_pole').val(characters[this_chid].chat); - await getChat(); - await createOrEditCharacter(new CustomEvent('newChat')); - if (isDelChatCheckbox) await delChat(chat_file_for_del + '.jsonl'); - } - } if (dialogueResolve) { if (popup_type == 'input') { @@ -9574,14 +9575,20 @@ jQuery(async function () { else if (id == 'option_start_new_chat') { if ((selected_group || this_chid !== undefined) && !is_send_press) { - callPopup(` -

Start new chat?


+ let deleteCurrentChat = false; + const result = await Popup.show.confirm('Start new chat?', `
- `, 'new_chat', ''); + `, { + onClose: () => deleteCurrentChat = !!$('#del_chat_checkbox').prop('checked'), + }); + if (!result) { + return; + } + + await doNewChat({ deleteCurrentChat: deleteCurrentChat }); } } From 1c6c9efba18182f297b6755c17e27717ed05f8a8 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Thu, 27 Jun 2024 01:01:43 +0200 Subject: [PATCH 017/181] Refactor convert to group chat to new popup --- public/script.js | 2 +- public/scripts/bookmarks.js | 4 ++-- public/scripts/popup.js | 16 ++++++++++++---- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/public/script.js b/public/script.js index 507791c19..519865dec 100644 --- a/public/script.js +++ b/public/script.js @@ -8553,7 +8553,7 @@ async function doImpersonate(args, prompt) { async function doNewChat({ deleteCurrentChat = false } = {}) { //Make a new chat for selected character - if ((!selected_group && this_chid == undefined) || menu_type != 'create') { + if ((!selected_group && this_chid == undefined) || menu_type == 'create') { return; } diff --git a/public/scripts/bookmarks.js b/public/scripts/bookmarks.js index f099ba047..f6a2d1d70 100644 --- a/public/scripts/bookmarks.js +++ b/public/scripts/bookmarks.js @@ -24,6 +24,7 @@ import { saveGroupBookmarkChat, selected_group, } from './group-chats.js'; +import { Popup } from './popup.js'; import { createTagMapFromList } from './tags.js'; import { @@ -239,8 +240,7 @@ async function convertSoloToGroupChat() { return; } - const confirm = await callPopup('Are you sure you want to convert this chat to a group chat?', 'confirm'); - + const confirm = await Popup.show.confirm('Convert to group chat', 'Are you sure you want to convert this chat to a group chat?
This cannot be reverted.'); if (!confirm) { return; } diff --git a/public/scripts/popup.js b/public/scripts/popup.js index c31824f0b..d2138b722 100644 --- a/public/scripts/popup.js +++ b/public/scripts/popup.js @@ -78,8 +78,8 @@ const showPopupHelper = { /** * Asynchronously displays a confirmation popup with the given header and text, returning the clicked result button value. * - * @param {string} header - The header text for the popup. - * @param {string} text - The main text for the popup. + * @param {string?} header - The header text for the popup. + * @param {string?} text - The main text for the popup. * @param {PopupOptions} [popupOptions={}] - Options for the popup. * @return {Promise} A Promise that resolves with the result of the user's interaction. */ @@ -491,9 +491,17 @@ export class Popup { } class PopupUtils { + /** + * Builds popup content with header and text below + * + * @param {string} header - The header to be added to the text + * @param {string} text - The main text content + */ static BuildTextWithHeader(header, text) { - return ` -

${header}

+ if (!header) { + return text; + } + return `

${header}

${text}`; } } From f73986d23f8f35b250cf0319bab105acc2f8ddcc Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 27 Jun 2024 02:25:08 +0300 Subject: [PATCH 018/181] Remove extra linebreak --- public/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/script.js b/public/script.js index 519865dec..8b3e9a30d 100644 --- a/public/script.js +++ b/public/script.js @@ -9306,7 +9306,7 @@ jQuery(async function () {
`, { + `, { onClose: () => deleteChats = !!$('#del_char_checkbox').prop('checked'), }); if (!confirm) { From 7bf793d2be2ab1e92297520bccc6cdc233fd36de Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Thu, 27 Jun 2024 01:39:05 +0200 Subject: [PATCH 019/181] Update /newchat command skip popup --- public/script.js | 2 +- public/scripts/power-user.js | 28 +++++++++++++++++----------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/public/script.js b/public/script.js index 8b3e9a30d..499f5cc6f 100644 --- a/public/script.js +++ b/public/script.js @@ -8551,7 +8551,7 @@ async function doImpersonate(args, prompt) { return ''; } -async function doNewChat({ deleteCurrentChat = false } = {}) { +export async function doNewChat({ deleteCurrentChat = false } = {}) { //Make a new chat for selected character if ((!selected_group && this_chid == undefined) || menu_type == 'create') { return; diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index 654e479b2..e167109e8 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -22,6 +22,7 @@ import { setActiveGroup, setActiveCharacter, entitiesFilter, + doNewChat, } from '../script.js'; import { isMobile, initMovingUI, favsToHotswap } from './RossAscends-mods.js'; import { @@ -39,11 +40,11 @@ import { tokenizers } from './tokenizers.js'; import { BIAS_CACHE } from './logit-bias.js'; import { renderTemplateAsync } from './templates.js'; -import { countOccurrences, debounce, delay, download, getFileText, isOdd, onlyUnique, resetScrollHeight, shuffle, sortMoments, stringToRange, timestampToMoment } from './utils.js'; +import { countOccurrences, debounce, delay, download, getFileText, isOdd, isTrueBoolean, onlyUnique, resetScrollHeight, shuffle, sortMoments, stringToRange, timestampToMoment } from './utils.js'; import { FILTER_TYPES } from './filters.js'; import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { SlashCommand } from './slash-commands/SlashCommand.js'; -import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js'; +import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; import { AUTOCOMPLETE_WIDTH } from './autocomplete/AutoComplete.js'; import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js'; import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; @@ -2530,14 +2531,6 @@ async function resetMovablePanels(type) { }); } -async function doNewChat() { - $('#option_start_new_chat').trigger('click'); - await delay(1); - $('#dialogue_popup_ok').trigger('click'); - await delay(1); - return ''; -} - /** * Finds the ID of the tag with the given name. * @param {string} name @@ -3926,7 +3919,20 @@ $(document).ready(() => { })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'newchat', - callback: doNewChat, + /** @type {(args: { delete: string?}, string) => Promise<''>} */ + callback: async (args, _) => { + await doNewChat({ deleteCurrentChat: isTrueBoolean(args.delete) }); + return ''; + }, + namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'delete', + description: 'delete the current chat', + typeList: [ARGUMENT_TYPE.BOOLEAN], + defaultValue: 'false', + enumList: commonEnumProviders.boolean('trueFalse')(), + }), + ], helpString: 'Start a new chat with the current character', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ From 124cbfdfa40f646eb8a00d42d3eccd2064a23417 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Thu, 27 Jun 2024 02:28:25 +0200 Subject: [PATCH 020/181] Update Popup to support custom input checkboxes --- public/css/popup.css | 18 ++++++++ public/index.html | 1 + public/scripts/popup.js | 95 +++++++++++++++++++++++++++++++---------- public/style.css | 12 ------ 4 files changed, 92 insertions(+), 34 deletions(-) diff --git a/public/css/popup.css b/public/css/popup.css index 045bfc2e5..90d0914f0 100644 --- a/public/css/popup.css +++ b/public/css/popup.css @@ -92,6 +92,24 @@ dialog { text-align: left; } +.popup-crop-wrap { + margin: 10px auto; + max-height: 75vh; + max-height: 75svh; + max-width: 100%; +} + +.popup-crop-wrap img { + max-width: 100%; + /* This rule is very important, please do not ignore this! */ +} + +.popup-inputs { + margin-top: 10px; + font-size: smaller; + opacity: 0.7; +} + .popup-input { margin-top: 10px; } diff --git a/public/index.html b/public/index.html index d815e2300..8593d2ffc 100644 --- a/public/index.html +++ b/public/index.html @@ -4883,6 +4883,7 @@
+
- - - -
From b8ae54fb2c9e8f5899cda8fe856c9cf5fa423c73 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Thu, 27 Jun 2024 03:01:07 +0200 Subject: [PATCH 024/181] Add i18n to popup controls --- public/scripts/popup.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/public/scripts/popup.js b/public/scripts/popup.js index 7f61b8ac5..a34b9f1f0 100644 --- a/public/scripts/popup.js +++ b/public/scripts/popup.js @@ -178,7 +178,9 @@ export class Popup { // If custom button captions are provided, we set them beforehand this.okButton.textContent = typeof okButton === 'string' ? okButton : 'OK'; + this.okButton.dataset.i18n = this.okButton.textContent; this.cancelButton.textContent = typeof cancelButton === 'string' ? cancelButton : template.getAttribute('popup-button-cancel'); + this.cancelButton.dataset.i18n = this.cancelButton.textContent; this.defaultResult = defaultResult; this.customButtons = customButtons; @@ -191,6 +193,7 @@ export class Popup { buttonElement.classList.add(...(button.classes ?? [])); buttonElement.dataset.result = String(button.result ?? undefined); buttonElement.textContent = button.text; + buttonElement.dataset.i18n = buttonElement.textContent; buttonElement.tabIndex = 0; if (button.appendAtEnd) { @@ -226,8 +229,9 @@ export class Popup { if (input.tooltip) { const tooltip = document.createElement('div'); - tooltip.title = input.tooltip; tooltip.classList.add('fa-solid', 'fa-circle-info', 'opacity50p'); + tooltip.title = input.tooltip; + tooltip.dataset.i18n = '[title]' + input.tooltip; label.appendChild(tooltip); } @@ -462,7 +466,7 @@ export class Popup { if (!shouldClose) return; } - Popup.util.lastResult = { value, result }; + Popup.util.lastResult = { value, result, inputResults: this.inputResults }; this.#hide(); } @@ -518,10 +522,10 @@ export class Popup { * Contains the list of all currently open popups, and it'll remember the result of the last closed popup. */ static util = { - /** @type {Popup[]} Remember all popups */ + /** @readonly @type {Popup[]} Remember all popups */ popups: [], - /** @type {{value: any, result: POPUP_RESULT|number?}?} Last popup result */ + /** @type {{value: any, result: POPUP_RESULT|number?, inputResults: Map?}?} Last popup result */ lastResult: null, /** @returns {boolean} Checks if any modal popup dialog is open */ From 5075534b2e6120f0932115ab0b6f4dbf6f038f9f Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 27 Jun 2024 08:42:23 +0000 Subject: [PATCH 025/181] Fix vertical scrolling in data bank --- public/scripts/chats.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/chats.js b/public/scripts/chats.js index 2402dd2db..0d1ecba4c 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -1119,7 +1119,7 @@ async function openAttachmentManager() { const cleanupFn = await renderButtons(); await verifyAttachments(); await renderAttachments(); - await callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: true, large: true, okButton: 'Close' }); + await callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: true, large: true, okButton: 'Close', allowVerticalScrolling: true }); cleanupFn(); dragDropHandler.destroy(); From 4e083ebd4f2d425e93fa48889c757ad4314cece1 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 27 Jun 2024 08:45:09 +0000 Subject: [PATCH 026/181] Allow vertical scrolling in settings snapshots --- public/scripts/user.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/user.js b/public/scripts/user.js index 993c0ce3f..4aab9bb1b 100644 --- a/public/scripts/user.js +++ b/public/scripts/user.js @@ -592,7 +592,7 @@ async function viewSettingsSnapshots() { } } - callGenericPopup(template, POPUP_TYPE.TEXT, '', { okButton: 'Close', wide: false, large: false }); + callGenericPopup(template, POPUP_TYPE.TEXT, '', { okButton: 'Close', wide: false, large: false, allowVerticalScrolling: true }); template.find('.makeSnapshotButton').on('click', () => makeSnapshot(renderSnapshots)); renderSnapshots(); } From 79b8dc98eb4946f1f257fb1a25a5ff336d71111b Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 27 Jun 2024 10:04:49 +0000 Subject: [PATCH 027/181] Fix performance of WI editor when adding a new entry --- public/scripts/world-info.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index 507395e4c..2150fa4ab 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -1756,14 +1756,21 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl return entriesArray; } + const storageKey = 'WI_PerPage'; + const perPageDefault = 25; let startPage = 1; if (navigation === navigation_option.previous) { startPage = $('#world_info_pagination').pagination('getCurrentPageNum'); } - const storageKey = 'WI_PerPage'; - const perPageDefault = 25; + if (typeof navigation === 'number' && Number(navigation) >= 0) { + const data = getDataArray(); + const uidIndex = data.findIndex(x => x.uid === navigation); + const perPage = Number(localStorage.getItem(storageKey)) || perPageDefault; + startPage = Math.floor(uidIndex / perPage) + 1; + } + $('#world_info_pagination').pagination({ dataSource: getDataArray, pageSize: Number(localStorage.getItem(storageKey)) || perPageDefault, @@ -1824,15 +1831,8 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl }, }); - - if (typeof navigation === 'number' && Number(navigation) >= 0) { const selector = `#world_popup_entries_list [uid="${navigation}"]`; - const data = getDataArray(); - const uidIndex = data.findIndex(x => x.uid === navigation); - const perPage = Number(localStorage.getItem(storageKey)) || perPageDefault; - const page = Math.floor(uidIndex / perPage) + 1; - $('#world_info_pagination').pagination('go', page); waitUntilCondition(() => document.querySelector(selector) !== null).finally(() => { const element = $(selector); From bd5592de7b341b49463fdb9fcf1452a4876852b1 Mon Sep 17 00:00:00 2001 From: DarokCx <77368869+DarokCx@users.noreply.github.com> Date: Thu, 27 Jun 2024 09:06:11 -0400 Subject: [PATCH 028/181] Added featherless, connect button not working --- public/index.html | 34 ++++++++++++++++ public/script.js | 7 +++- public/scripts/secrets.js | 1 + public/scripts/textgen-models.js | 31 +++++++++++++++ public/scripts/textgen-settings.js | 7 +++- src/constants.js | 45 ++++++++++++++++++++++ src/endpoints/backends/text-completions.js | 5 ++- 7 files changed, 126 insertions(+), 4 deletions(-) diff --git a/public/index.html b/public/index.html index 4c2ccdfcd..30ad41652 100644 --- a/public/index.html +++ b/public/index.html @@ -2038,6 +2038,7 @@ +
@@ -2182,6 +2183,39 @@
+ + + +
+ +

API key

+
+ + +
+
+ For privacy reasons, your API key will be hidden after you reload the page. +
+
+

Server URL

+ Try: Api.Featherless.ai/v1 + +
+ +
+ + + + + + +
diff --git a/public/script.js b/public/script.js index a9526a7a8..76c18a23b 100644 --- a/public/script.js +++ b/public/script.js @@ -22,7 +22,7 @@ import { parseTabbyLogprobs, } from './scripts/textgen-settings.js'; -const { MANCER, TOGETHERAI, OOBA, VLLM, APHRODITE, TABBY, OLLAMA, INFERMATICAI, DREAMGEN, OPENROUTER } = textgen_types; +const { MANCER, TOGETHERAI, OOBA, VLLM, APHRODITE, TABBY, OLLAMA, INFERMATICAI, DREAMGEN, OPENROUTER, FEATHERLESS} = textgen_types; import { world_info, @@ -222,7 +222,7 @@ import { import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js'; import { hideLoader, showLoader } from './scripts/loader.js'; import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js'; -import { loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels } from './scripts/textgen-models.js'; +import { loadFeatherlessModels, loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels } from './scripts/textgen-models.js'; import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId } from './scripts/chats.js'; import { initPresetManager } from './scripts/preset-manager.js'; import { evaluateMacros } from './scripts/macros.js'; @@ -1122,6 +1122,8 @@ async function getStatusTextgen() { } else if (textgen_settings.type === APHRODITE) { loadAphroditeModels(data?.data); online_status = textgen_settings.aphrodite_model; + } else if (textgen_settings.type === FEATHERLESS) { + loadFeatherlessModels(data?.data); } else { online_status = data?.result; } @@ -9322,6 +9324,7 @@ jQuery(async function () { { id: 'api_key_openrouter-tg', secret: SECRET_KEYS.OPENROUTER }, { id: 'api_key_koboldcpp', secret: SECRET_KEYS.KOBOLDCPP }, { id: 'api_key_llamacpp', secret: SECRET_KEYS.LLAMACPP }, + { id: 'api_key_featherless', secret: SECRET_KEYS.FEATHERLESS }, ]; for (const key of keys) { diff --git a/public/scripts/secrets.js b/public/scripts/secrets.js index cb4477a78..f9f3ec86d 100644 --- a/public/scripts/secrets.js +++ b/public/scripts/secrets.js @@ -28,6 +28,7 @@ export const SECRET_KEYS = { PERPLEXITY: 'api_key_perplexity', GROQ: 'api_key_groq', AZURE_TTS: 'api_key_azure_tts', + FEATHERLESS: 'api_key_featherless', }; const INPUT_MAP = { diff --git a/public/scripts/textgen-models.js b/public/scripts/textgen-models.js index d8f36cf45..5e5d69338 100644 --- a/public/scripts/textgen-models.js +++ b/public/scripts/textgen-models.js @@ -9,6 +9,7 @@ let infermaticAIModels = []; let dreamGenModels = []; let vllmModels = []; let aphroditeModels = []; +let featherlessModels = []; export let openRouterModels = []; /** @@ -231,6 +232,35 @@ export async function loadAphroditeModels(data) { } } +export async function loadFeatherlessModels(data) { + if (!Array.isArray(data)) { + console.error('Invalid Featherless models data', data); + return; + } + + featherlessModels = data; + + if (!data.find(x => x.id === textgen_settings.featherless_model)) { + textgen_settings.featherless_model = data[0]?.id || ''; + } + + $('#featherless_model').empty(); + for (const model of data) { + const option = document.createElement('option'); + option.value = model.id; + option.text = model.id; + option.selected = model.id === textgen_settings.featherless_model; + $('#featherless_model').append(option); + } +} + +function onFeatherlessModelSelect() { + const modelId = String($('#featherless_model').val()); + textgen_settings.featherless_model = modelId; + $('#api_button_textgenerationwebui').trigger('click'); +} + + function onMancerModelSelect() { const modelId = String($('#mancer_model').val()); textgen_settings.mancer_model = modelId; @@ -505,6 +535,7 @@ jQuery(function () { $('#ollama_download_model').on('click', downloadOllamaModel); $('#vllm_model').on('change', onVllmModelSelect); $('#aphrodite_model').on('change', onAphroditeModelSelect); + $('#featherless_model').on('change', onFeatherlessModelSelect); const providersSelect = $('.openrouter_providers'); for (const provider of OPENROUTER_PROVIDERS) { diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index dac12be1c..e97c62f17 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -38,9 +38,10 @@ export const textgen_types = { INFERMATICAI: 'infermaticai', DREAMGEN: 'dreamgen', OPENROUTER: 'openrouter', + FEATHERLESS: 'featherless', }; -const { MANCER, VLLM, APHRODITE, TABBY, TOGETHERAI, OOBA, OLLAMA, LLAMACPP, INFERMATICAI, DREAMGEN, OPENROUTER, KOBOLDCPP } = textgen_types; +const { MANCER, VLLM, APHRODITE, TABBY, TOGETHERAI, OOBA, OLLAMA, LLAMACPP, INFERMATICAI, DREAMGEN, OPENROUTER, KOBOLDCPP, FEATHERLESS } = textgen_types; const LLAMACPP_DEFAULT_ORDER = [ 'top_k', @@ -75,6 +76,7 @@ let TOGETHERAI_SERVER = 'https://api.together.xyz'; let INFERMATICAI_SERVER = 'https://api.totalgpt.ai'; let DREAMGEN_SERVER = 'https://dreamgen.com'; let OPENROUTER_SERVER = 'https://openrouter.ai/api'; +let FEATHERLESS_SERVER = 'https://api.featherless.ai/v1'; const SERVER_INPUTS = { [textgen_types.OOBA]: '#textgenerationwebui_api_url_text', @@ -84,6 +86,7 @@ const SERVER_INPUTS = { [textgen_types.KOBOLDCPP]: '#koboldcpp_api_url_text', [textgen_types.LLAMACPP]: '#llamacpp_api_url_text', [textgen_types.OLLAMA]: '#ollama_api_url_text', + [textgen_types.FEATHERLESS]: '#featherless_api_url_text', }; const KOBOLDCPP_ORDER = [6, 0, 1, 3, 4, 2, 5]; @@ -265,6 +268,8 @@ export function validateTextGenUrl() { export function getTextGenServer() { switch (settings.type) { + case FEATHERLESS: + return FEATHERLESS_SERVER; case MANCER: return MANCER_SERVER; case TOGETHERAI: diff --git a/src/constants.js b/src/constants.js index b4945fa6f..e3556c97b 100644 --- a/src/constants.js +++ b/src/constants.js @@ -212,6 +212,7 @@ const TEXTGEN_TYPES = { INFERMATICAI: 'infermaticai', DREAMGEN: 'dreamgen', OPENROUTER: 'openrouter', + FEATHERLESS: 'featherless', }; const INFERMATICAI_KEYS = [ @@ -226,6 +227,49 @@ const INFERMATICAI_KEYS = [ 'stop', ]; +const FEATHERLESS_KEYS = [ + 'model', + 'prompt', + 'best_of', + 'echo', + 'frequency_penalty', + 'logit_bias', + 'logprobs', + 'max_tokens', + 'n', + 'presence_penalty', + 'seed', + 'stop', + 'stream', + 'suffix', + 'temperature', + 'top_p', + 'user', + + 'use_beam_search', + 'top_k', + 'min_p', + 'repetition_penalty', + 'length_penalty', + 'early_stopping', + 'stop_token_ids', + 'ignore_eos', + 'min_tokens', + 'skip_special_tokens', + 'spaces_between_special_tokens', + 'truncate_prompt_tokens', + + 'include_stop_str_in_output', + 'response_format', + 'guided_json', + 'guided_regex', + 'guided_choice', + 'guided_grammar', + 'guided_decoding_backend', + 'guided_whitespace_pattern', +]; + + // https://dreamgen.com/docs/api#openai-text const DREAMGEN_KEYS = [ 'model', @@ -366,4 +410,5 @@ module.exports = { OPENROUTER_HEADERS, OPENROUTER_KEYS, VLLM_KEYS, + FEATHERLESS_KEYS, }; diff --git a/src/endpoints/backends/text-completions.js b/src/endpoints/backends/text-completions.js index 49324e443..aeff56258 100644 --- a/src/endpoints/backends/text-completions.js +++ b/src/endpoints/backends/text-completions.js @@ -126,6 +126,9 @@ router.post('/status', jsonParser, async function (request, response) { case TEXTGEN_TYPES.OLLAMA: url += '/api/tags'; break; + case TEXTGEN_TYPES.FEATHERLESS: + url += '/v1/models'; + break; } } @@ -135,7 +138,7 @@ router.post('/status', jsonParser, async function (request, response) { console.log('Models endpoint is offline.'); return response.status(400); } - + console.log("url for models", url) let data = await modelsReply.json(); if (request.body.legacy_api) { From 8608bc92ae205b96ca9ee512d7bf95744e24ed86 Mon Sep 17 00:00:00 2001 From: DarokCx <77368869+DarokCx@users.noreply.github.com> Date: Thu, 27 Jun 2024 10:02:28 -0400 Subject: [PATCH 029/181] no authorization --- public/script.js | 1 + src/endpoints/backends/text-completions.js | 8 +++++++- src/endpoints/secrets.js | 2 ++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/public/script.js b/public/script.js index 76c18a23b..995dd3e2e 100644 --- a/public/script.js +++ b/public/script.js @@ -1124,6 +1124,7 @@ async function getStatusTextgen() { online_status = textgen_settings.aphrodite_model; } else if (textgen_settings.type === FEATHERLESS) { loadFeatherlessModels(data?.data); + online_status = textgen_settings.featherless_model; } else { online_status = data?.result; } diff --git a/src/endpoints/backends/text-completions.js b/src/endpoints/backends/text-completions.js index aeff56258..782819058 100644 --- a/src/endpoints/backends/text-completions.js +++ b/src/endpoints/backends/text-completions.js @@ -4,7 +4,7 @@ const _ = require('lodash'); const Readable = require('stream').Readable; const { jsonParser } = require('../../express-common'); -const { TEXTGEN_TYPES, TOGETHERAI_KEYS, OLLAMA_KEYS, INFERMATICAI_KEYS, OPENROUTER_KEYS, VLLM_KEYS, DREAMGEN_KEYS } = require('../../constants'); +const { TEXTGEN_TYPES, TOGETHERAI_KEYS, OLLAMA_KEYS, INFERMATICAI_KEYS, OPENROUTER_KEYS, VLLM_KEYS, DREAMGEN_KEYS, FEATHERLESS_KEYS } = require('../../constants'); const { forwardFetchResponse, trimV1 } = require('../../util'); const { setAdditionalHeaders } = require('../../additional-headers'); @@ -238,6 +238,7 @@ router.post('/generate', jsonParser, async function (request, response) { } else { switch (request.body.api_type) { case TEXTGEN_TYPES.VLLM: + case TEXTGEN_TYPES.FEATHERLESS: case TEXTGEN_TYPES.APHRODITE: case TEXTGEN_TYPES.OOBA: case TEXTGEN_TYPES.TABBY: @@ -284,6 +285,11 @@ router.post('/generate', jsonParser, async function (request, response) { args.body = JSON.stringify(request.body); } + if (request.body.api_type === TEXTGEN_TYPES.FEATHERLESS) { + request.body = _.pickBy(request.body, (_, key) => FEATHERLESS_KEYS.includes(key)); + args.body = JSON.stringify(request.body); + } + if (request.body.api_type === TEXTGEN_TYPES.DREAMGEN) { request.body = _.pickBy(request.body, (_, key) => DREAMGEN_KEYS.includes(key)); // NOTE: DreamGen sometimes get confused by the unusual formatting in the character cards. diff --git a/src/endpoints/secrets.js b/src/endpoints/secrets.js index 9bf2eb765..df976df67 100644 --- a/src/endpoints/secrets.js +++ b/src/endpoints/secrets.js @@ -4,6 +4,7 @@ const express = require('express'); const { getConfigValue } = require('../util'); const writeFileAtomicSync = require('write-file-atomic').sync; const { jsonParser } = require('../express-common'); +const { FEATHERLESS_KEYS } = require('../constants'); const SECRETS_FILE = 'secrets.json'; const SECRET_KEYS = { @@ -40,6 +41,7 @@ const SECRET_KEYS = { PERPLEXITY: 'api_key_perplexity', GROQ: 'api_key_groq', AZURE_TTS: 'api_key_azure_tts', + FEATHERLESS: 'api_key_featherless', }; // These are the keys that are safe to expose, even if allowKeysExposure is false From 537cfbc02750698d287281ca1dcef453259de442 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 27 Jun 2024 23:40:47 +0300 Subject: [PATCH 030/181] Remove commented theme toggles --- public/index.html | 89 ----------------------------------------------- 1 file changed, 89 deletions(-) diff --git a/public/index.html b/public/index.html index 8593d2ffc..c38e7b76d 100644 --- a/public/index.html +++ b/public/index.html @@ -3749,95 +3749,6 @@
- - - - - - - - - - -

From cf56bfb6a949ad04149300ec2124f632cfd6a5c0 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 28 Jun 2024 00:51:09 +0300 Subject: [PATCH 031/181] Add 01.AI as a chat completion source --- public/img/01ai.svg | 59 ++++++++++++++++ public/index.html | 22 +++++- public/script.js | 5 ++ public/scripts/RossAscends-mods.js | 5 +- public/scripts/openai.js | 78 +++++++++++++++++++++- public/scripts/secrets.js | 2 + public/scripts/slash-commands.js | 1 + public/scripts/tokenizers.js | 4 ++ src/constants.js | 1 + src/endpoints/backends/chat-completions.js | 10 +++ src/endpoints/secrets.js | 1 + 11 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 public/img/01ai.svg diff --git a/public/img/01ai.svg b/public/img/01ai.svg new file mode 100644 index 000000000..317a5da59 --- /dev/null +++ b/public/img/01ai.svg @@ -0,0 +1,59 @@ + + + + + + + + + diff --git a/public/index.html b/public/index.html index c38e7b76d..2f9bee085 100644 --- a/public/index.html +++ b/public/index.html @@ -423,7 +423,7 @@
-
+
Temperature
@@ -488,7 +488,7 @@
-
+
Top P
@@ -2360,6 +2360,7 @@ + @@ -2890,6 +2891,23 @@ +
+

+ + 01.AI API Key + +

+
+ + +
+
+ For privacy reasons, your API key will be hidden after you reload the page. +
+

01.AI Model

+ +
diff --git a/public/script.js b/public/script.js index 499f5cc6f..8d5da9ae5 100644 --- a/public/script.js +++ b/public/script.js @@ -8289,6 +8289,11 @@ const CONNECT_API_MAP = { button: '#api_button_openai', source: chat_completion_sources.GROQ, }, + '01ai': { + selected: 'openai', + button: '#api_button_openai', + source: chat_completion_sources.ZEROONEAI, + }, 'infermaticai': { selected: 'textgenerationwebui', button: '#api_button_textgenerationwebui', diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index 3348a9e71..3a2761fd3 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -379,6 +379,7 @@ function RA_autoconnect(PrevApi) { || (secret_state[SECRET_KEYS.COHERE] && oai_settings.chat_completion_source == chat_completion_sources.COHERE) || (secret_state[SECRET_KEYS.PERPLEXITY] && oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY) || (secret_state[SECRET_KEYS.GROQ] && oai_settings.chat_completion_source == chat_completion_sources.GROQ) + || (secret_state[SECRET_KEYS.ZEROONEAI] && oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI) || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM) ) { $('#api_button_openai').trigger('click'); @@ -476,8 +477,8 @@ export function dragElement(elmnt) { } const style = getComputedStyle(target); - height = parseInt(style.height) - width = parseInt(style.width) + height = parseInt(style.height); + width = parseInt(style.width); top = parseInt(style.top); left = parseInt(style.left); right = parseInt(style.right); diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 37551dd34..440a37f04 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -181,6 +181,7 @@ export const chat_completion_sources = { COHERE: 'cohere', PERPLEXITY: 'perplexity', GROQ: 'groq', + ZEROONEAI: '01ai', }; const character_names_behavior = { @@ -251,6 +252,7 @@ const default_settings = { cohere_model: 'command-r', perplexity_model: 'llama-3-70b-instruct', groq_model: 'llama3-70b-8192', + zerooneai_model: 'yi-large', custom_model: '', custom_url: '', custom_include_body: '', @@ -329,6 +331,7 @@ const oai_settings = { cohere_model: 'command-r', perplexity_model: 'llama-3-70b-instruct', groq_model: 'llama3-70b-8192', + zerooneai_model: 'yi-large', custom_model: '', custom_url: '', custom_include_body: '', @@ -1470,6 +1473,8 @@ function getChatCompletionModel() { return oai_settings.perplexity_model; case chat_completion_sources.GROQ: return oai_settings.groq_model; + case chat_completion_sources.ZEROONEAI: + return oai_settings.zerooneai_model; default: throw new Error(`Unknown chat completion source: ${oai_settings.chat_completion_source}`); } @@ -1566,6 +1571,23 @@ function saveModelList(data) { $('#model_custom_select').val(model_list[0].id).trigger('change'); } } + + if (oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI) { + $('#model_01ai_select').empty(); + model_list.forEach((model) => { + $('#model_01ai_select').append( + $('
-
+
Seed
@@ -2029,6 +2029,7 @@ + @@ -2211,6 +2212,22 @@
+
+

HuggingFace Token

+
+ + +
+
+ For privacy reasons, your API key will be hidden after you reload the page. +
+
+

Endpoint URL

+ Example: https://****.endpoints.huggingface.cloud + +
+
@@ -2332,7 +2349,7 @@
- +
diff --git a/public/script.js b/public/script.js index ec738648c..ab9543751 100644 --- a/public/script.js +++ b/public/script.js @@ -8311,6 +8311,11 @@ const CONNECT_API_MAP = { button: '#api_button_textgenerationwebui', type: textgen_types.OPENROUTER, }, + 'huggingface': { + selected: 'textgenerationwebui', + button: '#api_button_textgenerationwebui', + type: textgen_types.HUGGINGFACE, + }, }; async function selectContextCallback(_, name) { @@ -9471,6 +9476,7 @@ jQuery(async function () { { id: 'api_key_openrouter-tg', secret: SECRET_KEYS.OPENROUTER }, { id: 'api_key_koboldcpp', secret: SECRET_KEYS.KOBOLDCPP }, { id: 'api_key_llamacpp', secret: SECRET_KEYS.LLAMACPP }, + { id: 'api_key_huggingface', secret: SECRET_KEYS.HUGGINGFACE }, ]; for (const key of keys) { diff --git a/public/scripts/secrets.js b/public/scripts/secrets.js index 8989cdb50..4410ed1ee 100644 --- a/public/scripts/secrets.js +++ b/public/scripts/secrets.js @@ -29,6 +29,7 @@ export const SECRET_KEYS = { GROQ: 'api_key_groq', AZURE_TTS: 'api_key_azure_tts', ZEROONEAI: 'api_key_01ai', + HUGGINGFACE: 'api_key_huggingface', }; const INPUT_MAP = { @@ -58,6 +59,7 @@ const INPUT_MAP = { [SECRET_KEYS.PERPLEXITY]: '#api_key_perplexity', [SECRET_KEYS.GROQ]: '#api_key_groq', [SECRET_KEYS.ZEROONEAI]: '#api_key_01ai', + [SECRET_KEYS.HUGGINGFACE]: '#api_key_huggingface', }; async function clearSecret() { diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index 6623f1d9e..eed2d0ef8 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -38,9 +38,24 @@ export const textgen_types = { INFERMATICAI: 'infermaticai', DREAMGEN: 'dreamgen', OPENROUTER: 'openrouter', + HUGGINGFACE: 'huggingface', }; -const { MANCER, VLLM, APHRODITE, TABBY, TOGETHERAI, OOBA, OLLAMA, LLAMACPP, INFERMATICAI, DREAMGEN, OPENROUTER, KOBOLDCPP } = textgen_types; +const { + MANCER, + VLLM, + APHRODITE, + TABBY, + TOGETHERAI, + OOBA, + OLLAMA, + LLAMACPP, + INFERMATICAI, + DREAMGEN, + OPENROUTER, + KOBOLDCPP, + HUGGINGFACE, +} = textgen_types; const LLAMACPP_DEFAULT_ORDER = [ 'top_k', @@ -84,6 +99,7 @@ const SERVER_INPUTS = { [textgen_types.KOBOLDCPP]: '#koboldcpp_api_url_text', [textgen_types.LLAMACPP]: '#llamacpp_api_url_text', [textgen_types.OLLAMA]: '#ollama_api_url_text', + [textgen_types.HUGGINGFACE]: '#huggingface_api_url_text', }; const KOBOLDCPP_ORDER = [6, 0, 1, 3, 4, 2, 5]; @@ -1009,6 +1025,8 @@ export function getTextGenModel() { throw new Error('No Ollama model selected'); } return settings.ollama_model; + case HUGGINGFACE: + return 'tgi'; default: return undefined; } @@ -1146,6 +1164,12 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, params.grammar = settings.grammar_string; } + if (settings.type === HUGGINGFACE) { + params.top_p = Math.min(Math.max(Number(params.top_p), 0.0), 0.999); + params.stop = Array.isArray(params.stop) ? params.stop.slice(0, 4) : []; + nonAphroditeParams.seed = settings.seed >= 0 ? settings.seed : undefined; + } + if (settings.type === MANCER) { params.n = canMultiSwipe ? settings.n : 1; params.epsilon_cutoff /= 1000; diff --git a/src/additional-headers.js b/src/additional-headers.js index b8a44b390..8a188caa6 100644 --- a/src/additional-headers.js +++ b/src/additional-headers.js @@ -147,6 +147,19 @@ function getKoboldCppHeaders(directories) { }) : {}; } +/** + * Gets the headers for the HuggingFace API. + * @param {import('./users').UserDirectoryList} directories + * @returns {object} Headers for the request + */ +function getHuggingFaceHeaders(directories) { + const apiKey = readSecret(directories, SECRET_KEYS.HUGGINGFACE); + + return apiKey ? ({ + 'Authorization': `Bearer ${apiKey}`, + }) : {}; +} + function getOverrideHeaders(urlHost) { const requestOverrides = getConfigValue('requestOverrides', []); const overrideHeaders = requestOverrides?.find((e) => e.hosts?.includes(urlHost))?.headers; @@ -187,6 +200,7 @@ function setAdditionalHeadersByType(requestHeaders, type, server, directories) { [TEXTGEN_TYPES.OPENROUTER]: getOpenRouterHeaders, [TEXTGEN_TYPES.KOBOLDCPP]: getKoboldCppHeaders, [TEXTGEN_TYPES.LLAMACPP]: getLlamaCppHeaders, + [TEXTGEN_TYPES.HUGGINGFACE]: getHuggingFaceHeaders, }; const getHeaders = headerGetters[type]; diff --git a/src/constants.js b/src/constants.js index 842c35d46..01ba9e32e 100644 --- a/src/constants.js +++ b/src/constants.js @@ -216,6 +216,7 @@ const TEXTGEN_TYPES = { INFERMATICAI: 'infermaticai', DREAMGEN: 'dreamgen', OPENROUTER: 'openrouter', + HUGGINGFACE: 'huggingface', }; const INFERMATICAI_KEYS = [ diff --git a/src/endpoints/backends/text-completions.js b/src/endpoints/backends/text-completions.js index a6c55acbd..8ca61ecf3 100644 --- a/src/endpoints/backends/text-completions.js +++ b/src/endpoints/backends/text-completions.js @@ -95,13 +95,14 @@ router.post('/status', jsonParser, async function (request, response) { setAdditionalHeaders(request, args, baseUrl); + const apiType = request.body.api_type; let url = baseUrl; let result = ''; if (request.body.legacy_api) { url += '/v1/model'; } else { - switch (request.body.api_type) { + switch (apiType) { case TEXTGEN_TYPES.OOBA: case TEXTGEN_TYPES.VLLM: case TEXTGEN_TYPES.APHRODITE: @@ -126,6 +127,9 @@ router.post('/status', jsonParser, async function (request, response) { case TEXTGEN_TYPES.OLLAMA: url += '/api/tags'; break; + case TEXTGEN_TYPES.HUGGINGFACE: + url += '/info'; + break; } } @@ -144,14 +148,18 @@ router.post('/status', jsonParser, async function (request, response) { } // Rewrap to OAI-like response - if (request.body.api_type === TEXTGEN_TYPES.TOGETHERAI && Array.isArray(data)) { + if (apiType === TEXTGEN_TYPES.TOGETHERAI && Array.isArray(data)) { data = { data: data.map(x => ({ id: x.name, ...x })) }; } - if (request.body.api_type === TEXTGEN_TYPES.OLLAMA && Array.isArray(data.models)) { + if (apiType === TEXTGEN_TYPES.OLLAMA && Array.isArray(data.models)) { data = { data: data.models.map(x => ({ id: x.name, ...x })) }; } + if (apiType === TEXTGEN_TYPES.HUGGINGFACE) { + data = { data: [] }; + } + if (!Array.isArray(data.data)) { console.log('Models response is not an array.'); return response.status(400); @@ -163,7 +171,7 @@ router.post('/status', jsonParser, async function (request, response) { // Set result to the first model ID result = modelIds[0] || 'Valid'; - if (request.body.api_type === TEXTGEN_TYPES.OOBA) { + if (apiType === TEXTGEN_TYPES.OOBA) { try { const modelInfoUrl = baseUrl + '/v1/internal/model/info'; const modelInfoReply = await fetch(modelInfoUrl, args); @@ -178,7 +186,7 @@ router.post('/status', jsonParser, async function (request, response) { } catch (error) { console.error(`Failed to get Ooba model info: ${error}`); } - } else if (request.body.api_type === TEXTGEN_TYPES.TABBY) { + } else if (apiType === TEXTGEN_TYPES.TABBY) { try { const modelInfoUrl = baseUrl + '/v1/model'; const modelInfoReply = await fetch(modelInfoUrl, args); @@ -241,6 +249,7 @@ router.post('/generate', jsonParser, async function (request, response) { case TEXTGEN_TYPES.KOBOLDCPP: case TEXTGEN_TYPES.TOGETHERAI: case TEXTGEN_TYPES.INFERMATICAI: + case TEXTGEN_TYPES.HUGGINGFACE: url += '/v1/completions'; break; case TEXTGEN_TYPES.DREAMGEN: diff --git a/src/endpoints/secrets.js b/src/endpoints/secrets.js index 669dd86b4..532fb1a32 100644 --- a/src/endpoints/secrets.js +++ b/src/endpoints/secrets.js @@ -41,6 +41,7 @@ const SECRET_KEYS = { GROQ: 'api_key_groq', AZURE_TTS: 'api_key_azure_tts', ZEROONEAI: 'api_key_01ai', + HUGGINGFACE: 'api_key_huggingface', }; // These are the keys that are safe to expose, even if allowKeysExposure is false From 62a14fb74b2acb6d03d434a4011cefdbf9da3706 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 28 Jun 2024 18:22:29 +0300 Subject: [PATCH 052/181] Create PULL_REQUEST_TEMPLATE.md --- .github/PULL_REQUEST_TEMPLATE.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..e8e292fef --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,2 @@ +* **Please check if the PR fulfills these requirements** +- [ ] I have read the [contribution guidelines](https://github.com/SillyTavern/SillyTavern/blob/release/CONTRIBUTING.md) From c69c5e07e3b1fcdbbc14bf628e43d6a34ad3b18d Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 28 Jun 2024 18:25:07 +0300 Subject: [PATCH 053/181] Update PULL_REQUEST_TEMPLATE.md --- .github/PULL_REQUEST_TEMPLATE.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e8e292fef..a8d364dc3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,2 +1,3 @@ -* **Please check if the PR fulfills these requirements** -- [ ] I have read the [contribution guidelines](https://github.com/SillyTavern/SillyTavern/blob/release/CONTRIBUTING.md) +## Checklist: + +- [ ] I have read the [Contributing guidelines](https://github.com/SillyTavern/SillyTavern/blob/release/CONTRIBUTING.md). From 0c129f6dbec09952a197364be1cb2f7a972b5641 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 28 Jun 2024 18:31:16 +0300 Subject: [PATCH 054/181] Rename PULL_REQUEST_TEMPLATE.md to pull_request_template.md --- .github/{PULL_REQUEST_TEMPLATE.md => pull_request_template.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{PULL_REQUEST_TEMPLATE.md => pull_request_template.md} (100%) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/pull_request_template.md similarity index 100% rename from .github/PULL_REQUEST_TEMPLATE.md rename to .github/pull_request_template.md From 4e33253a91bc3c708c0033dd9d99d97c23f54941 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 28 Jun 2024 19:12:56 +0300 Subject: [PATCH 055/181] Code clean-up --- public/index.html | 26 ++++++---------------- src/endpoints/backends/text-completions.js | 2 +- src/endpoints/secrets.js | 1 - 3 files changed, 8 insertions(+), 21 deletions(-) diff --git a/public/index.html b/public/index.html index 7827c3632..ad52d580f 100644 --- a/public/index.html +++ b/public/index.html @@ -2029,6 +2029,7 @@ + @@ -2039,7 +2040,6 @@ -
@@ -2184,15 +2184,11 @@
- - -

API key

@@ -2203,20 +2199,12 @@
For privacy reasons, your API key will be hidden after you reload the page.
-
-

Server URL

- Try: Api.Featherless.ai/v1 - -
- +
- - - - - - -
diff --git a/src/endpoints/backends/text-completions.js b/src/endpoints/backends/text-completions.js index 62ebb556d..68fa9ce14 100644 --- a/src/endpoints/backends/text-completions.js +++ b/src/endpoints/backends/text-completions.js @@ -142,7 +142,7 @@ router.post('/status', jsonParser, async function (request, response) { console.log('Models endpoint is offline.'); return response.status(400); } - console.log('url for models', url); + let data = await modelsReply.json(); if (request.body.legacy_api) { diff --git a/src/endpoints/secrets.js b/src/endpoints/secrets.js index 93597e7b9..dedb23096 100644 --- a/src/endpoints/secrets.js +++ b/src/endpoints/secrets.js @@ -4,7 +4,6 @@ const express = require('express'); const { getConfigValue } = require('../util'); const writeFileAtomicSync = require('write-file-atomic').sync; const { jsonParser } = require('../express-common'); -const { FEATHERLESS_KEYS } = require('../constants'); const SECRETS_FILE = 'secrets.json'; const SECRET_KEYS = { From cc9eca8427bfd261297c6c1c6c75310b833a810d Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 28 Jun 2024 19:13:46 +0300 Subject: [PATCH 056/181] Apply select2 to model selection --- public/scripts/textgen-models.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/public/scripts/textgen-models.js b/public/scripts/textgen-models.js index 371ac9d9d..a33c0d542 100644 --- a/public/scripts/textgen-models.js +++ b/public/scripts/textgen-models.js @@ -603,6 +603,12 @@ jQuery(function () { width: '100%', templateResult: getAphroditeModelTemplate, }); + $('#featherless_model').select2({ + placeholder: 'Select a model', + searchInputPlaceholder: 'Search models...', + searchInputCssClass: 'text_pole', + width: '100%', + }); providersSelect.select2({ sorter: data => data.sort((a, b) => a.text.localeCompare(b.text)), placeholder: 'Select providers. No selection = all providers.', From 7ea560307c934a2b626285752d6ce0199117a79b Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 28 Jun 2024 19:45:47 +0300 Subject: [PATCH 057/181] Add third-party extensions links to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index fbef3d33f..4be6774fb 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,4 @@ public/css/user.css /plugins/ /data /default/scaffold +public/scripts/extensions/third-party From 54c772622eb61ce6f61dac5ec9285b4e043cbe85 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 28 Jun 2024 20:26:25 +0300 Subject: [PATCH 058/181] Add you-know-what for featherless --- public/img/featherless.svg | 39 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 public/img/featherless.svg diff --git a/public/img/featherless.svg b/public/img/featherless.svg new file mode 100644 index 000000000..b386387f4 --- /dev/null +++ b/public/img/featherless.svg @@ -0,0 +1,39 @@ + + From fa1d45635b06df1b6833e6df0e3cda533cc5481d Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 28 Jun 2024 20:40:57 +0300 Subject: [PATCH 059/181] Put mobile height fix under a breakpoint --- public/css/popup.css | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/public/css/popup.css b/public/css/popup.css index d09744933..3a1881353 100644 --- a/public/css/popup.css +++ b/public/css/popup.css @@ -32,7 +32,7 @@ dialog { flex-direction: column; overflow: hidden; width: 100%; - height: auto; + height: 100%; padding: 1px; } @@ -170,3 +170,9 @@ body.no-blur .popup[open]::backdrop { /* Fix weird animation issue with font-scaling during popup open */ backface-visibility: hidden; } + +@media screen and (max-width: 1000px) { + .popup .popup-body { + height: auto; + } +} From 380371446504481b7efdb1185ff9584a05466e39 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 28 Jun 2024 20:46:41 +0300 Subject: [PATCH 060/181] Limit height of enlarged image prompt --- public/style.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/public/style.css b/public/style.css index a3c631b81..101fe4184 100644 --- a/public/style.css +++ b/public/style.css @@ -4549,6 +4549,13 @@ a { padding: 1em; } +.img_enlarged_container pre { + max-height: 25vh; + max-height: 25svh; + flex-shrink: 0; + overflow: auto; +} + .popup:has(.img_enlarged.zoomed).large_dialogue_popup { height: 100vh !important; height: 100svh !important; From 1c69ba1ae39a7b5780b0feabb15dbd138582158d Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 28 Jun 2024 20:51:10 +0300 Subject: [PATCH 061/181] Change mobile cope styles --- public/css/popup.css | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/css/popup.css b/public/css/popup.css index 3a1881353..b7a32ec89 100644 --- a/public/css/popup.css +++ b/public/css/popup.css @@ -173,6 +173,8 @@ body.no-blur .popup[open]::backdrop { @media screen and (max-width: 1000px) { .popup .popup-body { - height: auto; + height: fit-content; + max-height: 95vh; + max-height: 95svh; } } From e98f38b6dacbb5ef809ea64d41e4e032ad63ff39 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 28 Jun 2024 21:27:48 +0300 Subject: [PATCH 062/181] Final(?) iOS cope --- public/css/popup-safari-fix.css | 8 ++++++++ public/css/popup.css | 9 ++------- public/scripts/RossAscends-mods.js | 4 ++++ 3 files changed, 14 insertions(+), 7 deletions(-) create mode 100644 public/css/popup-safari-fix.css diff --git a/public/css/popup-safari-fix.css b/public/css/popup-safari-fix.css new file mode 100644 index 000000000..1dc5085ff --- /dev/null +++ b/public/css/popup-safari-fix.css @@ -0,0 +1,8 @@ +/* iPhone copium land */ +@media screen and (max-width: 1000px) { + .ios .popup .popup-body { + height: fit-content; + max-height: 95vh; + max-height: 95svh; + } +} diff --git a/public/css/popup.css b/public/css/popup.css index b7a32ec89..6a000efa2 100644 --- a/public/css/popup.css +++ b/public/css/popup.css @@ -1,3 +1,5 @@ +@import url('./popup-safari-fix.css'); + dialog { color: var(--SmartThemeBodyColor); } @@ -171,10 +173,3 @@ body.no-blur .popup[open]::backdrop { backface-visibility: hidden; } -@media screen and (max-width: 1000px) { - .popup .popup-body { - height: fit-content; - max-height: 95vh; - max-height: 95svh; - } -} diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index d8e652465..4dc86c9d6 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -725,6 +725,10 @@ export function initRossMods() { RA_autoconnect(); } + if (getParsedUA()?.os?.name === 'iOS') { + document.body.classList.add('ios'); + } + $('#main_api').change(function () { var PrevAPI = main_api; setTimeout(() => RA_autoconnect(PrevAPI), 100); From c34150fef016f39c0dd520032810ca66ffcb778f Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 28 Jun 2024 21:32:39 +0300 Subject: [PATCH 063/181] Limit height to match large popup --- public/css/popup-safari-fix.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/css/popup-safari-fix.css b/public/css/popup-safari-fix.css index 1dc5085ff..017486a22 100644 --- a/public/css/popup-safari-fix.css +++ b/public/css/popup-safari-fix.css @@ -2,7 +2,7 @@ @media screen and (max-width: 1000px) { .ios .popup .popup-body { height: fit-content; - max-height: 95vh; - max-height: 95svh; + max-height: 90vh; + max-height: 90svh; } } From 38792d071b580fba06c446c0b962b59f42e60a0d Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Fri, 28 Jun 2024 20:48:55 +0200 Subject: [PATCH 064/181] Remove variable usage of 'at' arg in send commands --- public/scripts/slash-commands.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index fb05d15bf..c595175d2 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -178,8 +178,8 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ SlashCommandNamedArgument.fromProps({ name: 'at', description: 'position to insert the message', - typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], - enumProvider: commonEnumProviders.messages({ allowIdAfter: true, allowVars: true }), + typeList: [ARGUMENT_TYPE.NUMBER], + enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), }), ], unnamedArgumentList: [ @@ -221,8 +221,8 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ SlashCommandNamedArgument.fromProps({ name: 'at', description: 'position to insert the message', - typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], - enumProvider: commonEnumProviders.messages({ allowIdAfter: true, allowVars: true }), + typeList: [ARGUMENT_TYPE.NUMBER], + enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), }), ], unnamedArgumentList: [ @@ -275,8 +275,8 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ SlashCommandNamedArgument.fromProps({ name: 'at', description: 'position to insert the message', - typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], - enumProvider: commonEnumProviders.messages({ allowIdAfter: true, allowVars: true }), + typeList: [ARGUMENT_TYPE.NUMBER], + enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), }), ], unnamedArgumentList: [ @@ -460,8 +460,8 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ SlashCommandNamedArgument.fromProps({ name: 'at', description: 'position to insert the message', - typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], - enumProvider: commonEnumProviders.messages({ allowIdAfter: true, allowVars: true }), + typeList: [ARGUMENT_TYPE.NUMBER], + enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), }), SlashCommandNamedArgument.fromProps({ name: 'name', @@ -2428,7 +2428,7 @@ async function sendUserMessageCallback(args, text) { text = text.trim(); const compact = isTrueBoolean(args?.compact); const bias = extractMessageBias(text); - const insertAt = Number(resolveVariable(args?.at)); + const insertAt = Number(args?.at); if ('name' in args) { const name = args.name || ''; @@ -2737,7 +2737,7 @@ export async function sendMessageAs(args, text) { }, }]; - const insertAt = Number(resolveVariable(args.at)); + const insertAt = Number(args.at); if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) { chat.splice(insertAt, 0, message); @@ -2784,7 +2784,7 @@ export async function sendNarratorMessage(args, text) { }, }; - const insertAt = Number(resolveVariable(args.at)); + const insertAt = Number(args.at); if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) { chat.splice(insertAt, 0, message); @@ -2866,7 +2866,7 @@ async function sendCommentMessage(args, text) { }, }; - const insertAt = Number(resolveVariable(args.at)); + const insertAt = Number(args.at); if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) { chat.splice(insertAt, 0, message); From be08e62fc1ae56f23cbbc6802e417cb5c931e36f Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Fri, 28 Jun 2024 23:01:54 +0200 Subject: [PATCH 065/181] Observer to find new elements with i18n attribute --- public/scripts/i18n.js | 70 ++++++++++++++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 17 deletions(-) diff --git a/public/scripts/i18n.js b/public/scripts/i18n.js index df86a3c26..1a1f9135d 100644 --- a/public/scripts/i18n.js +++ b/public/scripts/i18n.js @@ -9,6 +9,28 @@ const langs = await fetch('/locales/lang.json').then(response => response.json() // eslint-disable-next-line prefer-const var localeData = await getLocaleData(localeFile); +/** + * An observer that will check if any new i18n elements are added to the document + * @type {MutationObserver} + */ +const observer = new MutationObserver(mutations => { + mutations.forEach(mutation => { + mutation.addedNodes.forEach(node => { + if (node.nodeType === Node.ELEMENT_NODE && node instanceof Element) { + if (node.hasAttribute('data-i18n')) { + translateElement(node); + } + node.querySelectorAll('[data-i18n]').forEach(element => { + translateElement(element); + }); + } + }); + if (mutation.attributeName === 'data-i18n' && mutation.target instanceof Element) { + translateElement(mutation.target); + } + }); +}); + /** * Fetches the locale data for the given language. * @param {string} language Language code @@ -40,6 +62,29 @@ function findLang(language) { return supportedLang; } +/** + * Translates a given element based on its data-i18n attribute. + * @param {Element} element The element to translate + */ +function translateElement(element) { + const keys = element.getAttribute('data-i18n').split(';'); // Multi-key entries are ; delimited + for (const key of keys) { + const attributeMatch = key.match(/\[(\S+)\](.+)/); // [attribute]key + if (attributeMatch) { // attribute-tagged key + const localizedValue = localeData?.[attributeMatch[2]]; + if (localizedValue || localizedValue === '') { + element.setAttribute(attributeMatch[1], localizedValue); + } + } else { // No attribute tag, treat as 'text' + const localizedValue = localeData?.[key]; + if (localizedValue || localizedValue === '') { + element.textContent = localizedValue; + } + } + } +} + + async function getMissingTranslations() { const missingData = []; @@ -103,22 +148,7 @@ export function applyLocale(root = document) { //find all the elements with `data-i18n` attribute $root.find('[data-i18n]').each(function () { - //read the translation from the language data - const keys = $(this).data('i18n').split(';'); // Multi-key entries are ; delimited - for (const key of keys) { - const attributeMatch = key.match(/\[(\S+)\](.+)/); // [attribute]key - if (attributeMatch) { // attribute-tagged key - const localizedValue = localeData?.[attributeMatch[2]]; - if (localizedValue || localizedValue == '') { - $(this).attr(attributeMatch[1], localizedValue); - } - } else { // No attribute tag, treat as 'text' - const localizedValue = localeData?.[key]; - if (localizedValue || localizedValue == '') { - $(this).text(localizedValue); - } - } - } + translateElement(this); }); if (root !== document) { @@ -126,7 +156,6 @@ export function applyLocale(root = document) { } } - function addLanguagesToDropdown() { const uiLanguageSelects = $('#ui_language_select, #onboarding_ui_language_select'); for (const langObj of langs) { // Set the value to the language code @@ -159,6 +188,13 @@ export function initLocales() { location.reload(); }); + observer.observe(document, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['data-i18n'], + }); + registerDebugFunction('getMissingTranslations', 'Get missing translations', 'Detects missing localization data in the current locale and dumps the data into the browser console. If the current locale is English, searches all other locales.', getMissingTranslations); registerDebugFunction('applyLocale', 'Apply locale', 'Reapplies the currently selected locale to the page.', applyLocale); } From 003066a036f8640d875b44230b32b585e82445fc Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 29 Jun 2024 00:33:12 +0300 Subject: [PATCH 066/181] Add vLLM as multimodal captioning source --- public/scripts/extensions/caption/index.js | 1 + public/scripts/extensions/caption/settings.html | 2 ++ public/scripts/extensions/shared.js | 17 +++++++++++++++++ src/endpoints/openai.js | 8 ++++++-- 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/public/scripts/extensions/caption/index.js b/public/scripts/extensions/caption/index.js index e06449f88..543eef7f6 100644 --- a/public/scripts/extensions/caption/index.js +++ b/public/scripts/extensions/caption/index.js @@ -356,6 +356,7 @@ jQuery(async function () { (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'llamacpp' && textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]) || (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ooba' && textgenerationwebui_settings.server_urls[textgen_types.OOBA]) || (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'koboldcpp' && textgenerationwebui_settings.server_urls[textgen_types.KOBOLDCPP]) || + (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'vllm' && textgenerationwebui_settings.server_urls[textgen_types.VLLM]) || (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'custom') || extension_settings.caption.source === 'local' || extension_settings.caption.source === 'horde'; diff --git a/public/scripts/extensions/caption/settings.html b/public/scripts/extensions/caption/settings.html index 3e23cfbd5..90ff673ba 100644 --- a/public/scripts/extensions/caption/settings.html +++ b/public/scripts/extensions/caption/settings.html @@ -26,6 +26,7 @@ +
@@ -66,6 +67,7 @@ +
diff --git a/public/scripts/extensions/shared.js b/public/scripts/extensions/shared.js index 4a4390318..3e671c3af 100644 --- a/public/scripts/extensions/shared.js +++ b/public/scripts/extensions/shared.js @@ -34,6 +34,7 @@ export async function getMultimodalCaption(base64Img, prompt) { const isCustom = extension_settings.caption.multimodal_api === 'custom'; const isOoba = extension_settings.caption.multimodal_api === 'ooba'; const isKoboldCpp = extension_settings.caption.multimodal_api === 'koboldcpp'; + const isVllm = extension_settings.caption.multimodal_api === 'vllm'; const base64Bytes = base64Img.length * 0.75; const compressionLimit = 2 * 1024 * 1024; if ((['google', 'openrouter'].includes(extension_settings.caption.multimodal_api) && base64Bytes > compressionLimit) || isOoba || isKoboldCpp) { @@ -65,6 +66,14 @@ export async function getMultimodalCaption(base64Img, prompt) { requestBody.server_url = textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]; } + if (isVllm) { + if (extension_settings.caption.multimodal_model === 'vllm_current') { + requestBody.model = textgenerationwebui_settings.vllm_model; + } + + requestBody.server_url = textgenerationwebui_settings.server_urls[textgen_types.VLLM]; + } + if (isLlamaCpp) { requestBody.server_url = textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]; } @@ -151,6 +160,14 @@ function throwIfInvalidModel(useReverseProxy) { throw new Error('KoboldCpp server URL is not set.'); } + if (extension_settings.caption.multimodal_api === 'vllm' && !textgenerationwebui_settings.server_urls[textgen_types.VLLM]) { + throw new Error('vLLM server URL is not set.'); + } + + if (extension_settings.caption.multimodal_api === 'vllm' && extension_settings.caption.multimodal_model === 'vllm_current' && !textgenerationwebui_settings.vllm_model) { + throw new Error('vLLM model is not set.'); + } + if (extension_settings.caption.multimodal_api === 'custom' && !oai_settings.custom_url) { throw new Error('Custom API URL is not set.'); } diff --git a/src/endpoints/openai.js b/src/endpoints/openai.js index 75de75b7b..f63d72b11 100644 --- a/src/endpoints/openai.js +++ b/src/endpoints/openai.js @@ -43,7 +43,11 @@ router.post('/caption-image', jsonParser, async (request, response) => { key = readSecret(request.user.directories, SECRET_KEYS.KOBOLDCPP); } - if (!key && !request.body.reverse_proxy && ['custom', 'ooba', 'koboldcpp'].includes(request.body.api) === false) { + if (request.body.api === 'vllm') { + key = readSecret(request.user.directories, SECRET_KEYS.VLLM); + } + + if (!key && !request.body.reverse_proxy && ['custom', 'ooba', 'koboldcpp', 'vllm'].includes(request.body.api) === false) { console.log('No key found for API', request.body.api); return response.sendStatus(400); } @@ -110,7 +114,7 @@ router.post('/caption-image', jsonParser, async (request, response) => { }); } - if (request.body.api === 'koboldcpp') { + if (request.body.api === 'koboldcpp' || request.body.api === 'vllm') { apiUrl = `${trimV1(request.body.server_url)}/v1/chat/completions`; } From d99452854835696baf8e7fe898723c92633af13e Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Fri, 28 Jun 2024 23:53:25 +0200 Subject: [PATCH 067/181] Extend i18n with translate and template literal --- public/script.js | 4 +++- public/scripts/i18n.js | 43 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/public/script.js b/public/script.js index 8c9f10c6c..0f435b9e4 100644 --- a/public/script.js +++ b/public/script.js @@ -209,7 +209,7 @@ import { instruct_presets, selectContextPreset, } from './scripts/instruct-mode.js'; -import { initLocales } from './scripts/i18n.js'; +import { initLocales, t, translate } from './scripts/i18n.js'; import { getFriendlyTokenizerName, getTokenCount, getTokenCountAsync, getTokenizerModel, initTokenizers, saveTokenCache } from './scripts/tokenizers.js'; import { user_avatar, @@ -7825,6 +7825,8 @@ window['SillyTavern'].getContext = function () { messageFormatting: messageFormatting, shouldSendOnEnter: shouldSendOnEnter, isMobile: isMobile, + t: t, + translate: translate, tags: tags, tagMap: tag_map, menuType: menu_type, diff --git a/public/scripts/i18n.js b/public/scripts/i18n.js index 1a1f9135d..d1ed2f3ea 100644 --- a/public/scripts/i18n.js +++ b/public/scripts/i18n.js @@ -31,6 +31,49 @@ const observer = new MutationObserver(mutations => { }); }); +/** + * Translates a template string with named arguments + * + * Uses the template literal with all values replaced by index placeholder for translation key. + * + * @example + * ```js + * toastr.warn(t`Tag ${tagName} not found.`); + * ``` + * Should be translated in the translation files as: + * ``` + * Tag ${0} not found. -> Tag ${0} nicht gefunden. + * ``` + * + * @param {TemplateStringsArray} strings - Template strings array + * @param {...any} values - Values for placeholders in the template string + * @returns {string} Translated and formatted string + */ +export function t(strings, ...values) { + let str = strings.reduce((result, string, i) => result + string + (values[i] !== undefined ? `\${${i}}` : ''), ''); + let translatedStr = translate(str); + + // Replace indexed placeholders with actual values + return translatedStr.replace(/\$\{(\d+)\}/g, (match, index) => values[index]); +} + +/** + * Translates a given key or text + * + * If the translation is based on a key, that one is used to find a possible translation in the translation file. + * The original text still has to be provided, as that is the default value being returned if no translation is found. + * + * For in-code text translation on a format string, using the template literal `t` is preferred. + * + * @param {string} text - The text to translate + * @param {string?} key - The key to use for translation. If not provided, text is used as the key. + * @returns {string} - The translated text + */ +export function translate(text, key = null) { + const translationKey = key || text; + return localeData?.[translationKey] || text; +} + /** * Fetches the locale data for the given language. * @param {string} language Language code From cba2b54531293167424a7f4f17f3ba979cbe9a0a Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sat, 29 Jun 2024 00:25:10 +0200 Subject: [PATCH 068/181] Refactor main slassh-commands into init func --- public/script.js | 3 +- public/scripts/slash-commands.js | 1931 +++++++++++++++--------------- 2 files changed, 968 insertions(+), 966 deletions(-) diff --git a/public/script.js b/public/script.js index 8c9f10c6c..4534e12d6 100644 --- a/public/script.js +++ b/public/script.js @@ -159,7 +159,7 @@ import { import { debounce_timeout } from './scripts/constants.js'; import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, loadExtensionSettings, renderExtensionTemplate, renderExtensionTemplateAsync, runGenerationInterceptors, saveMetadataDebounced, writeExtensionField } from './scripts/extensions.js'; -import { COMMENT_NAME_DEFAULT, executeSlashCommands, executeSlashCommandsOnChatInput, getSlashCommandsHelp, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommands, registerSlashCommand, stopScriptExecution } from './scripts/slash-commands.js'; +import { COMMENT_NAME_DEFAULT, executeSlashCommands, executeSlashCommandsOnChatInput, getSlashCommandsHelp, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommands, registerSlashCommand, stopScriptExecution } from './scripts/slash-commands.js'; import { tag_map, tags, @@ -911,6 +911,7 @@ async function firstLoadInit() { initKeyboard(); initDynamicStyles(); initTags(); + initDefaultSlashCommands(); await getUserAvatars(true, user_avatar); await getCharacters(); await getBackgrounds(); diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index c595175d2..3573453bd 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -38,13 +38,13 @@ import { system_message_types, this_chid, } from '../script.js'; -import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js'; +import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { SlashCommandParserError } from './slash-commands/SlashCommandParserError.js'; import { getMessageTimeStamp } from './RossAscends-mods.js'; import { hideChatMessageRange } from './chats.js'; -import { extension_settings, getContext, saveMetadataDebounced } from './extensions.js'; +import { getContext, saveMetadataDebounced } from './extensions.js'; import { getRegexedString, regex_placement } from './extensions/regex/engine.js'; -import { findGroupMemberId, getGroupMembers, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group } from './group-chats.js'; +import { findGroupMemberId, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group } from './group-chats.js'; import { chat_completion_sources, oai_settings, setupChatCompletionPromptManager } from './openai.js'; import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockState, togglePersonaLock, user_avatar } from './personas.js'; import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js'; @@ -53,7 +53,6 @@ import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCoun import { debounce, delay, isFalseBoolean, isTrueBoolean, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js'; import { registerVariableCommands, resolveVariable } from './variables.js'; import { background_settings } from './backgrounds.js'; -import { SlashCommandScope } from './slash-commands/SlashCommandScope.js'; import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; @@ -75,78 +74,79 @@ export const parser = new SlashCommandParser(); const registerSlashCommand = SlashCommandParser.addCommand.bind(SlashCommandParser); const getSlashCommandsHelp = parser.getHelpString.bind(parser); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: '?', - callback: helpCommandCallback, - aliases: ['help'], - unnamedArgumentList: [SlashCommandArgument.fromProps({ - description: 'help topic', - typeList: [ARGUMENT_TYPE.STRING], - enumList: [ - new SlashCommandEnumValue('slash', 'slash commands (STscript)', enumTypes.command, '/'), - new SlashCommandEnumValue('macros', '{{macros}} (text replacement)', enumTypes.macro, enumIcons.macro), - new SlashCommandEnumValue('format', 'chat/text formatting', enumTypes.name, '★'), - new SlashCommandEnumValue('hotkeys', 'keyboard shortcuts', enumTypes.enum, '⏎'), +export function initDefaultSlashCommands() { + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: '?', + callback: helpCommandCallback, + aliases: ['help'], + unnamedArgumentList: [SlashCommandArgument.fromProps({ + description: 'help topic', + typeList: [ARGUMENT_TYPE.STRING], + enumList: [ + new SlashCommandEnumValue('slash', 'slash commands (STscript)', enumTypes.command, '/'), + new SlashCommandEnumValue('macros', '{{macros}} (text replacement)', enumTypes.macro, enumIcons.macro), + new SlashCommandEnumValue('format', 'chat/text formatting', enumTypes.name, '★'), + new SlashCommandEnumValue('hotkeys', 'keyboard shortcuts', enumTypes.enum, '⏎'), + ], + })], + helpString: 'Get help on macros, chat formatting and commands.', + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'persona', + callback: setNameCallback, + namedArgumentList: [ + new SlashCommandNamedArgument( + 'mode', 'The mode for persona selection. ("lookup" = search for existing persona, "temp" = create a temporary name, set a temporary name, "all" = allow both in the same command)', + [ARGUMENT_TYPE.STRING], false, false, 'all', ['lookup', 'temp', 'all'], + ), ], - })], - helpString: 'Get help on macros, chat formatting and commands.', -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'persona', - callback: setNameCallback, - namedArgumentList: [ - new SlashCommandNamedArgument( - 'mode', 'The mode for persona selection. ("lookup" = search for existing persona, "temp" = create a temporary name, set a temporary name, "all" = allow both in the same command)', - [ARGUMENT_TYPE.STRING], false, false, 'all', ['lookup', 'temp', 'all'], - ), - ], - unnamedArgumentList: [ - SlashCommandArgument.fromProps({ - description: 'persona name', - typeList: [ARGUMENT_TYPE.STRING], - isRequired: true, - enumProvider: commonEnumProviders.personas, - }), - ], - helpString: 'Selects the given persona with its name and avatar (by name or avatar url). If no matching persona exists, applies a temporary name.', - aliases: ['name'], -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'sync', - callback: syncCallback, - helpString: 'Syncs the user persona in user-attributed messages in the current chat.', -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'lock', - callback: lockPersonaCallback, - aliases: ['bind'], - helpString: 'Locks/unlocks a persona (name and avatar) to the current chat', - unnamedArgumentList: [ - SlashCommandArgument.fromProps({ - description: 'state', - typeList: [ARGUMENT_TYPE.STRING], - isRequired: true, - defaultValue: 'toggle', - enumProvider: commonEnumProviders.boolean('onOffToggle'), - }), - ], -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'bg', - callback: setBackgroundCallback, - aliases: ['background'], - returns: 'the current background', - unnamedArgumentList: [ - SlashCommandArgument.fromProps({ - description: 'filename', - typeList: [ARGUMENT_TYPE.STRING], - isRequired: true, - enumProvider: () => [...document.querySelectorAll('.bg_example')] - .map(it => new SlashCommandEnumValue(it.getAttribute('bgfile'))) - .filter(it => it.value?.length), - }), - ], - helpString: ` + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'persona name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: commonEnumProviders.personas, + }), + ], + helpString: 'Selects the given persona with its name and avatar (by name or avatar url). If no matching persona exists, applies a temporary name.', + aliases: ['name'], + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'sync', + callback: syncCallback, + helpString: 'Syncs the user persona in user-attributed messages in the current chat.', + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'lock', + callback: lockPersonaCallback, + aliases: ['bind'], + helpString: 'Locks/unlocks a persona (name and avatar) to the current chat', + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'state', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + defaultValue: 'toggle', + enumProvider: commonEnumProviders.boolean('onOffToggle'), + }), + ], + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'bg', + callback: setBackgroundCallback, + aliases: ['background'], + returns: 'the current background', + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'filename', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: () => [...document.querySelectorAll('.bg_example')] + .map(it => new SlashCommandEnumValue(it.getAttribute('bgfile'))) + .filter(it => it.value?.length), + }), + ], + helpString: `
Sets a background according to the provided filename. Partial names allowed.
@@ -159,35 +159,35 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
`, -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'sendas', - callback: sendMessageAs, - namedArgumentList: [ - SlashCommandNamedArgument.fromProps({ - name: 'name', - description: 'Character name', - typeList: [ARGUMENT_TYPE.STRING], - isRequired: true, - enumProvider: commonEnumProviders.characters('character'), - forceEnum: false, - }), - new SlashCommandNamedArgument( - 'compact', 'Use compact layout', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', - ), - SlashCommandNamedArgument.fromProps({ - name: 'at', - description: 'position to insert the message', - typeList: [ARGUMENT_TYPE.NUMBER], - enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), - }), - ], - unnamedArgumentList: [ - new SlashCommandArgument( - 'text', [ARGUMENT_TYPE.STRING], true, - ), - ], - helpString: ` + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'sendas', + callback: sendMessageAs, + namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'name', + description: 'Character name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: commonEnumProviders.characters('character'), + forceEnum: false, + }), + new SlashCommandNamedArgument( + 'compact', 'Use compact layout', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', + ), + SlashCommandNamedArgument.fromProps({ + name: 'at', + description: 'position to insert the message', + typeList: [ARGUMENT_TYPE.NUMBER], + enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), + }), + ], + unnamedArgumentList: [ + new SlashCommandArgument( + 'text', [ARGUMENT_TYPE.STRING], true, + ), + ], + helpString: `
Sends a message as a specific character. Uses the character avatar if it exists in the characters list.
@@ -204,33 +204,33 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ If "compact" is set to true, the message is sent using a compact layout.
`, -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'sys', - callback: sendNarratorMessage, - aliases: ['nar'], - namedArgumentList: [ - new SlashCommandNamedArgument( - 'compact', - 'compact layout', - [ARGUMENT_TYPE.BOOLEAN], - false, - false, - 'false', - ), - SlashCommandNamedArgument.fromProps({ - name: 'at', - description: 'position to insert the message', - typeList: [ARGUMENT_TYPE.NUMBER], - enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), - }), - ], - unnamedArgumentList: [ - new SlashCommandArgument( - 'text', [ARGUMENT_TYPE.STRING], true, - ), - ], - helpString: ` + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'sys', + callback: sendNarratorMessage, + aliases: ['nar'], + namedArgumentList: [ + new SlashCommandNamedArgument( + 'compact', + 'compact layout', + [ARGUMENT_TYPE.BOOLEAN], + false, + false, + 'false', + ), + SlashCommandNamedArgument.fromProps({ + name: 'at', + description: 'position to insert the message', + typeList: [ARGUMENT_TYPE.NUMBER], + enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), + }), + ], + unnamedArgumentList: [ + new SlashCommandArgument( + 'text', [ARGUMENT_TYPE.STRING], true, + ), + ], + helpString: `
Sends a message as a system narrator.
@@ -249,44 +249,44 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ `, -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'sysname', - callback: setNarratorName, - unnamedArgumentList: [ - new SlashCommandArgument( - 'name', [ARGUMENT_TYPE.STRING], false, - ), - ], - helpString: 'Sets a name for future system narrator messages in this chat (display only). Default: System. Leave empty to reset.', -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'comment', - callback: sendCommentMessage, - namedArgumentList: [ - new SlashCommandNamedArgument( - 'compact', - 'Whether to use a compact layout', - [ARGUMENT_TYPE.BOOLEAN], - false, - false, - 'false', - ), - SlashCommandNamedArgument.fromProps({ - name: 'at', - description: 'position to insert the message', - typeList: [ARGUMENT_TYPE.NUMBER], - enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), - }), - ], - unnamedArgumentList: [ - new SlashCommandArgument( - 'text', - [ARGUMENT_TYPE.STRING], - true, - ), - ], - helpString: ` + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'sysname', + callback: setNarratorName, + unnamedArgumentList: [ + new SlashCommandArgument( + 'name', [ARGUMENT_TYPE.STRING], false, + ), + ], + helpString: 'Sets a name for future system narrator messages in this chat (display only). Default: System. Leave empty to reset.', + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'comment', + callback: sendCommentMessage, + namedArgumentList: [ + new SlashCommandNamedArgument( + 'compact', + 'Whether to use a compact layout', + [ARGUMENT_TYPE.BOOLEAN], + false, + false, + 'false', + ), + SlashCommandNamedArgument.fromProps({ + name: 'at', + description: 'position to insert the message', + typeList: [ARGUMENT_TYPE.NUMBER], + enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), + }), + ], + unnamedArgumentList: [ + new SlashCommandArgument( + 'text', + [ARGUMENT_TYPE.STRING], + true, + ), + ], + helpString: `
Adds a note/comment message not part of the chat.
@@ -305,35 +305,35 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ `, -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'single', - callback: setStoryModeCallback, - aliases: ['story'], - helpString: 'Sets the message style to single document mode without names or avatars visible.', -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'bubble', - callback: setBubbleModeCallback, - aliases: ['bubbles'], - helpString: 'Sets the message style to bubble chat mode.', -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'flat', - callback: setFlatModeCallback, - aliases: ['default'], - helpString: 'Sets the message style to flat chat mode.', -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'continue', - callback: continueChatCallback, - aliases: ['cont'], - unnamedArgumentList: [ - new SlashCommandArgument( - 'prompt', [ARGUMENT_TYPE.STRING], false, - ), - ], - helpString: ` + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'single', + callback: setStoryModeCallback, + aliases: ['story'], + helpString: 'Sets the message style to single document mode without names or avatars visible.', + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'bubble', + callback: setBubbleModeCallback, + aliases: ['bubbles'], + helpString: 'Sets the message style to bubble chat mode.', + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'flat', + callback: setFlatModeCallback, + aliases: ['default'], + helpString: 'Sets the message style to flat chat mode.', + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'continue', + callback: continueChatCallback, + aliases: ['cont'], + unnamedArgumentList: [ + new SlashCommandArgument( + 'prompt', [ARGUMENT_TYPE.STRING], false, + ), + ], + helpString: `
Continues the last message in the chat, with an optional additional prompt.
@@ -351,87 +351,87 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ `, -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'go', - callback: goToCharacterCallback, - unnamedArgumentList: [ - SlashCommandArgument.fromProps({ - description: 'name', - typeList: [ARGUMENT_TYPE.STRING], - isRequired: true, - enumProvider: commonEnumProviders.characters('all'), - }), - ], - helpString: 'Opens up a chat with the character or group by its name', - aliases: ['char'], -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'rename-char', - /** @param {{silent: string, chats: string}} options @param {string} name */ - callback: async ({ silent = 'true', chats = null }, name) => { - const renamed = await renameCharacter(name, { silent: isTrueBoolean(silent), renameChats: chats !== null ? isTrueBoolean(chats) : null }); - return String(renamed); - }, - returns: 'true/false - Whether the rename was successful', - namedArgumentList: [ - new SlashCommandNamedArgument( - 'silent', 'Hide any blocking popups. (if false, the name is optional. If not supplied, a popup asking for it will appear)', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true', - ), - new SlashCommandNamedArgument( - 'chats', 'Rename char in all previous chats', [ARGUMENT_TYPE.BOOLEAN], false, false, '', - ), - ], - unnamedArgumentList: [ - new SlashCommandArgument( - 'new char name', [ARGUMENT_TYPE.STRING], true, - ), - ], - helpString: 'Renames the current character.', -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'sysgen', - callback: generateSystemMessage, - unnamedArgumentList: [ - new SlashCommandArgument( - 'prompt', [ARGUMENT_TYPE.STRING], true, - ), - ], - helpString: 'Generates a system message using a specified prompt.', -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'ask', - callback: askCharacter, - namedArgumentList: [ - SlashCommandNamedArgument.fromProps({ - name: 'name', - description: 'character name', - typeList: [ARGUMENT_TYPE.STRING], - isRequired: true, - enumProvider: commonEnumProviders.characters('character'), - }), - ], - unnamedArgumentList: [ - new SlashCommandArgument( - 'prompt', [ARGUMENT_TYPE.STRING], true, false, - ), - ], - helpString: 'Asks a specified character card a prompt. Character name must be provided in a named argument.', -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'delname', - callback: deleteMessagesByNameCallback, - namedArgumentList: [], - unnamedArgumentList: [ - SlashCommandArgument.fromProps({ - description: 'name', - typeList: [ARGUMENT_TYPE.STRING], - isRequired: true, - enumProvider: commonEnumProviders.characters('character'), - }), - ], - aliases: ['cancel'], - helpString: ` + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'go', + callback: goToCharacterCallback, + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: commonEnumProviders.characters('all'), + }), + ], + helpString: 'Opens up a chat with the character or group by its name', + aliases: ['char'], + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'rename-char', + /** @param {{silent: string, chats: string}} options @param {string} name */ + callback: async ({ silent = 'true', chats = null }, name) => { + const renamed = await renameCharacter(name, { silent: isTrueBoolean(silent), renameChats: chats !== null ? isTrueBoolean(chats) : null }); + return String(renamed); + }, + returns: 'true/false - Whether the rename was successful', + namedArgumentList: [ + new SlashCommandNamedArgument( + 'silent', 'Hide any blocking popups. (if false, the name is optional. If not supplied, a popup asking for it will appear)', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true', + ), + new SlashCommandNamedArgument( + 'chats', 'Rename char in all previous chats', [ARGUMENT_TYPE.BOOLEAN], false, false, '', + ), + ], + unnamedArgumentList: [ + new SlashCommandArgument( + 'new char name', [ARGUMENT_TYPE.STRING], true, + ), + ], + helpString: 'Renames the current character.', + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'sysgen', + callback: generateSystemMessage, + unnamedArgumentList: [ + new SlashCommandArgument( + 'prompt', [ARGUMENT_TYPE.STRING], true, + ), + ], + helpString: 'Generates a system message using a specified prompt.', + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'ask', + callback: askCharacter, + namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'name', + description: 'character name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: commonEnumProviders.characters('character'), + }), + ], + unnamedArgumentList: [ + new SlashCommandArgument( + 'prompt', [ARGUMENT_TYPE.STRING], true, false, + ), + ], + helpString: 'Asks a specified character card a prompt. Character name must be provided in a named argument.', + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'delname', + callback: deleteMessagesByNameCallback, + namedArgumentList: [], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: commonEnumProviders.characters('character'), + }), + ], + aliases: ['cancel'], + helpString: `
Deletes all messages attributed to a specified name.
@@ -444,41 +444,41 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ `, -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'send', - callback: sendUserMessageCallback, - namedArgumentList: [ - new SlashCommandNamedArgument( - 'compact', - 'whether to use a compact layout', - [ARGUMENT_TYPE.BOOLEAN], - false, - false, - 'false', - ), - SlashCommandNamedArgument.fromProps({ - name: 'at', - description: 'position to insert the message', - typeList: [ARGUMENT_TYPE.NUMBER], - enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), - }), - SlashCommandNamedArgument.fromProps({ - name: 'name', - description: 'display name', - typeList: [ARGUMENT_TYPE.STRING], - defaultValue: '{{user}}', - enumProvider: commonEnumProviders.characters('character'), - }), - ], - unnamedArgumentList: [ - new SlashCommandArgument( - 'text', - [ARGUMENT_TYPE.STRING], - true, - ), - ], - helpString: ` + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'send', + callback: sendUserMessageCallback, + namedArgumentList: [ + new SlashCommandNamedArgument( + 'compact', + 'whether to use a compact layout', + [ARGUMENT_TYPE.BOOLEAN], + false, + false, + 'false', + ), + SlashCommandNamedArgument.fromProps({ + name: 'at', + description: 'position to insert the message', + typeList: [ARGUMENT_TYPE.NUMBER], + enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), + }), + SlashCommandNamedArgument.fromProps({ + name: 'name', + description: 'display name', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: '{{user}}', + enumProvider: commonEnumProviders.characters('character'), + }), + ], + unnamedArgumentList: [ + new SlashCommandArgument( + 'text', + [ARGUMENT_TYPE.STRING], + true, + ), + ], + helpString: `
Adds a user message to the chat log without triggering a generation.
@@ -500,29 +500,29 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ `, -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'trigger', - callback: triggerGenerationCallback, - namedArgumentList: [ - new SlashCommandNamedArgument( - 'await', - 'Whether to await for the triggered generation before continuing', - [ARGUMENT_TYPE.BOOLEAN], - false, - false, - 'false', - ), - ], - unnamedArgumentList: [ - SlashCommandArgument.fromProps({ - description: 'group member index (starts with 0) or name', - typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], - isRequired: false, - enumProvider: commonEnumProviders.groupMembers(), - }), - ], - helpString: ` + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'trigger', + callback: triggerGenerationCallback, + namedArgumentList: [ + new SlashCommandNamedArgument( + 'await', + 'Whether to await for the triggered generation before continuing', + [ARGUMENT_TYPE.BOOLEAN], + false, + false, + 'false', + ), + ], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'group member index (starts with 0) or name', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], + isRequired: false, + enumProvider: commonEnumProviders.groupMembers(), + }), + ], + helpString: `
Triggers a message generation. If in group, can trigger a message for the specified group member index or name.
@@ -530,74 +530,74 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ If await=true named argument is passed, the command will await for the triggered generation before continuing. `, -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'hide', - callback: hideMessageCallback, - unnamedArgumentList: [ - SlashCommandArgument.fromProps({ - description: 'message index (starts with 0) or range', - typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE], - isRequired: true, - enumProvider: commonEnumProviders.messages(), - }), - ], - helpString: 'Hides a chat message from the prompt.', -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'unhide', - callback: unhideMessageCallback, - unnamedArgumentList: [ - SlashCommandArgument.fromProps({ - description: 'message index (starts with 0) or range', - typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE], - isRequired: true, - enumProvider: commonEnumProviders.messages(), - }), - ], - helpString: 'Unhides a message from the prompt.', -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'member-disable', - callback: disableGroupMemberCallback, - aliases: ['disable', 'disablemember', 'memberdisable'], - unnamedArgumentList: [ - SlashCommandArgument.fromProps({ - description: 'member index (starts with 0) or name', - typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], - isRequired: true, - enumProvider: commonEnumProviders.groupMembers(), - }), - ], - helpString: 'Disables a group member from being drafted for replies.', -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'member-enable', - aliases: ['enable', 'enablemember', 'memberenable'], - callback: enableGroupMemberCallback, - unnamedArgumentList: [ - SlashCommandArgument.fromProps({ - description: 'member index (starts with 0) or name', - typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], - isRequired: true, - enumProvider: commonEnumProviders.groupMembers(), - }), - ], - helpString: 'Enables a group member to be drafted for replies.', -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'member-add', - callback: addGroupMemberCallback, - aliases: ['addmember', 'memberadd'], - unnamedArgumentList: [ - SlashCommandArgument.fromProps({ - description: 'character name', - typeList: [ARGUMENT_TYPE.STRING], - isRequired: true, - enumProvider: () => selected_group ? commonEnumProviders.characters('character')() : [], - }), - ], - helpString: ` + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'hide', + callback: hideMessageCallback, + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'message index (starts with 0) or range', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE], + isRequired: true, + enumProvider: commonEnumProviders.messages(), + }), + ], + helpString: 'Hides a chat message from the prompt.', + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'unhide', + callback: unhideMessageCallback, + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'message index (starts with 0) or range', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE], + isRequired: true, + enumProvider: commonEnumProviders.messages(), + }), + ], + helpString: 'Unhides a message from the prompt.', + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'member-disable', + callback: disableGroupMemberCallback, + aliases: ['disable', 'disablemember', 'memberdisable'], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'member index (starts with 0) or name', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: commonEnumProviders.groupMembers(), + }), + ], + helpString: 'Disables a group member from being drafted for replies.', + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'member-enable', + aliases: ['enable', 'enablemember', 'memberenable'], + callback: enableGroupMemberCallback, + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'member index (starts with 0) or name', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: commonEnumProviders.groupMembers(), + }), + ], + helpString: 'Enables a group member to be drafted for replies.', + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'member-add', + callback: addGroupMemberCallback, + aliases: ['addmember', 'memberadd'], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'character name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: () => selected_group ? commonEnumProviders.characters('character')() : [], + }), + ], + helpString: `
Adds a new group member to the group chat.
@@ -610,20 +610,20 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ `, -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'member-remove', - callback: removeGroupMemberCallback, - aliases: ['removemember', 'memberremove'], - unnamedArgumentList: [ - SlashCommandArgument.fromProps({ - description: 'member index (starts with 0) or name', - typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], - isRequired: true, - enumProvider: commonEnumProviders.groupMembers(), - }), - ], - helpString: ` + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'member-remove', + callback: removeGroupMemberCallback, + aliases: ['removemember', 'memberremove'], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'member index (starts with 0) or name', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: commonEnumProviders.groupMembers(), + }), + ], + helpString: `
Removes a group member from the group chat.
@@ -637,47 +637,47 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ `, -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'member-up', - callback: moveGroupMemberUpCallback, - aliases: ['upmember', 'memberup'], - unnamedArgumentList: [ - SlashCommandArgument.fromProps({ - description: 'member index (starts with 0) or name', - typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], - isRequired: true, - enumProvider: commonEnumProviders.groupMembers(), - }), - ], - helpString: 'Moves a group member up in the group chat list.', -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'member-down', - callback: moveGroupMemberDownCallback, - aliases: ['downmember', 'memberdown'], - unnamedArgumentList: [ - SlashCommandArgument.fromProps({ - description: 'member index (starts with 0) or name', - typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], - isRequired: true, - enumProvider: commonEnumProviders.groupMembers(), - }), - ], - helpString: 'Moves a group member down in the group chat list.', -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'peek', - callback: peekCallback, - unnamedArgumentList: [ - SlashCommandArgument.fromProps({ - description: 'member index (starts with 0) or name', - typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], - isRequired: true, - enumProvider: commonEnumProviders.groupMembers(), - }), - ], - helpString: ` + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'member-up', + callback: moveGroupMemberUpCallback, + aliases: ['upmember', 'memberup'], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'member index (starts with 0) or name', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: commonEnumProviders.groupMembers(), + }), + ], + helpString: 'Moves a group member up in the group chat list.', + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'member-down', + callback: moveGroupMemberDownCallback, + aliases: ['downmember', 'memberdown'], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'member index (starts with 0) or name', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: commonEnumProviders.groupMembers(), + }), + ], + helpString: 'Moves a group member down in the group chat list.', + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'peek', + callback: peekCallback, + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'member index (starts with 0) or name', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: commonEnumProviders.groupMembers(), + }), + ], + helpString: `
Shows a group member character card without switching chats.
@@ -691,22 +691,22 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ `, -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'delswipe', - callback: deleteSwipeCallback, - aliases: ['swipedel'], - unnamedArgumentList: [ - SlashCommandArgument.fromProps({ - description: '1-based swipe id', - typeList: [ARGUMENT_TYPE.NUMBER], - isRequired: true, - enumProvider: () => Array.isArray(chat[chat.length - 1]?.swipes) ? - chat[chat.length - 1].swipes.map((/** @type {string} */ swipe, /** @type {number} */ i) => new SlashCommandEnumValue(String(i + 1), swipe, enumTypes.enum, enumIcons.message)) - : [], - }), - ], - helpString: ` + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'delswipe', + callback: deleteSwipeCallback, + aliases: ['swipedel'], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: '1-based swipe id', + typeList: [ARGUMENT_TYPE.NUMBER], + isRequired: true, + enumProvider: () => Array.isArray(chat[chat.length - 1]?.swipes) ? + chat[chat.length - 1].swipes.map((/** @type {string} */ swipe, /** @type {number} */ i) => new SlashCommandEnumValue(String(i + 1), swipe, enumTypes.enum, enumIcons.message)) + : [], + }), + ], + helpString: `
Deletes a swipe from the last chat message. If swipe id is not provided, it deletes the current swipe.
@@ -724,34 +724,34 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ `, -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'echo', - callback: echoCallback, - returns: 'the text', - namedArgumentList: [ - new SlashCommandNamedArgument( - 'title', 'title of the toast message', [ARGUMENT_TYPE.STRING], false, - ), - SlashCommandNamedArgument.fromProps({ - name: 'severity', - description: 'severity level of the toast message', - typeList: [ARGUMENT_TYPE.STRING], - defaultValue: 'info', - enumProvider: () => [ - new SlashCommandEnumValue('info', 'info', enumTypes.macro, 'ℹ️'), - new SlashCommandEnumValue('warning', 'warning', enumTypes.enum, '⚠️'), - new SlashCommandEnumValue('error', 'error', enumTypes.enum, '❗'), - new SlashCommandEnumValue('success', 'success', enumTypes.enum, '✅'), - ], - }), - ], - unnamedArgumentList: [ - new SlashCommandArgument( - 'text', [ARGUMENT_TYPE.STRING], true, - ), - ], - helpString: ` + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'echo', + callback: echoCallback, + returns: 'the text', + namedArgumentList: [ + new SlashCommandNamedArgument( + 'title', 'title of the toast message', [ARGUMENT_TYPE.STRING], false, + ), + SlashCommandNamedArgument.fromProps({ + name: 'severity', + description: 'severity level of the toast message', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: 'info', + enumProvider: () => [ + new SlashCommandEnumValue('info', 'info', enumTypes.macro, 'ℹ️'), + new SlashCommandEnumValue('warning', 'warning', enumTypes.enum, '⚠️'), + new SlashCommandEnumValue('error', 'error', enumTypes.enum, '❗'), + new SlashCommandEnumValue('success', 'success', enumTypes.enum, '✅'), + ], + }), + ], + unnamedArgumentList: [ + new SlashCommandArgument( + 'text', [ARGUMENT_TYPE.STRING], true, + ), + ], + helpString: `
Echoes the provided text to a toast message. Useful for pipes debugging.
@@ -764,42 +764,42 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ `, -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'gen', - callback: generateCallback, - returns: 'generated text', - namedArgumentList: [ - new SlashCommandNamedArgument( - 'lock', 'lock user input during generation', [ARGUMENT_TYPE.BOOLEAN], false, false, null, commonEnumProviders.boolean('onOff')(), - ), - SlashCommandNamedArgument.fromProps({ - name: 'name', - description: 'in-prompt name for instruct mode', - typeList: [ARGUMENT_TYPE.STRING], - defaultValue: 'System', - enumProvider: () => [...commonEnumProviders.characters('character')(), new SlashCommandEnumValue('System', null, enumTypes.enum, enumIcons.assistant)], - forceEnum: false, - }), - new SlashCommandNamedArgument( - 'length', 'API response length in tokens', [ARGUMENT_TYPE.NUMBER], false, - ), - SlashCommandNamedArgument.fromProps({ - name: 'as', - description: 'role of the output prompt', - typeList: [ARGUMENT_TYPE.STRING], - enumList: [ - new SlashCommandEnumValue('system', null, enumTypes.enum, enumIcons.assistant), - new SlashCommandEnumValue('char', null, enumTypes.enum, enumIcons.character), - ], - }), - ], - unnamedArgumentList: [ - new SlashCommandArgument( - 'prompt', [ARGUMENT_TYPE.STRING], true, - ), - ], - helpString: ` + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'gen', + callback: generateCallback, + returns: 'generated text', + namedArgumentList: [ + new SlashCommandNamedArgument( + 'lock', 'lock user input during generation', [ARGUMENT_TYPE.BOOLEAN], false, false, null, commonEnumProviders.boolean('onOff')(), + ), + SlashCommandNamedArgument.fromProps({ + name: 'name', + description: 'in-prompt name for instruct mode', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: 'System', + enumProvider: () => [...commonEnumProviders.characters('character')(), new SlashCommandEnumValue('System', null, enumTypes.enum, enumIcons.assistant)], + forceEnum: false, + }), + new SlashCommandNamedArgument( + 'length', 'API response length in tokens', [ARGUMENT_TYPE.NUMBER], false, + ), + SlashCommandNamedArgument.fromProps({ + name: 'as', + description: 'role of the output prompt', + typeList: [ARGUMENT_TYPE.STRING], + enumList: [ + new SlashCommandEnumValue('system', null, enumTypes.enum, enumIcons.assistant), + new SlashCommandEnumValue('char', null, enumTypes.enum, enumIcons.character), + ], + }), + ], + unnamedArgumentList: [ + new SlashCommandArgument( + 'prompt', [ARGUMENT_TYPE.STRING], true, + ), + ], + helpString: `
Generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating and allowing to configure the in-prompt name for instruct mode (default = "System").
@@ -807,43 +807,43 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ "as" argument controls the role of the output prompt: system (default) or char. If "length" argument is provided as a number in tokens, allows to temporarily override an API response length. `, -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'genraw', - callback: generateRawCallback, - returns: 'generated text', - namedArgumentList: [ - new SlashCommandNamedArgument( - 'lock', 'lock user input during generation', [ARGUMENT_TYPE.BOOLEAN], false, false, null, commonEnumProviders.boolean('onOff')(), - ), - new SlashCommandNamedArgument( - 'instruct', 'use instruct mode', [ARGUMENT_TYPE.BOOLEAN], false, false, 'on', commonEnumProviders.boolean('onOff')(), - ), - new SlashCommandNamedArgument( - 'stop', 'one-time custom stop strings', [ARGUMENT_TYPE.LIST], false, - ), - SlashCommandNamedArgument.fromProps({ - name: 'as', - description: 'role of the output prompt', - typeList: [ARGUMENT_TYPE.STRING], - enumList: [ - new SlashCommandEnumValue('system', null, enumTypes.enum, enumIcons.assistant), - new SlashCommandEnumValue('char', null, enumTypes.enum, enumIcons.character), - ], - }), - new SlashCommandNamedArgument( - 'system', 'system prompt at the start', [ARGUMENT_TYPE.STRING], false, - ), - new SlashCommandNamedArgument( - 'length', 'API response length in tokens', [ARGUMENT_TYPE.NUMBER], false, - ), - ], - unnamedArgumentList: [ - new SlashCommandArgument( - 'prompt', [ARGUMENT_TYPE.STRING], true, - ), - ], - helpString: ` + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'genraw', + callback: generateRawCallback, + returns: 'generated text', + namedArgumentList: [ + new SlashCommandNamedArgument( + 'lock', 'lock user input during generation', [ARGUMENT_TYPE.BOOLEAN], false, false, null, commonEnumProviders.boolean('onOff')(), + ), + new SlashCommandNamedArgument( + 'instruct', 'use instruct mode', [ARGUMENT_TYPE.BOOLEAN], false, false, 'on', commonEnumProviders.boolean('onOff')(), + ), + new SlashCommandNamedArgument( + 'stop', 'one-time custom stop strings', [ARGUMENT_TYPE.LIST], false, + ), + SlashCommandNamedArgument.fromProps({ + name: 'as', + description: 'role of the output prompt', + typeList: [ARGUMENT_TYPE.STRING], + enumList: [ + new SlashCommandEnumValue('system', null, enumTypes.enum, enumIcons.assistant), + new SlashCommandEnumValue('char', null, enumTypes.enum, enumIcons.character), + ], + }), + new SlashCommandNamedArgument( + 'system', 'system prompt at the start', [ARGUMENT_TYPE.STRING], false, + ), + new SlashCommandNamedArgument( + 'length', 'API response length in tokens', [ARGUMENT_TYPE.NUMBER], false, + ), + ], + unnamedArgumentList: [ + new SlashCommandArgument( + 'prompt', [ARGUMENT_TYPE.STRING], true, + ), + ], + helpString: `
Generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating. Does not include chat history or character card.
@@ -860,55 +860,55 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ If "length" argument is provided as a number in tokens, allows to temporarily override an API response length. `, -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'addswipe', - callback: addSwipeCallback, - aliases: ['swipeadd'], - unnamedArgumentList: [ - new SlashCommandArgument( - 'text', [ARGUMENT_TYPE.STRING], true, - ), - ], - helpString: 'Adds a swipe to the last chat message.', -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'abort', - callback: abortCallback, - namedArgumentList: [ - SlashCommandNamedArgument.fromProps({ - name: 'quiet', - description: 'Whether to suppress the toast message notifying about the /abort call.', - typeList: [ARGUMENT_TYPE.BOOLEAN], - defaultValue: 'true', - }), - ], - unnamedArgumentList: [ - SlashCommandArgument.fromProps({ - description: 'The reason for aborting command execution. Shown when quiet=false', - typeList: [ARGUMENT_TYPE.STRING], - }), - ], - helpString: 'Aborts the slash command batch execution.', -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'fuzzy', - callback: fuzzyCallback, - returns: 'first matching item', - namedArgumentList: [ - new SlashCommandNamedArgument( - 'list', 'list of items to match against', [ARGUMENT_TYPE.LIST], true, - ), - new SlashCommandNamedArgument( - 'threshold', 'fuzzy match threshold (0.0 to 1.0)', [ARGUMENT_TYPE.NUMBER], false, false, '0.4', - ), - ], - unnamedArgumentList: [ - new SlashCommandArgument( - 'text to search', [ARGUMENT_TYPE.STRING], true, - ), - ], - helpString: ` + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'addswipe', + callback: addSwipeCallback, + aliases: ['swipeadd'], + unnamedArgumentList: [ + new SlashCommandArgument( + 'text', [ARGUMENT_TYPE.STRING], true, + ), + ], + helpString: 'Adds a swipe to the last chat message.', + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'abort', + callback: abortCallback, + namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'quiet', + description: 'Whether to suppress the toast message notifying about the /abort call.', + typeList: [ARGUMENT_TYPE.BOOLEAN], + defaultValue: 'true', + }), + ], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'The reason for aborting command execution. Shown when quiet=false', + typeList: [ARGUMENT_TYPE.STRING], + }), + ], + helpString: 'Aborts the slash command batch execution.', + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'fuzzy', + callback: fuzzyCallback, + returns: 'first matching item', + namedArgumentList: [ + new SlashCommandNamedArgument( + 'list', 'list of items to match against', [ARGUMENT_TYPE.LIST], true, + ), + new SlashCommandNamedArgument( + 'threshold', 'fuzzy match threshold (0.0 to 1.0)', [ARGUMENT_TYPE.NUMBER], false, false, '0.4', + ), + ], + unnamedArgumentList: [ + new SlashCommandArgument( + 'text to search', [ARGUMENT_TYPE.STRING], true, + ), + ], + helpString: `
Performs a fuzzy match of each item in the list against the text to search. If any item matches, then its name is returned. If no item matches the text, no value is returned. @@ -930,23 +930,23 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
`, -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'pass', - callback: (_, arg) => { - // We do not support arrays of closures. Arrays of strings will be send as JSON - if (Array.isArray(arg) && arg.some(x => x instanceof SlashCommandClosure)) throw new Error('Command /pass does not support multiple closures'); - if (Array.isArray(arg)) return JSON.stringify(arg); - return arg; - }, - returns: 'the provided value', - unnamedArgumentList: [ - new SlashCommandArgument( - 'text', [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.BOOLEAN, ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.DICTIONARY, ARGUMENT_TYPE.CLOSURE], true, - ), - ], - aliases: ['return'], - helpString: ` + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'pass', + callback: (_, arg) => { + // We do not support arrays of closures. Arrays of strings will be send as JSON + if (Array.isArray(arg) && arg.some(x => x instanceof SlashCommandClosure)) throw new Error('Command /pass does not support multiple closures'); + if (Array.isArray(arg)) return JSON.stringify(arg); + return arg; + }, + returns: 'the provided value', + unnamedArgumentList: [ + new SlashCommandArgument( + 'text', [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.BOOLEAN, ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.DICTIONARY, ARGUMENT_TYPE.CLOSURE], true, + ), + ], + aliases: ['return'], + helpString: `
/pass (text) – passes the text to the next command through the pipe.
@@ -957,17 +957,17 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ `, -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'delay', - callback: delayCallback, - aliases: ['wait', 'sleep'], - unnamedArgumentList: [ - new SlashCommandArgument( - 'milliseconds', [ARGUMENT_TYPE.NUMBER], true, - ), - ], - helpString: ` + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'delay', + callback: delayCallback, + aliases: ['wait', 'sleep'], + unnamedArgumentList: [ + new SlashCommandArgument( + 'milliseconds', [ARGUMENT_TYPE.NUMBER], true, + ), + ], + helpString: `
Delays the next command in the pipe by the specified number of milliseconds.
@@ -980,101 +980,101 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ `, -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'input', - aliases: ['prompt'], - callback: inputCallback, - returns: 'user input', - namedArgumentList: [ - new SlashCommandNamedArgument( - 'default', 'default value of the input field', [ARGUMENT_TYPE.STRING], false, false, '"string"', - ), - new SlashCommandNamedArgument( - 'large', 'show large input field', [ARGUMENT_TYPE.BOOLEAN], false, false, 'off', commonEnumProviders.boolean('onOff')(), - ), - new SlashCommandNamedArgument( - 'wide', 'show wide input field', [ARGUMENT_TYPE.BOOLEAN], false, false, 'off', commonEnumProviders.boolean('onOff')(), - ), - new SlashCommandNamedArgument( - 'okButton', 'text for the ok button', [ARGUMENT_TYPE.STRING], false, - ), - new SlashCommandNamedArgument( - 'rows', 'number of rows for the input field', [ARGUMENT_TYPE.NUMBER], false, - ), - ], - unnamedArgumentList: [ - new SlashCommandArgument( - 'text to display', [ARGUMENT_TYPE.STRING], false, - ), - ], - helpString: ` + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'input', + aliases: ['prompt'], + callback: inputCallback, + returns: 'user input', + namedArgumentList: [ + new SlashCommandNamedArgument( + 'default', 'default value of the input field', [ARGUMENT_TYPE.STRING], false, false, '"string"', + ), + new SlashCommandNamedArgument( + 'large', 'show large input field', [ARGUMENT_TYPE.BOOLEAN], false, false, 'off', commonEnumProviders.boolean('onOff')(), + ), + new SlashCommandNamedArgument( + 'wide', 'show wide input field', [ARGUMENT_TYPE.BOOLEAN], false, false, 'off', commonEnumProviders.boolean('onOff')(), + ), + new SlashCommandNamedArgument( + 'okButton', 'text for the ok button', [ARGUMENT_TYPE.STRING], false, + ), + new SlashCommandNamedArgument( + 'rows', 'number of rows for the input field', [ARGUMENT_TYPE.NUMBER], false, + ), + ], + unnamedArgumentList: [ + new SlashCommandArgument( + 'text to display', [ARGUMENT_TYPE.STRING], false, + ), + ], + helpString: `
Shows a popup with the provided text and an input field. The default argument is the default value of the input field, and the text argument is the text to display.
`, -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'run', - aliases: ['call', 'exec'], - callback: runCallback, - returns: 'result of the executed closure of QR', - namedArgumentList: [ - new SlashCommandNamedArgument( - 'args', 'named arguments', [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.BOOLEAN, ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.DICTIONARY], false, true, - ), - ], - unnamedArgumentList: [ - SlashCommandArgument.fromProps({ - description: 'scoped variable or qr label', - typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING], - isRequired: true, - enumProvider: () => [ - ...commonEnumProviders.variables('scope')(), - ...(typeof window['qrEnumProviderExecutables'] === 'function') ? window['qrEnumProviderExecutables']() : [], - ], - }), - ], - helpString: ` + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'run', + aliases: ['call', 'exec'], + callback: runCallback, + returns: 'result of the executed closure of QR', + namedArgumentList: [ + new SlashCommandNamedArgument( + 'args', 'named arguments', [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.BOOLEAN, ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.DICTIONARY], false, true, + ), + ], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'scoped variable or qr label', + typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: () => [ + ...commonEnumProviders.variables('scope')(), + ...(typeof window['qrEnumProviderExecutables'] === 'function') ? window['qrEnumProviderExecutables']() : [], + ], + }), + ], + helpString: `
Runs a closure from a scoped variable, or a Quick Reply with the specified name from a currently active preset or from another preset. Named arguments can be referenced in a QR with {{arg::key}}.
`, -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'messages', - callback: getMessagesCallback, - aliases: ['message'], - namedArgumentList: [ - new SlashCommandNamedArgument( - 'names', 'show message author names', [ARGUMENT_TYPE.BOOLEAN], false, false, 'off', commonEnumProviders.boolean('onOff')(), - ), - new SlashCommandNamedArgument( - 'hidden', 'include hidden messages', [ARGUMENT_TYPE.BOOLEAN], false, false, 'on', commonEnumProviders.boolean('onOff')(), - ), - SlashCommandNamedArgument.fromProps({ - name: 'role', - description: 'filter messages by role', - typeList: [ARGUMENT_TYPE.STRING], - enumList: [ - new SlashCommandEnumValue('system', null, enumTypes.enum, enumIcons.system), - new SlashCommandEnumValue('assistant', null, enumTypes.enum, enumIcons.assistant), - new SlashCommandEnumValue('user', null, enumTypes.enum, enumIcons.user), - ], - }), - ], - unnamedArgumentList: [ - SlashCommandArgument.fromProps({ - description: 'message index (starts with 0) or range', - typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE], - isRequired: true, - enumProvider: commonEnumProviders.messages(), - }), - ], - returns: 'the specified message or range of messages as a string', - helpString: ` + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'messages', + callback: getMessagesCallback, + aliases: ['message'], + namedArgumentList: [ + new SlashCommandNamedArgument( + 'names', 'show message author names', [ARGUMENT_TYPE.BOOLEAN], false, false, 'off', commonEnumProviders.boolean('onOff')(), + ), + new SlashCommandNamedArgument( + 'hidden', 'include hidden messages', [ARGUMENT_TYPE.BOOLEAN], false, false, 'on', commonEnumProviders.boolean('onOff')(), + ), + SlashCommandNamedArgument.fromProps({ + name: 'role', + description: 'filter messages by role', + typeList: [ARGUMENT_TYPE.STRING], + enumList: [ + new SlashCommandEnumValue('system', null, enumTypes.enum, enumIcons.system), + new SlashCommandEnumValue('assistant', null, enumTypes.enum, enumIcons.assistant), + new SlashCommandEnumValue('user', null, enumTypes.enum, enumIcons.user), + ], + }), + ], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'message index (starts with 0) or range', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE], + isRequired: true, + enumProvider: commonEnumProviders.messages(), + }), + ], + returns: 'the specified message or range of messages as a string', + helpString: `
Returns the specified message or range of messages as a string.
@@ -1098,16 +1098,16 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ `, -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'setinput', - callback: setInputCallback, - unnamedArgumentList: [ - new SlashCommandArgument( - 'text', [ARGUMENT_TYPE.STRING], true, - ), - ], - helpString: ` + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'setinput', + callback: setInputCallback, + unnamedArgumentList: [ + new SlashCommandArgument( + 'text', [ARGUMENT_TYPE.STRING], true, + ), + ], + helpString: `
Sets the user input to the specified text and passes it to the next command through the pipe.
@@ -1120,28 +1120,28 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ `, -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'popup', - callback: popupCallback, - returns: 'popup text', - namedArgumentList: [ - new SlashCommandNamedArgument( - 'large', 'show large popup', [ARGUMENT_TYPE.BOOLEAN], false, false, null, commonEnumProviders.boolean('onOff')(), - ), - new SlashCommandNamedArgument( - 'wide', 'show wide popup', [ARGUMENT_TYPE.BOOLEAN], false, false, null, commonEnumProviders.boolean('onOff')(), - ), - new SlashCommandNamedArgument( - 'okButton', 'text for the OK button', [ARGUMENT_TYPE.STRING], false, - ), - ], - unnamedArgumentList: [ - new SlashCommandArgument( - 'text', [ARGUMENT_TYPE.STRING], true, - ), - ], - helpString: ` + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'popup', + callback: popupCallback, + returns: 'popup text', + namedArgumentList: [ + new SlashCommandNamedArgument( + 'large', 'show large popup', [ARGUMENT_TYPE.BOOLEAN], false, false, null, commonEnumProviders.boolean('onOff')(), + ), + new SlashCommandNamedArgument( + 'wide', 'show wide popup', [ARGUMENT_TYPE.BOOLEAN], false, false, null, commonEnumProviders.boolean('onOff')(), + ), + new SlashCommandNamedArgument( + 'okButton', 'text for the OK button', [ARGUMENT_TYPE.STRING], false, + ), + ], + unnamedArgumentList: [ + new SlashCommandArgument( + 'text', [ARGUMENT_TYPE.STRING], true, + ), + ], + helpString: `
Shows a blocking popup with the specified text and buttons. Returns the popup text. @@ -1155,22 +1155,22 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
`, -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'buttons', - callback: buttonsCallback, - returns: 'clicked button label', - namedArgumentList: [ - new SlashCommandNamedArgument( - 'labels', 'button labels', [ARGUMENT_TYPE.LIST], true, - ), - ], - unnamedArgumentList: [ - new SlashCommandArgument( - 'text', [ARGUMENT_TYPE.STRING], true, - ), - ], - helpString: ` + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'buttons', + callback: buttonsCallback, + returns: 'clicked button label', + namedArgumentList: [ + new SlashCommandNamedArgument( + 'labels', 'button labels', [ARGUMENT_TYPE.LIST], true, + ), + ], + unnamedArgumentList: [ + new SlashCommandArgument( + 'text', [ARGUMENT_TYPE.STRING], true, + ), + ], + helpString: `
Shows a blocking popup with the specified text and buttons. Returns the clicked button label into the pipe or empty string if canceled. @@ -1184,32 +1184,32 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
`, -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'trimtokens', - callback: trimTokensCallback, - returns: 'trimmed text', - namedArgumentList: [ - new SlashCommandNamedArgument( - 'limit', 'number of tokens to keep', [ARGUMENT_TYPE.NUMBER], true, - ), - SlashCommandNamedArgument.fromProps({ - name: 'direction', - description: 'trim direction', - typeList: [ARGUMENT_TYPE.STRING], - isRequired: true, - enumList: [ - new SlashCommandEnumValue('start', null, enumTypes.enum, '⏪'), - new SlashCommandEnumValue('end', null, enumTypes.enum, '⏩'), - ], - }), - ], - unnamedArgumentList: [ - new SlashCommandArgument( - 'text', [ARGUMENT_TYPE.STRING], false, - ), - ], - helpString: ` + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'trimtokens', + callback: trimTokensCallback, + returns: 'trimmed text', + namedArgumentList: [ + new SlashCommandNamedArgument( + 'limit', 'number of tokens to keep', [ARGUMENT_TYPE.NUMBER], true, + ), + SlashCommandNamedArgument.fromProps({ + name: 'direction', + description: 'trim direction', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumList: [ + new SlashCommandEnumValue('start', null, enumTypes.enum, '⏪'), + new SlashCommandEnumValue('end', null, enumTypes.enum, '⏩'), + ], + }), + ], + unnamedArgumentList: [ + new SlashCommandArgument( + 'text', [ARGUMENT_TYPE.STRING], false, + ), + ], + helpString: `
Trims the start or end of text to the specified number of tokens.
@@ -1222,17 +1222,17 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ `, -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'trimstart', - callback: trimStartCallback, - returns: 'trimmed text', - unnamedArgumentList: [ - new SlashCommandArgument( - 'text', [ARGUMENT_TYPE.STRING], true, - ), - ], - helpString: ` + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'trimstart', + callback: trimStartCallback, + returns: 'trimmed text', + unnamedArgumentList: [ + new SlashCommandArgument( + 'text', [ARGUMENT_TYPE.STRING], true, + ), + ], + helpString: `
Trims the text to the start of the first full sentence.
@@ -1245,148 +1245,149 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ `, -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'trimend', - callback: trimEndCallback, - returns: 'trimmed text', - unnamedArgumentList: [ - new SlashCommandArgument( - 'text', [ARGUMENT_TYPE.STRING], true, - ), - ], - helpString: 'Trims the text to the end of the last full sentence.', -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'inject', - callback: injectCallback, - namedArgumentList: [ - SlashCommandNamedArgument.fromProps({ - name: 'id', - description: 'injection ID or variable name pointing to ID', - typeList: [ARGUMENT_TYPE.STRING], - isRequired: true, - enumProvider: commonEnumProviders.injects, - }), - new SlashCommandNamedArgument( - 'position', 'injection position', [ARGUMENT_TYPE.STRING], false, false, 'after', ['before', 'after', 'chat'], - ), - new SlashCommandNamedArgument( - 'depth', 'injection depth', [ARGUMENT_TYPE.NUMBER], false, false, '4', - ), - new SlashCommandNamedArgument( - 'scan', 'include injection content into World Info scans', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', - ), - SlashCommandNamedArgument.fromProps({ - name: 'role', - description: 'role for in-chat injections', - typeList: [ARGUMENT_TYPE.STRING], - isRequired: false, - enumList: [ - new SlashCommandEnumValue('system', null, enumTypes.enum, enumIcons.system), - new SlashCommandEnumValue('assistant', null, enumTypes.enum, enumIcons.assistant), - new SlashCommandEnumValue('user', null, enumTypes.enum, enumIcons.user), - ], - }), - new SlashCommandNamedArgument( - 'ephemeral', 'remove injection after generation', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', - ), - ], - unnamedArgumentList: [ - new SlashCommandArgument( - 'text', [ARGUMENT_TYPE.STRING], false, - ), - ], - helpString: 'Injects a text into the LLM prompt for the current chat. Requires a unique injection ID. Positions: "before" main prompt, "after" main prompt, in-"chat" (default: after). Depth: injection depth for the prompt (default: 4). Role: role for in-chat injections (default: system). Scan: include injection content into World Info scans (default: false).', -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'listinjects', - callback: listInjectsCallback, - helpString: 'Lists all script injections for the current chat.', -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'flushinject', - aliases: ['flushinjects'], - unnamedArgumentList: [ - SlashCommandArgument.fromProps({ - description: 'injection ID or a variable name pointing to ID', - typeList: [ARGUMENT_TYPE.STRING], - defaultValue: '', - enumProvider: commonEnumProviders.injects, - }), - ], - callback: flushInjectsCallback, - helpString: 'Removes a script injection for the current chat. If no ID is provided, removes all script injections.', -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'tokens', - callback: (_, text) => { - if (text instanceof SlashCommandClosure || Array.isArray(text)) throw new Error('Unnamed argument cannot be a closure for command /tokens'); - return getTokenCountAsync(text).then(count => String(count)); - }, - returns: 'number of tokens', - unnamedArgumentList: [ - new SlashCommandArgument( - 'text', [ARGUMENT_TYPE.STRING], true, - ), - ], - helpString: 'Counts the number of tokens in the provided text.', -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'model', - callback: modelCallback, - returns: 'current model', - unnamedArgumentList: [ - SlashCommandArgument.fromProps({ - description: 'model name', - typeList: [ARGUMENT_TYPE.STRING], - enumProvider: () => getModelOptions()?.options.map(option => new SlashCommandEnumValue(option.value, option.value !== option.text ? option.text : null)), - }), - ], - helpString: 'Sets the model for the current API. Gets the current model name if no argument is provided.', -})); -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'setpromptentry', - aliases: ['setpromptentries'], - callback: setPromptEntryCallback, - namedArgumentList: [ - SlashCommandNamedArgument.fromProps({ - name: 'identifier', - description: 'Prompt entry identifier(s) to target', - typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.LIST], - acceptsMultiple: true, - enumProvider: () => { - const promptManager = setupChatCompletionPromptManager(oai_settings); - const prompts = promptManager.serviceSettings.prompts; - return prompts.map(prompt => new SlashCommandEnumValue(prompt.identifier, prompt.name, enumTypes.enum)); - }, - }), - SlashCommandNamedArgument.fromProps({ - name: 'name', - description: 'Prompt entry name(s) to target', - typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.LIST], - acceptsMultiple: true, - enumProvider: () => { - const promptManager = setupChatCompletionPromptManager(oai_settings); - const prompts = promptManager.serviceSettings.prompts; - return prompts.map(prompt => new SlashCommandEnumValue(prompt.name, prompt.identifier, enumTypes.enum)); - }, - }), - ], - unnamedArgumentList: [ - SlashCommandArgument.fromProps({ - description: 'Set entry/entries on or off', - typeList: [ARGUMENT_TYPE.STRING], - isRequired: true, - acceptsMultiple: false, - defaultValue: 'toggle', // unnamed arguments don't support default values yet - enumList: commonEnumProviders.boolean('onOffToggle')(), - }), - ], - helpString: 'Sets the specified prompt manager entry/entries on or off.', -})); + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'trimend', + callback: trimEndCallback, + returns: 'trimmed text', + unnamedArgumentList: [ + new SlashCommandArgument( + 'text', [ARGUMENT_TYPE.STRING], true, + ), + ], + helpString: 'Trims the text to the end of the last full sentence.', + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'inject', + callback: injectCallback, + namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'id', + description: 'injection ID or variable name pointing to ID', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: commonEnumProviders.injects, + }), + new SlashCommandNamedArgument( + 'position', 'injection position', [ARGUMENT_TYPE.STRING], false, false, 'after', ['before', 'after', 'chat'], + ), + new SlashCommandNamedArgument( + 'depth', 'injection depth', [ARGUMENT_TYPE.NUMBER], false, false, '4', + ), + new SlashCommandNamedArgument( + 'scan', 'include injection content into World Info scans', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', + ), + SlashCommandNamedArgument.fromProps({ + name: 'role', + description: 'role for in-chat injections', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: false, + enumList: [ + new SlashCommandEnumValue('system', null, enumTypes.enum, enumIcons.system), + new SlashCommandEnumValue('assistant', null, enumTypes.enum, enumIcons.assistant), + new SlashCommandEnumValue('user', null, enumTypes.enum, enumIcons.user), + ], + }), + new SlashCommandNamedArgument( + 'ephemeral', 'remove injection after generation', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', + ), + ], + unnamedArgumentList: [ + new SlashCommandArgument( + 'text', [ARGUMENT_TYPE.STRING], false, + ), + ], + helpString: 'Injects a text into the LLM prompt for the current chat. Requires a unique injection ID. Positions: "before" main prompt, "after" main prompt, in-"chat" (default: after). Depth: injection depth for the prompt (default: 4). Role: role for in-chat injections (default: system). Scan: include injection content into World Info scans (default: false).', + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'listinjects', + callback: listInjectsCallback, + helpString: 'Lists all script injections for the current chat.', + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'flushinject', + aliases: ['flushinjects'], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'injection ID or a variable name pointing to ID', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: '', + enumProvider: commonEnumProviders.injects, + }), + ], + callback: flushInjectsCallback, + helpString: 'Removes a script injection for the current chat. If no ID is provided, removes all script injections.', + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'tokens', + callback: (_, text) => { + if (text instanceof SlashCommandClosure || Array.isArray(text)) throw new Error('Unnamed argument cannot be a closure for command /tokens'); + return getTokenCountAsync(text).then(count => String(count)); + }, + returns: 'number of tokens', + unnamedArgumentList: [ + new SlashCommandArgument( + 'text', [ARGUMENT_TYPE.STRING], true, + ), + ], + helpString: 'Counts the number of tokens in the provided text.', + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'model', + callback: modelCallback, + returns: 'current model', + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'model name', + typeList: [ARGUMENT_TYPE.STRING], + enumProvider: () => getModelOptions()?.options.map(option => new SlashCommandEnumValue(option.value, option.value !== option.text ? option.text : null)), + }), + ], + helpString: 'Sets the model for the current API. Gets the current model name if no argument is provided.', + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'setpromptentry', + aliases: ['setpromptentries'], + callback: setPromptEntryCallback, + namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'identifier', + description: 'Prompt entry identifier(s) to target', + typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.LIST], + acceptsMultiple: true, + enumProvider: () => { + const promptManager = setupChatCompletionPromptManager(oai_settings); + const prompts = promptManager.serviceSettings.prompts; + return prompts.map(prompt => new SlashCommandEnumValue(prompt.identifier, prompt.name, enumTypes.enum)); + }, + }), + SlashCommandNamedArgument.fromProps({ + name: 'name', + description: 'Prompt entry name(s) to target', + typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.LIST], + acceptsMultiple: true, + enumProvider: () => { + const promptManager = setupChatCompletionPromptManager(oai_settings); + const prompts = promptManager.serviceSettings.prompts; + return prompts.map(prompt => new SlashCommandEnumValue(prompt.name, prompt.identifier, enumTypes.enum)); + }, + }), + ], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'Set entry/entries on or off', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + acceptsMultiple: false, + defaultValue: 'toggle', // unnamed arguments don't support default values yet + enumList: commonEnumProviders.boolean('onOffToggle')(), + }), + ], + helpString: 'Sets the specified prompt manager entry/entries on or off.', + })); -registerVariableCommands(); + registerVariableCommands(); +} const NARRATOR_NAME_KEY = 'narrator_name'; const NARRATOR_NAME_DEFAULT = 'System'; From 1d6f0386013991b6acd56e3c6d4a121a513387cf Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sat, 29 Jun 2024 02:16:36 +0200 Subject: [PATCH 069/181] Add optional args to /echo command --- public/scripts/slash-commands.js | 77 ++++++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 8 deletions(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 3573453bd..6abec1972 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -745,6 +745,32 @@ export function initDefaultSlashCommands() { new SlashCommandEnumValue('success', 'success', enumTypes.enum, '✅'), ], }), + SlashCommandNamedArgument.fromProps({ + name: 'timeOut', + description: 'time in milliseconds to display the toast message. Set this and \'extendedTimeout\' to 0 to show indefinitely until dismissed.', + typeList: [ARGUMENT_TYPE.NUMBER], + defaultValue: `${toastr.options.timeOut}`, + }), + SlashCommandNamedArgument.fromProps({ + name: 'extendedTimeout', + description: 'time in milliseconds to display the toast message. Set this and \'timeOut\' to 0 to show indefinitely until dismissed.', + typeList: [ARGUMENT_TYPE.NUMBER], + defaultValue: `${toastr.options.extendedTimeOut}`, + }), + SlashCommandNamedArgument.fromProps({ + name: 'preventDuplicates', + description: 'prevent duplicate toasts with the same message from being displayed.', + typeList: [ARGUMENT_TYPE.BOOLEAN], + defaultValue: 'false', + enumList: commonEnumProviders.boolean('trueFalse')(), + }), + SlashCommandNamedArgument.fromProps({ + name: 'awaitDismissal', + description: 'wait for the toast to be dismissed before continuing.', + typeList: [ARGUMENT_TYPE.BOOLEAN], + defaultValue: 'false', + enumList: commonEnumProviders.boolean('trueFalse')(), + }), ], unnamedArgumentList: [ new SlashCommandArgument( @@ -1939,32 +1965,67 @@ async function generateCallback(args, value) { } } +/** + * + * @param {{title?: string, severity?: string, timeOut?: string, extendedTimeOut?: string, preventDuplicates?: string, awaitDismissal?: string}} args - named arguments from the slash command + * @param {string} value - The string to echo (unnamed argument from the slash command) + * @returns {Promise} The text that was echoed + */ async function echoCallback(args, value) { // Note: We don't need to sanitize input, as toastr is set up by default to escape HTML via toastr options if (value === '') { console.warn('WARN: No argument provided for /echo command'); - return; + return ''; } - const title = args?.title !== undefined && typeof args?.title === 'string' ? args.title : undefined; - const severity = args?.severity !== undefined && typeof args?.severity === 'string' ? args.severity : 'info'; + + if (args.severity && !['error', 'warning', 'success', 'info'].includes(args.severity)) { + toastr.warning(`Invalid severity provided for /echo command: ${args.severity}`); + args.severity = null; + } + + const title = args.title ? args.title : undefined; + const severity = args.severity ? args.severity : 'info'; + + /** @type {ToastrOptions} */ + const options = {}; + if (args.timeOut && !isNaN(parseInt(args.timeOut))) options.timeOut = parseInt(args.timeOut); + if (args.extendedTimeOut && !isNaN(parseInt(args.extendedTimeOut))) options.extendedTimeOut = parseInt(args.extendedTimeOut); + if (isTrueBoolean(args.preventDuplicates)) options.preventDuplicates = true; + + // Prepare possible await handling + let awaitDismissal = isTrueBoolean(args.awaitDismissal); + let resolveToastDismissal; + + if (awaitDismissal) { + options.onHidden = () => resolveToastDismissal(value); + } + switch (severity) { case 'error': - toastr.error(value, title); + toastr.error(value, title, options); break; case 'warning': - toastr.warning(value, title); + toastr.warning(value, title, options); break; case 'success': - toastr.success(value, title); + toastr.success(value, title, options); break; case 'info': default: - toastr.info(value, title); + toastr.info(value, title, options); break; } - return value; + + if (awaitDismissal) { + return new Promise((resolve) => { + resolveToastDismissal = resolve; + }); + } else { + return value; + } } + async function addSwipeCallback(_, arg) { const lastMessage = chat[chat.length - 1]; From 5c3b799d6589539d6804b74733bad6f1a7dedd9e Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sat, 29 Jun 2024 02:24:20 +0200 Subject: [PATCH 070/181] Fix naming inconsistencies --- public/scripts/slash-commands.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 6abec1972..cfc206e89 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -746,14 +746,14 @@ export function initDefaultSlashCommands() { ], }), SlashCommandNamedArgument.fromProps({ - name: 'timeOut', + name: 'timeout', description: 'time in milliseconds to display the toast message. Set this and \'extendedTimeout\' to 0 to show indefinitely until dismissed.', typeList: [ARGUMENT_TYPE.NUMBER], defaultValue: `${toastr.options.timeOut}`, }), SlashCommandNamedArgument.fromProps({ name: 'extendedTimeout', - description: 'time in milliseconds to display the toast message. Set this and \'timeOut\' to 0 to show indefinitely until dismissed.', + description: 'time in milliseconds to display the toast message. Set this and \'timeout\' to 0 to show indefinitely until dismissed.', typeList: [ARGUMENT_TYPE.NUMBER], defaultValue: `${toastr.options.extendedTimeOut}`, }), @@ -1967,7 +1967,7 @@ async function generateCallback(args, value) { /** * - * @param {{title?: string, severity?: string, timeOut?: string, extendedTimeOut?: string, preventDuplicates?: string, awaitDismissal?: string}} args - named arguments from the slash command + * @param {{title?: string, severity?: string, timeout?: string, extendedTimeout?: string, preventDuplicates?: string, awaitDismissal?: string}} args - named arguments from the slash command * @param {string} value - The string to echo (unnamed argument from the slash command) * @returns {Promise} The text that was echoed */ @@ -1988,8 +1988,8 @@ async function echoCallback(args, value) { /** @type {ToastrOptions} */ const options = {}; - if (args.timeOut && !isNaN(parseInt(args.timeOut))) options.timeOut = parseInt(args.timeOut); - if (args.extendedTimeOut && !isNaN(parseInt(args.extendedTimeOut))) options.extendedTimeOut = parseInt(args.extendedTimeOut); + if (args.timeout && !isNaN(parseInt(args.timeout))) options.timeOut = parseInt(args.timeout); + if (args.extendedTimeout && !isNaN(parseInt(args.extendedTimeout))) options.extendedTimeOut = parseInt(args.extendedTimeout); if (isTrueBoolean(args.preventDuplicates)) options.preventDuplicates = true; // Prepare possible await handling From 75099d3a2260476b877d904119a0241902beeeaa Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sat, 29 Jun 2024 02:42:01 +0200 Subject: [PATCH 071/181] Fix oversight in forceEnum slash commands --- public/scripts/slash-commands/SlashCommandArgument.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/public/scripts/slash-commands/SlashCommandArgument.js b/public/scripts/slash-commands/SlashCommandArgument.js index 76642a160..02c88b7c2 100644 --- a/public/scripts/slash-commands/SlashCommandArgument.js +++ b/public/scripts/slash-commands/SlashCommandArgument.js @@ -59,7 +59,7 @@ export class SlashCommandArgument { * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} enums * @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} enumProvider function that returns auto complete options */ - constructor(description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], enumProvider = null, forceEnum = true) { + constructor(description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], enumProvider = null, forceEnum = false) { this.description = description; this.typeList = types ? Array.isArray(types) ? types : [types] : []; this.isRequired = isRequired ?? false; @@ -90,7 +90,7 @@ export class SlashCommandNamedArgument extends SlashCommandArgument { * @param {string|SlashCommandClosure} [props.defaultValue=null] default value if no value is provided * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList=[]] list of accepted values * @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} [props.enumProvider=null] function that returns auto complete options - * @param {boolean} [props.forceEnum=true] default: true - whether the input must match one of the enum values + * @param {boolean} [props.forceEnum=false] default: false - whether the input must match one of the enum values */ static fromProps(props) { return new SlashCommandNamedArgument( @@ -103,7 +103,7 @@ export class SlashCommandNamedArgument extends SlashCommandArgument { props.enumList ?? [], props.aliasList ?? [], props.enumProvider ?? null, - props.forceEnum ?? true, + props.forceEnum ?? false, ); } @@ -120,9 +120,9 @@ export class SlashCommandNamedArgument extends SlashCommandArgument { * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [enums=[]] * @param {string[]} [aliases=[]] * @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} [enumProvider=null] function that returns auto complete options - * @param {boolean} [forceEnum=true] + * @param {boolean} [forceEnum=false] */ - constructor(name, description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], aliases = [], enumProvider = null, forceEnum = true) { + constructor(name, description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], aliases = [], enumProvider = null, forceEnum = false) { super(description, types, isRequired, acceptsMultiple, defaultValue, enums, enumProvider, forceEnum); this.name = name; this.aliasList = aliases ? Array.isArray(aliases) ? aliases : [aliases] : []; From f7d3a1c942b338b43f28738bb4f5b41cb4b8f6cf Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sat, 29 Jun 2024 02:52:30 +0200 Subject: [PATCH 072/181] Various commands with 'at' support depth values --- public/scripts/slash-commands.js | 41 +++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 3573453bd..3628c0142 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -177,7 +177,7 @@ export function initDefaultSlashCommands() { ), SlashCommandNamedArgument.fromProps({ name: 'at', - description: 'position to insert the message', + description: 'position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values are accepted and will work similarly to how \'depth\' usually works. For example, -1 will insert the message right before the last message in chat.', typeList: [ARGUMENT_TYPE.NUMBER], enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), }), @@ -220,7 +220,7 @@ export function initDefaultSlashCommands() { ), SlashCommandNamedArgument.fromProps({ name: 'at', - description: 'position to insert the message', + description: 'position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values are accepted and will work similarly to how \'depth\' usually works. For example, -1 will insert the message right before the last message in chat.', typeList: [ARGUMENT_TYPE.NUMBER], enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), }), @@ -274,7 +274,7 @@ export function initDefaultSlashCommands() { ), SlashCommandNamedArgument.fromProps({ name: 'at', - description: 'position to insert the message', + description: 'position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values are accepted and will work similarly to how \'depth\' usually works. For example, -1 will insert the message right before the last message in chat.', typeList: [ARGUMENT_TYPE.NUMBER], enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), }), @@ -459,7 +459,7 @@ export function initDefaultSlashCommands() { ), SlashCommandNamedArgument.fromProps({ name: 'at', - description: 'position to insert the message', + description: 'position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values are accepted and will work similarly to how \'depth\' usually works. For example, -1 will insert the message right before the last message in chat.', typeList: [ARGUMENT_TYPE.NUMBER], enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), }), @@ -2429,7 +2429,14 @@ async function sendUserMessageCallback(args, text) { text = text.trim(); const compact = isTrueBoolean(args?.compact); const bias = extractMessageBias(text); - const insertAt = Number(args?.at); + + let insertAt = Number(args?.at); + + // Convert possible depth parameter to index + if (!isNaN(insertAt) && (insertAt < 0 || insertAt === Number(-0))) { + // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7) + insertAt = chat.length + insertAt; + } if ('name' in args) { const name = args.name || ''; @@ -2738,7 +2745,13 @@ export async function sendMessageAs(args, text) { }, }]; - const insertAt = Number(args.at); + let insertAt = Number(args.at); + + // Convert possible depth parameter to index + if (!isNaN(insertAt) && (insertAt < 0 || insertAt === Number(-0))) { + // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7) + insertAt = chat.length + insertAt; + } if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) { chat.splice(insertAt, 0, message); @@ -2785,7 +2798,13 @@ export async function sendNarratorMessage(args, text) { }, }; - const insertAt = Number(args.at); + let insertAt = Number(args.at); + + // Convert possible depth parameter to index + if (!isNaN(insertAt) && (insertAt < 0 || insertAt === Number(-0))) { + // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7) + insertAt = chat.length + insertAt; + } if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) { chat.splice(insertAt, 0, message); @@ -2867,7 +2886,13 @@ async function sendCommentMessage(args, text) { }, }; - const insertAt = Number(args.at); + let insertAt = Number(args.at); + + // Convert possible depth parameter to index + if (!isNaN(insertAt) && (insertAt < 0 || insertAt === Number(-0))) { + // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7) + insertAt = chat.length + insertAt; + } if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) { chat.splice(insertAt, 0, message); From bb39e852b88eca7cc5ba5caf7ac8565ec2e285fb Mon Sep 17 00:00:00 2001 From: Risenafis <91325858+Risenafis@users.noreply.github.com> Date: Sat, 29 Jun 2024 20:15:37 +0900 Subject: [PATCH 073/181] TTS: Add support for VITS/W2V2-VITS/Bert-VITS2 (#2439) * add VITSTtsProvider * add options * fix params, drop reference_audio_path * post with body * improve preview lang * add space in label --- public/scripts/extensions/tts/index.js | 2 + public/scripts/extensions/tts/vits.js | 404 +++++++++++++++++++++++++ 2 files changed, 406 insertions(+) create mode 100644 public/scripts/extensions/tts/vits.js diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js index 13d3718ed..3ea6f491a 100644 --- a/public/scripts/extensions/tts/index.js +++ b/public/scripts/extensions/tts/index.js @@ -10,6 +10,7 @@ import { NovelTtsProvider } from './novel.js'; import { power_user } from '../../power-user.js'; import { OpenAITtsProvider } from './openai.js'; import { XTTSTtsProvider } from './xtts.js'; +import { VITSTtsProvider } from './vits.js'; import { GSVITtsProvider } from './gsvi.js'; import { SBVits2TtsProvider } from './sbvits2.js'; import { AllTalkTtsProvider } from './alltalk.js'; @@ -83,6 +84,7 @@ const ttsProviders = { ElevenLabs: ElevenLabsTtsProvider, Silero: SileroTtsProvider, XTTSv2: XTTSTtsProvider, + VITS: VITSTtsProvider, GSVI: GSVITtsProvider, SBVits2: SBVits2TtsProvider, System: SystemTtsProvider, diff --git a/public/scripts/extensions/tts/vits.js b/public/scripts/extensions/tts/vits.js new file mode 100644 index 000000000..4cfa72953 --- /dev/null +++ b/public/scripts/extensions/tts/vits.js @@ -0,0 +1,404 @@ +import { getPreviewString, saveTtsProviderSettings } from './index.js'; + +export { VITSTtsProvider }; + +class VITSTtsProvider { + //########// + // Config // + //########// + + settings; + ready = false; + voices = []; + separator = '. '; + audioElement = document.createElement('audio'); + + /** + * Perform any text processing before passing to TTS engine. + * @param {string} text Input text + * @returns {string} Processed text + */ + processText(text) { + return text; + } + + audioFormats = ['wav', 'ogg', 'silk', 'mp3', 'flac']; + + languageLabels = { + 'Auto': 'auto', + 'Chinese': 'zh', + 'English': 'en', + 'Japanese': 'ja', + 'Korean': 'ko', + }; + + langKey2LangCode = { + 'zh': 'zh-CN', + 'en': 'en-US', + 'ja': 'ja-JP', + 'ko': 'ko-KR', + }; + + modelTypes = { + VITS: 'VITS', + W2V2_VITS: 'W2V2-VITS', + BERT_VITS2: 'BERT-VITS2', + }; + + defaultSettings = { + provider_endpoint: 'http://localhost:23456', + format: 'wav', + lang: 'auto', + length: 1.0, + noise: 0.33, + noisew: 0.4, + segment_size: 50, + streaming: false, + dim_emotion: 0, + sdp_ratio: 0.2, + emotion: 0, + text_prompt: '', + style_text: '', + style_weight: 1, + }; + + get settingsHtml() { + let html = ` + + +
+ + + Use
vits-simple-api.
+ + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + + + + + + + `; + + return html; + } + + onSettingsChange() { + // Used when provider settings are updated from UI + this.settings.provider_endpoint = $('#vits_endpoint').val(); + this.settings.lang = $('#vits_lang').val(); + this.settings.format = $('#vits_format').val(); + this.settings.dim_emotion = $('#vits_dim_emotion').val(); + this.settings.text_prompt = $('#vits_text_prompt').val(); + this.settings.style_text = $('#vits_style_text').val(); + + // Update the default TTS settings based on input fields + this.settings.length = $('#vits_length').val(); + this.settings.noise = $('#vits_noise').val(); + this.settings.noisew = $('#vits_noisew').val(); + this.settings.segment_size = $('#vits_segment_size').val(); + this.settings.streaming = $('#vits_streaming').is(':checked'); + this.settings.sdp_ratio = $('#vits_sdp_ratio').val(); + this.settings.emotion = $('#vits_emotion').val(); + this.settings.style_weight = $('#vits_style_weight').val(); + + // Update the UI to reflect changes + $('#vits_length_output').text(this.settings.length); + $('#vits_noise_output').text(this.settings.noise); + $('#vits_noisew_output').text(this.settings.noisew); + $('#vits_segment_size_output').text(this.settings.segment_size); + $('#vits_sdp_ratio_output').text(this.settings.sdp_ratio); + $('#vits_emotion_output').text(this.settings.emotion); + $('#vits_style_weight_output').text(this.settings.style_weight); + + saveTtsProviderSettings(); + this.changeTTSSettings(); + } + + async loadSettings(settings) { + // Pupulate Provider UI given input settings + if (Object.keys(settings).length == 0) { + console.info('Using default TTS Provider settings'); + } + + // Only accept keys defined in defaultSettings + this.settings = this.defaultSettings; + + for (const key in settings) { + if (key in this.settings) { + this.settings[key] = settings[key]; + } else { + console.debug(`Ignoring non-user-configurable setting: ${key}`); + } + } + + // Set initial values from the settings + $('#vits_endpoint').val(this.settings.provider_endpoint); + $('#vits_lang').val(this.settings.lang); + $('#vits_format').val(this.settings.format); + $('#vits_length').val(this.settings.length); + $('#vits_noise').val(this.settings.noise); + $('#vits_noisew').val(this.settings.noisew); + $('#vits_segment_size').val(this.settings.segment_size); + $('#vits_streaming').prop('checked', this.settings.streaming); + $('#vits_dim_emotion').val(this.settings.dim_emotion); + $('#vits_sdp_ratio').val(this.settings.sdp_ratio); + $('#vits_emotion').val(this.settings.emotion); + $('#vits_text_prompt').val(this.settings.text_prompt); + $('#vits_style_text').val(this.settings.style_text); + $('#vits_style_weight').val(this.settings.style_weight); + + // Update the UI to reflect changes + $('#vits_length_output').text(this.settings.length); + $('#vits_noise_output').text(this.settings.noise); + $('#vits_noisew_output').text(this.settings.noisew); + $('#vits_segment_size_output').text(this.settings.segment_size); + $('#vits_sdp_ratio_output').text(this.settings.sdp_ratio); + $('#vits_emotion_output').text(this.settings.emotion); + $('#vits_style_weight_output').text(this.settings.style_weight); + + // Register input/change event listeners to update settings on user interaction + $('#vits_endpoint').on('input', () => { this.onSettingsChange(); }); + $('#vits_lang').on('change', () => { this.onSettingsChange(); }); + $('#vits_format').on('change', () => { this.onSettingsChange(); }); + $('#vits_length').on('change', () => { this.onSettingsChange(); }); + $('#vits_noise').on('change', () => { this.onSettingsChange(); }); + $('#vits_noisew').on('change', () => { this.onSettingsChange(); }); + $('#vits_segment_size').on('change', () => { this.onSettingsChange(); }); + $('#vits_streaming').on('change', () => { this.onSettingsChange(); }); + $('#vits_dim_emotion').on('change', () => { this.onSettingsChange(); }); + $('#vits_sdp_ratio').on('change', () => { this.onSettingsChange(); }); + $('#vits_emotion').on('change', () => { this.onSettingsChange(); }); + $('#vits_text_prompt').on('change', () => { this.onSettingsChange(); }); + $('#vits_style_text').on('change', () => { this.onSettingsChange(); }); + $('#vits_style_weight').on('change', () => { this.onSettingsChange(); }); + + await this.checkReady(); + + console.info('VITS: Settings loaded'); + } + + // Perform a simple readiness check by trying to fetch voiceIds + async checkReady() { + await Promise.allSettled([this.fetchTtsVoiceObjects(), this.changeTTSSettings()]); + } + + async onRefreshClick() { + return; + } + + //#################// + // TTS Interfaces // + //#################// + + async getVoice(voiceName) { + if (this.voices.length == 0) { + this.voices = await this.fetchTtsVoiceObjects(); + } + const match = this.voices.filter( + v => v.name == voiceName, + )[0]; + if (!match) { + throw `TTS Voice name ${voiceName} not found`; + } + return match; + } + + async getVoiceById(voiceId) { + if (this.voices.length == 0) { + this.voices = await this.fetchTtsVoiceObjects(); + } + const match = this.voices.filter( + v => v.voice_id == voiceId, + )[0]; + if (!match) { + throw `TTS Voice id ${voiceId} not found`; + } + return match; + } + + async generateTts(text, voiceId) { + const response = await this.fetchTtsGeneration(text, voiceId); + return response; + } + + //###########// + // API CALLS // + //###########// + async fetchTtsVoiceObjects() { + const response = await fetch(`${this.settings.provider_endpoint}/voice/speakers`); + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${await response.json()}`); + } + const jsonData = await response.json(); + const voices = []; + + const addVoices = (modelType) => { + jsonData[modelType].forEach(voice => { + voices.push({ + name: `[${modelType}] ${voice.name} (${voice.lang})`, + voice_id: `${modelType}&${voice.id}`, + preview_url: false, + lang: voice.lang, + }); + }); + }; + for (const key in this.modelTypes) { + addVoices(this.modelTypes[key]); + } + + this.voices = voices; // Assign to the class property + return voices; // Also return this list + } + + // Each time a parameter is changed, we change the configuration + async changeTTSSettings() { + } + + /** + * Fetch TTS generation from the API. + * @param {string} inputText Text to generate TTS for + * @param {string} voiceId Voice ID to use (model_type&speaker_id)) + * @returns {Promise} Fetch response + */ + async fetchTtsGeneration(inputText, voiceId, lang = null, forceNoStreaming = false) { + console.info(`Generating new TTS for voice_id ${voiceId}`); + + const streaming = !forceNoStreaming && this.settings.streaming; + const [model_type, speaker_id] = voiceId.split('&'); + const params = new URLSearchParams(); + params.append('text', inputText); + params.append('id', speaker_id); + if (streaming) { + params.append('streaming', streaming); + // Streaming response only supports MP3 + } + else { + params.append('format', this.settings.format); + } + params.append('lang', lang ?? this.settings.lang); + params.append('length', this.settings.length); + params.append('noise', this.settings.noise); + params.append('noisew', this.settings.noisew); + params.append('segment_size', this.settings.segment_size); + + if (model_type == this.modelTypes.W2V2_VITS) { + params.append('emotion', this.settings.dim_emotion); + } + else if (model_type == this.modelTypes.BERT_VITS2) { + params.append('sdp_ratio', this.settings.sdp_ratio); + params.append('emotion', this.settings.emotion); + if (this.settings.text_prompt) { + params.append('text_prompt', this.settings.text_prompt); + } + if (this.settings.style_text) { + params.append('style_text', this.settings.style_text); + params.append('style_weight', this.settings.style_weight); + } + } + + const url = `${this.settings.provider_endpoint}/voice/${model_type.toLowerCase()}`; + + if (streaming) { + return url + `?${params.toString()}`; + } + + const response = await fetch( + url, + { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: params, + }, + ); + if (!response.ok) { + toastr.error(response.statusText, 'TTS Generation Failed'); + throw new Error(`HTTP ${response.status}: ${await response.text()}`); + } + return response; + } + + /** + * Preview TTS for a given voice ID. + * @param {string} id Voice ID + */ + async previewTtsVoice(id) { + this.audioElement.pause(); + this.audioElement.currentTime = 0; + const voice = await this.getVoiceById(id); + const lang = voice.lang.includes(this.settings.lang) ? this.settings.lang : voice.lang[0]; + + let lang_code = this.langKey2LangCode[lang]; + const text = getPreviewString(lang_code); + const response = await this.fetchTtsGeneration(text, id, lang, true); + if (typeof response != 'string') { + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${await response.text()}`); + } + const audio = await response.blob(); + const url = URL.createObjectURL(audio); + this.audioElement.src = url; + this.audioElement.play(); + } + } + + // Interface not used + async fetchTtsFromHistory(history_item_id) { + return Promise.resolve(history_item_id); + } +} From 2b50ab398b00cbf28ba1c14cf55420156670fcde Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 29 Jun 2024 14:35:16 +0300 Subject: [PATCH 074/181] Add jsconfig exclusions --- jsconfig.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/jsconfig.json b/jsconfig.json index e7691789d..a48606f1f 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -15,6 +15,11 @@ "**/node_modules/*", "public/lib", "backups/*", - "data/*" + "data/*", + "**/dist/*", + "dist/*", + "cache/*", + "src/tokenizers/*", + "docker/*", ] } From 7fe329b5cf2f6ea98bce8896a0d00992705fbecc Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 29 Jun 2024 23:14:55 +0300 Subject: [PATCH 075/181] Allow paste file and images into chat input form --- public/scripts/chats.js | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/public/scripts/chats.js b/public/scripts/chats.js index 0d1ecba4c..9419e6d7b 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -319,12 +319,10 @@ export function hasPendingFileAttachment() { /** * Displays file information in the message sending form. + * @param {File} file File object * @returns {Promise} */ -async function onFileAttach() { - const fileInput = document.getElementById('file_form_input'); - if (!(fileInput instanceof HTMLInputElement)) return; - const file = fileInput.files[0]; +async function onFileAttach(file) { if (!file) return; const isValid = await validateFile(file); @@ -1503,8 +1501,28 @@ jQuery(function () { $(document).on('click', '.mes_img_enlarge', enlargeMessageImage); $(document).on('click', '.mes_img_delete', deleteMessageImage); - $('#file_form_input').on('change', onFileAttach); + $('#file_form_input').on('change', async () => { + const fileInput = document.getElementById('file_form_input'); + if (!(fileInput instanceof HTMLInputElement)) return; + const file = fileInput.files[0]; + await onFileAttach(file); + }); $('#file_form').on('reset', function () { $('#file_form').addClass('displayNone'); }); + + document.getElementById('send_textarea').addEventListener('paste', async function (event) { + if (event.clipboardData.files.length === 0) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + const fileInput = document.getElementById('file_form_input'); + if (!(fileInput instanceof HTMLInputElement)) return; + + fileInput.files = event.clipboardData.files; + await onFileAttach(fileInput.files[0]); + }); }); From 2670709237c18754b33ab9dcb14c709704cb0c6b Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 29 Jun 2024 23:22:29 +0300 Subject: [PATCH 076/181] Fix caption template references --- public/scripts/extensions/caption/index.js | 2 +- public/scripts/extensions/caption/settings.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/scripts/extensions/caption/index.js b/public/scripts/extensions/caption/index.js index 543eef7f6..960de7b32 100644 --- a/public/scripts/extensions/caption/index.js +++ b/public/scripts/extensions/caption/index.js @@ -409,7 +409,7 @@ jQuery(async function () { }); } async function addSettings() { - const html = await renderExtensionTemplateAsync('caption', 'settings'); + const html = await renderExtensionTemplateAsync('caption', 'settings', { TEMPLATE_DEFAULT, PROMPT_DEFAULT }); $('#caption_container').append(html); } diff --git a/public/scripts/extensions/caption/settings.html b/public/scripts/extensions/caption/settings.html index 90ff673ba..185d76908 100644 --- a/public/scripts/extensions/caption/settings.html +++ b/public/scripts/extensions/caption/settings.html @@ -84,14 +84,14 @@
- +
- +