diff --git a/public/script.js b/public/script.js index 22aa140f3..3a8d5f525 100644 --- a/public/script.js +++ b/public/script.js @@ -242,6 +242,7 @@ import { INTERACTABLE_CONTROL_CLASS, initKeyboard } from './scripts/keyboard.js' import { initDynamicStyles } from './scripts/dynamic-styles.js'; import { SlashCommandEnumValue, enumTypes } from './scripts/slash-commands/SlashCommandEnumValue.js'; import { commonEnumProviders, enumIcons } from './scripts/slash-commands/SlashCommandCommonEnumsProvider.js'; +import { AbortReason } from './scripts/util/AbortReason.js'; //exporting functions and vars for mods export { @@ -462,6 +463,7 @@ export const event_types = { LLM_FUNCTION_TOOL_CALL: 'llm_function_tool_call', ONLINE_STATUS_CHANGED: 'online_status_changed', IMAGE_SWIPED: 'image_swiped', + CONNECTION_PROFILE_LOADED: 'connection_profile_loaded', }; export const eventSource = new EventEmitter(); @@ -977,8 +979,8 @@ async function fixViewport() { document.body.style.position = ''; } -function cancelStatusCheck() { - abortStatusCheck?.abort(); +function cancelStatusCheck(reason = 'Manually cancelled status check') { + abortStatusCheck?.abort(new AbortReason(reason)); abortStatusCheck = new AbortController(); setOnlineStatus('no_connection'); } @@ -1228,7 +1230,12 @@ async function getStatusTextgen() { toastr.error(data.response, 'API Error', { timeOut: 5000, preventDuplicates: true }); } } catch (err) { - console.error('Error getting status', err); + if (err instanceof AbortReason) { + console.info('Status check aborted.', err.reason); + } else { + console.error('Error getting status', err); + + } setOnlineStatus('no_connection'); } @@ -8519,7 +8526,7 @@ async function selectContextCallback(args, name) { } const foundName = result[0].item; - selectContextPreset(foundName, quiet); + selectContextPreset(foundName, { quiet: quiet }); return foundName; } @@ -8539,7 +8546,7 @@ async function selectInstructCallback(args, name) { } const foundName = result[0].item; - selectInstructPreset(foundName, quiet); + selectInstructPreset(foundName, { quiet: quiet }); return foundName; } @@ -9316,7 +9323,7 @@ jQuery(async function () { $('#groupCurrentMemberListToggle .inline-drawer-icon').trigger('click'); }, 200); - $(document).on('click', '.api_loading', cancelStatusCheck); + $(document).on('click', '.api_loading', () => cancelStatusCheck('Canceled because connecting was manually canceled')); //////////INPUT BAR FOCUS-KEEPING LOGIC///////////// let S_TAPreviouslyFocused = false; @@ -10075,7 +10082,7 @@ jQuery(async function () { }); $('#main_api').change(function () { - cancelStatusCheck(); + cancelStatusCheck('Canceled because main api changed'); changeMainAPI(); saveSettingsDebounced(); }); diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index ac300984b..5ec3ff3e2 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -123,6 +123,11 @@ const extension_settings = { /** @type {string[]} */ custom: [], }, + connectionManager: { + selectedProfile: '', + /** @type {import('./extensions/connection-manager/index.js').ConnectionProfile[]} */ + profiles: [], + }, dice: {}, /** @type {import('./char-data.js').RegexScriptData[]} */ regex: [], diff --git a/public/scripts/extensions/connection-manager/index.js b/public/scripts/extensions/connection-manager/index.js new file mode 100644 index 000000000..7f30a8cc7 --- /dev/null +++ b/public/scripts/extensions/connection-manager/index.js @@ -0,0 +1,594 @@ +import { event_types, eventSource, main_api, saveSettingsDebounced } from '../../../script.js'; +import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js'; +import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js'; +import { executeSlashCommandsWithOptions } from '../../slash-commands.js'; +import { SlashCommand } from '../../slash-commands/SlashCommand.js'; +import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; +import { commonEnumProviders, enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; +import { enumTypes, SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js'; +import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; +import { collapseSpaces, getUniqueName, isFalseBoolean, uuidv4 } from '../../utils.js'; + +const MODULE_NAME = 'connection-manager'; +const NONE = ''; + +const DEFAULT_SETTINGS = { + profiles: [], + selectedProfile: null, +}; + +const COMMON_COMMANDS = [ + 'api', + 'preset', + 'api-url', + 'model', +]; + +const CC_COMMANDS = [ + ...COMMON_COMMANDS, + 'proxy', +]; + +const TC_COMMANDS = [ + ...COMMON_COMMANDS, + 'instruct', + 'context', + 'instruct-state', + 'tokenizer', +]; + +const FANCY_NAMES = { + 'api': 'API', + 'api-url': 'Server URL', + 'preset': 'Settings Preset', + 'model': 'Model', + 'proxy': 'Proxy Preset', + 'instruct-state': 'Instruct Mode', + 'instruct': 'Instruct Template', + 'context': 'Context Template', + 'tokenizer': 'Tokenizer', +}; + +/** + * A wrapper for the connection manager spinner. + */ +class ConnectionManagerSpinner { + /** + * @type {AbortController[]} + */ + static abortControllers = []; + + /** @type {HTMLElement} */ + spinnerElement; + + /** @type {AbortController} */ + abortController = new AbortController(); + + constructor() { + // @ts-ignore + this.spinnerElement = document.getElementById('connection_profile_spinner'); + this.abortController = new AbortController(); + } + + start() { + ConnectionManagerSpinner.abortControllers.push(this.abortController); + this.spinnerElement.classList.remove('hidden'); + } + + stop() { + this.spinnerElement.classList.add('hidden'); + } + + isAborted() { + return this.abortController.signal.aborted; + } + + static abort() { + for (const controller of ConnectionManagerSpinner.abortControllers) { + controller.abort(); + } + ConnectionManagerSpinner.abortControllers = []; + } +} + +/** @type {() => SlashCommandEnumValue[]} */ +const profilesProvider = () => [ + new SlashCommandEnumValue(NONE), + ...extension_settings.connectionManager.profiles.map(p => new SlashCommandEnumValue(p.name, null, enumTypes.name, enumIcons.server)), +]; + +/** + * @typedef {Object} ConnectionProfile + * @property {string} id Unique identifier + * @property {string} mode Mode of the connection profile + * @property {string} [name] Name of the connection profile + * @property {string} [api] API + * @property {string} [preset] Settings Preset + * @property {string} [model] Model + * @property {string} [proxy] Proxy Preset + * @property {string} [instruct] Instruct Template + * @property {string} [context] Context Template + * @property {string} [instruct-state] Instruct Mode + * @property {string} [tokenizer] Tokenizer + */ + +const escapeArgument = (a) => a.replace(/"/g, '\\"').replace(/\|/g, '\\|'); + +/** + * Finds the best match for the search value. + * @param {string} value Search value + * @returns {ConnectionProfile|null} Best match or null + */ +function findProfileByName(value) { + // Try to find exact match + const profile = extension_settings.connectionManager.profiles.find(p => p.name === value); + + if (profile) { + return profile; + } + + // Try to find fuzzy match + const fuse = new Fuse(extension_settings.connectionManager.profiles, { keys: ['name'] }); + const results = fuse.search(value); + + if (results.length === 0) { + return null; + } + + const bestMatch = results[0]; + return bestMatch.item; +} + +/** + * Reads the connection profile from the commands. + * @param {string} mode Mode of the connection profile + * @param {ConnectionProfile} profile Connection profile + * @param {boolean} [cleanUp] Whether to clean up the profile + */ +async function readProfileFromCommands(mode, profile, cleanUp = false) { + const commands = mode === 'cc' ? CC_COMMANDS : TC_COMMANDS; + const opposingCommands = mode === 'cc' ? TC_COMMANDS : CC_COMMANDS; + for (const command of commands) { + const commandText = `/${command} quiet=true`; + try { + const result = await executeSlashCommandsWithOptions(commandText, { handleParserErrors: false, handleExecutionErrors: false }); + if (result.pipe) { + profile[command] = result.pipe; + continue; + } + } catch (error) { + console.warn(`Failed to execute command: ${commandText}`, error); + } + } + + if (cleanUp) { + for (const command of opposingCommands) { + if (commands.includes(command)) { + continue; + } + + delete profile[command]; + } + } +} + +/** + * Creates a new connection profile. + * @param {string} [forceName] Name of the connection profile + * @returns {Promise} Created connection profile + */ +async function createConnectionProfile(forceName = null) { + const mode = main_api === 'openai' ? 'cc' : 'tc'; + const id = uuidv4(); + const profile = { + id, + mode, + }; + + await readProfileFromCommands(mode, profile); + + const profileForDisplay = makeFancyProfile(profile); + const template = await renderExtensionTemplateAsync(MODULE_NAME, 'profile', { profile: profileForDisplay }); + const isNameTaken = (n) => extension_settings.connectionManager.profiles.some(p => p.name === n); + const suggestedName = getUniqueName(collapseSpaces(`${profile.api ?? ''} ${profile.model ?? ''} - ${profile.preset ?? ''}`), isNameTaken); + const name = forceName ?? await callGenericPopup(template, POPUP_TYPE.INPUT, suggestedName, { rows: 2 }); + + if (!name) { + return null; + } + + if (isNameTaken(name) || name === NONE) { + toastr.error('A profile with the same name already exists.'); + return null; + } + + profile.name = name; + return profile; +} + +/** + * Deletes the selected connection profile. + * @returns {Promise} + */ +async function deleteConnectionProfile() { + const selectedProfile = extension_settings.connectionManager.selectedProfile; + if (!selectedProfile) { + return; + } + + const index = extension_settings.connectionManager.profiles.findIndex(p => p.id === selectedProfile); + if (index === -1) { + return; + } + + const name = extension_settings.connectionManager.profiles[index].name; + const confirm = await Popup.show.confirm('Are you sure you want to delete the selected profile?', name); + + if (!confirm) { + return; + } + + extension_settings.connectionManager.profiles.splice(index, 1); + extension_settings.connectionManager.selectedProfile = null; + saveSettingsDebounced(); +} + +/** + * Formats the connection profile for display. + * @param {ConnectionProfile} profile Connection profile + * @returns {Object} Fancy profile + */ +function makeFancyProfile(profile) { + return Object.entries(FANCY_NAMES).reduce((acc, [key, value]) => { + if (!profile[key]) return acc; + acc[value] = profile[key]; + return acc; + }, {}); +} + +/** + * Applies the connection profile. + * @param {ConnectionProfile} profile Connection profile + * @returns {Promise} + */ +async function applyConnectionProfile(profile) { + if (!profile) { + return; + } + + // Abort any ongoing profile application + ConnectionManagerSpinner.abort(); + + const mode = profile.mode; + const commands = mode === 'cc' ? CC_COMMANDS : TC_COMMANDS; + const spinner = new ConnectionManagerSpinner(); + spinner.start(); + + for (const command of commands) { + if (spinner.isAborted()) { + throw new Error('Profile application aborted'); + } + + const argument = profile[command]; + if (!argument) { + continue; + } + const commandText = `/${command} quiet=true ${escapeArgument(argument)}`; + try { + await executeSlashCommandsWithOptions(commandText, { handleParserErrors: false, handleExecutionErrors: false }); + } catch (error) { + console.error(`Failed to execute command: ${commandText}`, error); + } + } + + spinner.stop(); +} + +/** + * Updates the selected connection profile. + * @param {ConnectionProfile} profile Connection profile + * @returns {Promise} + */ +async function updateConnectionProfile(profile) { + profile.mode = main_api === 'openai' ? 'cc' : 'tc'; + await readProfileFromCommands(profile.mode, profile, true); +} + +/** + * Renders the connection profile details. + * @param {HTMLSelectElement} profiles Select element containing connection profiles + */ +function renderConnectionProfiles(profiles) { + profiles.innerHTML = ''; + const noneOption = document.createElement('option'); + + noneOption.value = ''; + noneOption.textContent = NONE; + noneOption.selected = !extension_settings.connectionManager.selectedProfile; + profiles.appendChild(noneOption); + + for (const profile of extension_settings.connectionManager.profiles) { + const option = document.createElement('option'); + option.value = profile.id; + option.textContent = profile.name; + option.selected = profile.id === extension_settings.connectionManager.selectedProfile; + profiles.appendChild(option); + } +} + +/** + * Renders the content of the details element. + * @param {HTMLElement} detailsContent Content element of the details + */ +async function renderDetailsContent(detailsContent) { + detailsContent.innerHTML = ''; + if (detailsContent.classList.contains('hidden')) { + return; + } + const selectedProfile = extension_settings.connectionManager.selectedProfile; + const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile); + if (profile) { + const profileForDisplay = makeFancyProfile(profile); + const template = await renderExtensionTemplateAsync(MODULE_NAME, 'view', { profile: profileForDisplay }); + detailsContent.innerHTML = template; + } else { + detailsContent.textContent = 'No profile selected'; + } +} + +(async function () { + extension_settings.connectionManager = extension_settings.connectionManager || structuredClone(DEFAULT_SETTINGS); + + for (const key of Object.keys(DEFAULT_SETTINGS)) { + if (extension_settings.connectionManager[key] === undefined) { + extension_settings.connectionManager[key] = DEFAULT_SETTINGS[key]; + } + } + + const container = document.getElementById('rm_api_block'); + const settings = await renderExtensionTemplateAsync(MODULE_NAME, 'settings'); + container.insertAdjacentHTML('afterbegin', settings); + + /** @type {HTMLSelectElement} */ + // @ts-ignore + const profiles = document.getElementById('connection_profiles'); + renderConnectionProfiles(profiles); + + function toggleProfileSpecificButtons() { + const profileId = extension_settings.connectionManager.selectedProfile; + const profileSpecificButtons = ['update_connection_profile', 'reload_connection_profile', 'delete_connection_profile']; + profileSpecificButtons.forEach(id => document.getElementById(id).classList.toggle('disabled', !profileId)); + } + toggleProfileSpecificButtons(); + + profiles.addEventListener('change', async function () { + const selectedProfile = profiles.selectedOptions[0]; + if (!selectedProfile) { + // Safety net for preventing the command getting stuck + await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, NONE); + return; + } + + const profileId = selectedProfile.value; + extension_settings.connectionManager.selectedProfile = profileId; + saveSettingsDebounced(); + await renderDetailsContent(detailsContent); + + toggleProfileSpecificButtons(); + + // None option selected + if (!profileId) { + await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, NONE); + return; + } + + const profile = extension_settings.connectionManager.profiles.find(p => p.id === profileId); + + if (!profile) { + console.log(`Profile not found: ${profileId}`); + return; + } + + await applyConnectionProfile(profile); + await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name); + }); + + const reloadButton = document.getElementById('reload_connection_profile'); + reloadButton.addEventListener('click', async () => { + const selectedProfile = extension_settings.connectionManager.selectedProfile; + const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile); + if (!profile) { + console.log('No profile selected'); + return; + } + await applyConnectionProfile(profile); + await renderDetailsContent(detailsContent); + await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name); + toastr.success('Connection profile reloaded', '', { timeOut: 1500 }); + }); + + const createButton = document.getElementById('create_connection_profile'); + createButton.addEventListener('click', async () => { + const profile = await createConnectionProfile(); + if (!profile) { + return; + } + extension_settings.connectionManager.profiles.push(profile); + extension_settings.connectionManager.selectedProfile = profile.id; + saveSettingsDebounced(); + renderConnectionProfiles(profiles); + await renderDetailsContent(detailsContent); + await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name); + }); + + const updateButton = document.getElementById('update_connection_profile'); + updateButton.addEventListener('click', async () => { + const selectedProfile = extension_settings.connectionManager.selectedProfile; + const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile); + if (!profile) { + console.log('No profile selected'); + return; + } + await updateConnectionProfile(profile); + await renderDetailsContent(detailsContent); + saveSettingsDebounced(); + await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name); + toastr.success('Connection profile updated', '', { timeOut: 1500 }); + }); + + const deleteButton = document.getElementById('delete_connection_profile'); + deleteButton.addEventListener('click', async () => { + await deleteConnectionProfile(); + renderConnectionProfiles(profiles); + await renderDetailsContent(detailsContent); + await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, NONE); + }); + + /** @type {HTMLElement} */ + const viewDetails = document.getElementById('view_connection_profile'); + const detailsContent = document.getElementById('connection_profile_details_content'); + viewDetails.addEventListener('click', async () => { + viewDetails.classList.toggle('active'); + detailsContent.classList.toggle('hidden'); + await renderDetailsContent(detailsContent); + }); + + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'profile', + helpString: 'Switch to a connection profile or return the name of the current profile in no argument is provided. Use <None> to switch to no profile.', + returns: 'name of the profile', + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'Name of the connection profile', + enumProvider: profilesProvider, + isRequired: false, + }), + ], + namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'await', + description: 'Wait for the connection profile to be applied before returning.', + isRequired: false, + typeList: [ARGUMENT_TYPE.BOOLEAN], + defaultValue: 'true', + enumList: commonEnumProviders.boolean('trueFalse')(), + }), + ], + callback: async (args, value) => { + if (!value || typeof value !== 'string') { + const selectedProfile = extension_settings.connectionManager.selectedProfile; + const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile); + if (!profile) { + return NONE; + } + return profile.name; + } + + if (value === NONE) { + profiles.selectedIndex = 0; + profiles.dispatchEvent(new Event('change')); + return NONE; + } + + const profile = findProfileByName(value); + + if (!profile) { + return ''; + } + + const shouldAwait = !isFalseBoolean(String(args?.await)); + const awaitPromise = new Promise((resolve) => eventSource.once(event_types.CONNECTION_PROFILE_LOADED, resolve)); + + profiles.selectedIndex = Array.from(profiles.options).findIndex(o => o.value === profile.id); + profiles.dispatchEvent(new Event('change')); + + if (shouldAwait) { + await awaitPromise; + } + + return profile.name; + }, + })); + + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'profile-list', + helpString: 'List all connection profile names.', + returns: 'list of profile names', + callback: () => JSON.stringify(extension_settings.connectionManager.profiles.map(p => p.name)), + })); + + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'profile-create', + returns: 'name of the new profile', + helpString: 'Create a new connection profile using the current settings.', + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'name of the new connection profile', + isRequired: true, + typeList: [ARGUMENT_TYPE.STRING], + }), + ], + callback: async (_args, name) => { + if (!name || typeof name !== 'string') { + toastr.warning('Please provide a name for the new connection profile.'); + return ''; + } + const profile = await createConnectionProfile(name); + if (!profile) { + return ''; + } + extension_settings.connectionManager.profiles.push(profile); + extension_settings.connectionManager.selectedProfile = profile.id; + saveSettingsDebounced(); + renderConnectionProfiles(profiles); + await renderDetailsContent(detailsContent); + return profile.name; + }, + })); + + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'profile-update', + helpString: 'Update the selected connection profile.', + callback: async () => { + const selectedProfile = extension_settings.connectionManager.selectedProfile; + const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile); + if (!profile) { + toastr.warning('No profile selected.'); + return ''; + } + await updateConnectionProfile(profile); + await renderDetailsContent(detailsContent); + saveSettingsDebounced(); + return profile.name; + }, + })); + + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'profile-get', + helpString: 'Get the details of the connection profile. Returns the selected profile if no argument is provided.', + returns: 'object of the selected profile', + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'Name of the connection profile', + enumProvider: profilesProvider, + isRequired: false, + }), + ], + callback: async (_args, value) => { + if (!value || typeof value !== 'string') { + const selectedProfile = extension_settings.connectionManager.selectedProfile; + const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile); + if (!profile) { + return ''; + } + return JSON.stringify(profile); + } + + const profile = findProfileByName(value); + if (!profile) { + return ''; + } + return JSON.stringify(profile); + }, + })); +})(); diff --git a/public/scripts/extensions/connection-manager/manifest.json b/public/scripts/extensions/connection-manager/manifest.json new file mode 100644 index 000000000..601f8970c --- /dev/null +++ b/public/scripts/extensions/connection-manager/manifest.json @@ -0,0 +1,11 @@ +{ + "display_name": "Connection Profiles", + "loading_order": 1, + "requires": [], + "optional": [], + "js": "index.js", + "css": "style.css", + "author": "Cohee1207", + "version": "1.0.0", + "homePage": "https://github.com/SillyTavern/SillyTavern" +} diff --git a/public/scripts/extensions/connection-manager/profile.html b/public/scripts/extensions/connection-manager/profile.html new file mode 100644 index 000000000..d8be91561 --- /dev/null +++ b/public/scripts/extensions/connection-manager/profile.html @@ -0,0 +1,13 @@ +
+

+ Creating a Connection Profile +

+
    + {{#each profile}} +
  • {{@key}}: {{this}}
  • + {{/each}} +
+

+ Enter a name: +

+
diff --git a/public/scripts/extensions/connection-manager/settings.html b/public/scripts/extensions/connection-manager/settings.html new file mode 100644 index 000000000..b77b91416 --- /dev/null +++ b/public/scripts/extensions/connection-manager/settings.html @@ -0,0 +1,18 @@ +
+
+

+ Connection Profile +

+
+ +
+
+ + + + + + +
+ +
diff --git a/public/scripts/extensions/connection-manager/style.css b/public/scripts/extensions/connection-manager/style.css new file mode 100644 index 000000000..efd046895 --- /dev/null +++ b/public/scripts/extensions/connection-manager/style.css @@ -0,0 +1,11 @@ +#connection_profile_details_content { + margin: 5px 0; +} + +#connection_profile_details_content ul { + margin: 0; +} + +#connection_profile_spinner { + margin-left: 5px; +} diff --git a/public/scripts/extensions/connection-manager/view.html b/public/scripts/extensions/connection-manager/view.html new file mode 100644 index 000000000..42d1ad35f --- /dev/null +++ b/public/scripts/extensions/connection-manager/view.html @@ -0,0 +1,5 @@ +
    + {{#each profile}} +
  • {{@key}}: {{this}}
  • + {{/each}} +
diff --git a/public/scripts/instruct-mode.js b/public/scripts/instruct-mode.js index 3319ebd23..d22c82ee2 100644 --- a/public/scripts/instruct-mode.js +++ b/public/scripts/instruct-mode.js @@ -130,13 +130,15 @@ function highlightDefaultPreset() { /** * Select context template if not already selected. * @param {string} preset Preset name. - * @param {boolean} quiet Suppress info message. + * @param {object} [options={}] Optional arguments. + * @param {boolean} [options.quiet=false] Suppress toast messages. + * @param {boolean} [options.isAuto=false] Is auto-select. */ -export function selectContextPreset(preset, quiet) { +export function selectContextPreset(preset, { quiet = false, isAuto = false } = {}) { // If context template is not already selected, select it if (preset !== power_user.context.preset) { $('#context_presets').val(preset).trigger('change'); - !quiet && toastr.info(`Context Template: preset "${preset}" auto-selected`); + !quiet && toastr.info(`Context Template: "${preset}" ${isAuto ? 'auto-' : ''}selected`); } // If instruct mode is disabled, enable it, except for default context template @@ -152,13 +154,15 @@ export function selectContextPreset(preset, quiet) { /** * Select instruct preset if not already selected. * @param {string} preset Preset name. - * @param {boolean} quiet Suppress info message. + * @param {object} [options={}] Optional arguments. + * @param {boolean} [options.quiet=false] Suppress toast messages. + * @param {boolean} [options.isAuto=false] Is auto-select. */ -export function selectInstructPreset(preset, quiet) { +export function selectInstructPreset(preset, { quiet = false, isAuto = false } = {}) { // If instruct preset is not already selected, select it if (preset !== power_user.instruct.preset) { $('#instruct_presets').val(preset).trigger('change'); - !quiet && toastr.info(`Instruct Mode: template "${preset}" auto-selected`); + !quiet && toastr.info(`Instruct Template: "${preset}" ${isAuto ? 'auto-' : ''}selected`); } // If instruct mode is disabled, enable it @@ -189,7 +193,7 @@ export function autoSelectInstructPreset(modelId) { // If instruct preset matches the context template if (power_user.instruct.bind_to_context && instruct_preset.name === power_user.context.preset) { foundMatch = true; - selectInstructPreset(instruct_preset.name); + selectInstructPreset(instruct_preset.name, { isAuto: true }); break; } } @@ -203,7 +207,7 @@ export function autoSelectInstructPreset(modelId) { // Stop on first match so it won't cycle back and forth between presets if multiple regexes match if (regex instanceof RegExp && regex.test(modelId)) { - selectInstructPreset(preset.name); + selectInstructPreset(preset.name, { isAuto: true }); return true; } @@ -541,13 +545,13 @@ function selectMatchingContextTemplate(name) { // If context template matches the instruct preset if (context_preset.name === name) { foundMatch = true; - selectContextPreset(context_preset.name); + selectContextPreset(context_preset.name, { isAuto: true }); break; } } if (!foundMatch) { // If no match was found, select default context preset - selectContextPreset(power_user.default_context); + selectContextPreset(power_user.default_context, { isAuto: true }); } } diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index 06c0c3921..b18a13377 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -1798,7 +1798,7 @@ async function loadContextSettings() { for (const instruct_preset of instruct_presets) { // If instruct preset matches the context template if (instruct_preset.name === name) { - selectInstructPreset(instruct_preset.name); + selectInstructPreset(instruct_preset.name, { isAuto: true }); break; } } diff --git a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js index ad064e4e1..c6876482b 100644 --- a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js +++ b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js @@ -33,6 +33,7 @@ export const enumIcons = { file: '📄', message: '💬', voice: '🎤', + server: '🖥️', true: '✔️', false: '❌', diff --git a/public/scripts/util/AbortReason.js b/public/scripts/util/AbortReason.js new file mode 100644 index 000000000..3218fdb87 --- /dev/null +++ b/public/scripts/util/AbortReason.js @@ -0,0 +1,9 @@ +export class AbortReason { + constructor(reason) { + this.reason = reason; + } + + toString() { + return this.reason; + } +} diff --git a/public/scripts/utils.js b/public/scripts/utils.js index 7e4deed0f..b63721c16 100644 --- a/public/scripts/utils.js +++ b/public/scripts/utils.js @@ -1436,6 +1436,15 @@ export function uuidv4() { }); } +/** + * Collapses multiple spaces in a strings into one. + * @param {string} s String to process + * @returns {string} String with collapsed spaces + */ +export function collapseSpaces(s) { + return s.replace(/\s+/g, ' ').trim(); +} + function postProcessText(text, collapse = true) { // Remove carriage returns text = text.replace(/\r/g, ''); @@ -2041,7 +2050,7 @@ export async function fetchFaFile(name) { style.remove(); return [...sheet.cssRules] .filter(rule => rule.style?.content) - .map(rule => rule.selectorText.split(/,\s*/).map(selector=>selector.split('::').shift().slice(1))) + .map(rule => rule.selectorText.split(/,\s*/).map(selector => selector.split('::').shift().slice(1))) ; } export async function fetchFa() { @@ -2068,7 +2077,7 @@ export async function showFontAwesomePicker(customList = null) { qry.placeholder = 'Filter icons'; qry.autofocus = true; const qryDebounced = debounce(() => { - const result = faList.filter(fa => fa.find(className=>className.includes(qry.value.toLowerCase()))); + const result = faList.filter(fa => fa.find(className => className.includes(qry.value.toLowerCase()))); for (const fa of faList) { if (!result.includes(fa)) { fas[fa].classList.add('hidden'); @@ -2090,7 +2099,7 @@ export async function showFontAwesomePicker(customList = null) { opt.classList.add('menu_button'); opt.classList.add('fa-solid'); opt.classList.add(fa[0]); - opt.title = fa.map(it=>it.slice(3)).join(', '); + opt.title = fa.map(it => it.slice(3)).join(', '); opt.dataset.result = POPUP_RESULT.AFFIRMATIVE.toString(); opt.addEventListener('click', () => value = fa[0]); grid.append(opt);