From 7dc1c9f7ab98346495f66cbc5a6233248eba282a Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 7 Sep 2024 00:23:42 +0300 Subject: [PATCH 01/26] Add connection manager as a core extension --- public/scripts/extensions.js | 5 + .../extensions/connection-manager/index.js | 326 ++++++++++++++++++ .../connection-manager/manifest.json | 11 + .../connection-manager/profile.html | 9 + .../connection-manager/settings.html | 21 ++ .../extensions/connection-manager/style.css | 9 + .../extensions/connection-manager/view.html | 5 + 7 files changed, 386 insertions(+) create mode 100644 public/scripts/extensions/connection-manager/index.js create mode 100644 public/scripts/extensions/connection-manager/manifest.json create mode 100644 public/scripts/extensions/connection-manager/profile.html create mode 100644 public/scripts/extensions/connection-manager/settings.html create mode 100644 public/scripts/extensions/connection-manager/style.css create mode 100644 public/scripts/extensions/connection-manager/view.html 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..d5f3882bf --- /dev/null +++ b/public/scripts/extensions/connection-manager/index.js @@ -0,0 +1,326 @@ +import { 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'; + +const MODULE_NAME = 'connection-manager'; + +const DEFAULT_SETTINGS = { + profiles: [], + selectedProfile: null, +}; + +const COMMON_COMMANDS = [ + 'api', + 'preset', + 'model', +]; + +const CC_COMMANDS = [ + ...COMMON_COMMANDS, + 'proxy', +]; + +const TC_COMMANDS = [ + ...COMMON_COMMANDS, + 'instruct', + 'context', + 'instruct-state', + 'tokenizer', +]; + +const FANCY_NAMES = { + 'api': 'API', + 'preset': 'Settings Preset', + 'model': 'Model', + 'proxy': 'Proxy Preset', + 'instruct-state': 'Instruct Mode', + 'instruct': 'Instruct Template', + 'context': 'Context Template', + 'tokenizer': 'Tokenizer', +}; + +/** + * @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, '\\|'); + +/** + * 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. + * @returns {Promise} Created connection profile + */ +async function createConnectionProfile() { + const mode = main_api === 'openai' ? 'cc' : 'tc'; + const id = 'profile-' + Math.random().toString(36).substring(2); + const profile = { + id, + mode, + }; + + await readProfileFromCommands(mode, profile); + + const profileForDisplay = makeFancyProfile(profile); + const template = await renderExtensionTemplateAsync(MODULE_NAME, 'profile', { profile: profileForDisplay }); + const suggestedName = `${profile.api} ${profile.model} - ${profile.preset}`; + const name = await callGenericPopup(template, POPUP_TYPE.INPUT, suggestedName, { rows: 2 }); + + if (!name) { + return; + } + + 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 confirm = await Popup.show.confirm('Are you sure you want to delete the selected profile?', null); + + 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; + } + + const mode = profile.mode; + const commands = mode === 'cc' ? CC_COMMANDS : TC_COMMANDS; + + for (const command of commands) { + 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.warn(`Failed to execute command: ${commandText}`, error); + } + } +} + +/** + * Updates the selected connection profile. + * @returns {Promise} + */ +async function updateConnectionProfile() { + 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; + } + + 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 = ''; + 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 {HTMLDetailsElement} details Details element + * @param {HTMLElement} detailsContent Content element of the details + */ +async function renderDetailsContent(details, detailsContent) { + detailsContent.innerHTML = ''; + if (details.open) { + 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); + + profiles.addEventListener('change', async function () { + const selectedProfile = profiles.selectedOptions[0]; + if (!selectedProfile) { + return; + } + + const profileId = selectedProfile.value; + extension_settings.connectionManager.selectedProfile = profileId; + saveSettingsDebounced(); + await renderDetailsContent(details, detailsContent); + + const profile = extension_settings.connectionManager.profiles.find(p => p.id === profileId); + + if (!profile) { + console.log(`Profile not found: ${profileId}`); + return; + } + + await applyConnectionProfile(profile); + }); + + 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(details, detailsContent); + toastr.success('Connection profile reloaded', '', { timeOut: 1500 }); + }); + + const createButton = document.getElementById('create_connection_profile'); + createButton.addEventListener('click', async () => { + const profile = await createConnectionProfile(); + extension_settings.connectionManager.profiles.push(profile); + extension_settings.connectionManager.selectedProfile = profile.id; + saveSettingsDebounced(); + renderConnectionProfiles(profiles); + await renderDetailsContent(details, detailsContent); + }); + + const updateButton = document.getElementById('update_connection_profile'); + updateButton.addEventListener('click', async () => { + await updateConnectionProfile(); + await renderDetailsContent(details, detailsContent); + saveSettingsDebounced(); + toastr.success('Connection profile updated', '', { timeOut: 1500 }); + }); + + const deleteButton = document.getElementById('delete_connection_profile'); + deleteButton.addEventListener('click', async () => { + await deleteConnectionProfile(); + renderConnectionProfiles(profiles); + await renderDetailsContent(details, detailsContent); + }); + + /** @type {HTMLDetailsElement} */ + // @ts-ignore + const details = document.getElementById('connection_profile_details'); + const detailsContent = document.getElementById('connection_profile_details_content'); + details.addEventListener('toggle', () => renderDetailsContent(details, detailsContent)); +})(); diff --git a/public/scripts/extensions/connection-manager/manifest.json b/public/scripts/extensions/connection-manager/manifest.json new file mode 100644 index 000000000..69c78eab9 --- /dev/null +++ b/public/scripts/extensions/connection-manager/manifest.json @@ -0,0 +1,11 @@ +{ + "display_name": "Connection Manager", + "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..8a54af458 --- /dev/null +++ b/public/scripts/extensions/connection-manager/profile.html @@ -0,0 +1,9 @@ +
+

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..3921859bc --- /dev/null +++ b/public/scripts/extensions/connection-manager/settings.html @@ -0,0 +1,21 @@ +
+
+

+ Connection Profile +

+
+
+
+ + + + + +
+
+ + Profile Details + +
+
+
diff --git a/public/scripts/extensions/connection-manager/style.css b/public/scripts/extensions/connection-manager/style.css new file mode 100644 index 000000000..153a14767 --- /dev/null +++ b/public/scripts/extensions/connection-manager/style.css @@ -0,0 +1,9 @@ +#connection_profile_details>summary { + cursor: pointer; + font-weight: bold; + font-size: 1.1em; +} + +#connection_profile_details ul { + margin: 5px 0; +} diff --git a/public/scripts/extensions/connection-manager/view.html b/public/scripts/extensions/connection-manager/view.html new file mode 100644 index 000000000..445f3bd0b --- /dev/null +++ b/public/scripts/extensions/connection-manager/view.html @@ -0,0 +1,5 @@ +
    + {{#each profile}} +
  • {{@key}}: {{this}}
  • + {{/each}} +
From 4c4477098d061a513684bd31a9c7aede4807aca9 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 7 Sep 2024 00:27:52 +0300 Subject: [PATCH 02/26] Fix saving null profiles --- public/scripts/extensions/connection-manager/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/public/scripts/extensions/connection-manager/index.js b/public/scripts/extensions/connection-manager/index.js index d5f3882bf..f3b3898d2 100644 --- a/public/scripts/extensions/connection-manager/index.js +++ b/public/scripts/extensions/connection-manager/index.js @@ -110,7 +110,7 @@ async function createConnectionProfile() { const name = await callGenericPopup(template, POPUP_TYPE.INPUT, suggestedName, { rows: 2 }); if (!name) { - return; + return null; } profile.name = name; @@ -296,6 +296,9 @@ async function renderDetailsContent(details, detailsContent) { 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(); From c4aa79a8e1c371c0426f6d77d86e21770cbac2b8 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 7 Sep 2024 00:30:47 +0300 Subject: [PATCH 03/26] Don't display toast on updating empty profile --- .../extensions/connection-manager/index.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/public/scripts/extensions/connection-manager/index.js b/public/scripts/extensions/connection-manager/index.js index f3b3898d2..91c0f459d 100644 --- a/public/scripts/extensions/connection-manager/index.js +++ b/public/scripts/extensions/connection-manager/index.js @@ -185,16 +185,10 @@ async function applyConnectionProfile(profile) { /** * Updates the selected connection profile. + * @param {ConnectionProfile} profile Connection profile * @returns {Promise} */ -async function updateConnectionProfile() { - 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; - } - +async function updateConnectionProfile(profile) { profile.mode = main_api === 'openai' ? 'cc' : 'tc'; await readProfileFromCommands(profile.mode, profile, true); } @@ -308,7 +302,13 @@ async function renderDetailsContent(details, detailsContent) { const updateButton = document.getElementById('update_connection_profile'); updateButton.addEventListener('click', async () => { - await updateConnectionProfile(); + 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(details, detailsContent); saveSettingsDebounced(); toastr.success('Connection profile updated', '', { timeOut: 1500 }); From d7011e8a113b82b1bfa6d229c7a657707418c0e4 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 7 Sep 2024 00:41:48 +0300 Subject: [PATCH 04/26] Add i18n attributes --- public/scripts/extensions/connection-manager/profile.html | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/public/scripts/extensions/connection-manager/profile.html b/public/scripts/extensions/connection-manager/profile.html index 8a54af458..1ee5346fe 100644 --- a/public/scripts/extensions/connection-manager/profile.html +++ b/public/scripts/extensions/connection-manager/profile.html @@ -1,9 +1,13 @@
-

Creating a Connection Profile

+

+ Creating a Connection Profile +

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

Enter a name:

+

+ Enter a name: +

From 0019da260c9662b762e70ddca6c0157262378202 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 7 Sep 2024 01:12:53 +0300 Subject: [PATCH 05/26] Use UUIDv4 for profile IDs --- public/scripts/extensions/connection-manager/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/scripts/extensions/connection-manager/index.js b/public/scripts/extensions/connection-manager/index.js index 91c0f459d..75819d2fb 100644 --- a/public/scripts/extensions/connection-manager/index.js +++ b/public/scripts/extensions/connection-manager/index.js @@ -2,6 +2,7 @@ import { 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 { uuidv4 } from '../../utils.js'; const MODULE_NAME = 'connection-manager'; @@ -96,7 +97,7 @@ async function readProfileFromCommands(mode, profile, cleanUp = false) { */ async function createConnectionProfile() { const mode = main_api === 'openai' ? 'cc' : 'tc'; - const id = 'profile-' + Math.random().toString(36).substring(2); + const id = uuidv4(); const profile = { id, mode, From 916d73da4cbfa1fb69f02b63e26a90c2d1a6288a Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 7 Sep 2024 01:18:35 +0300 Subject: [PATCH 06/26] Display profile name in delete confirmation --- public/scripts/extensions/connection-manager/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/scripts/extensions/connection-manager/index.js b/public/scripts/extensions/connection-manager/index.js index 75819d2fb..ee3344535 100644 --- a/public/scripts/extensions/connection-manager/index.js +++ b/public/scripts/extensions/connection-manager/index.js @@ -133,7 +133,8 @@ async function deleteConnectionProfile() { return; } - const confirm = await Popup.show.confirm('Are you sure you want to delete the selected profile?', null); + 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; From e77f5a1c93bcbf93e06a20718fc032c529ee348a Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 7 Sep 2024 13:24:56 +0300 Subject: [PATCH 07/26] Set api-url with connection-manager --- public/scripts/extensions/connection-manager/index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/public/scripts/extensions/connection-manager/index.js b/public/scripts/extensions/connection-manager/index.js index ee3344535..90cbe7039 100644 --- a/public/scripts/extensions/connection-manager/index.js +++ b/public/scripts/extensions/connection-manager/index.js @@ -14,6 +14,7 @@ const DEFAULT_SETTINGS = { const COMMON_COMMANDS = [ 'api', 'preset', + 'api-url', 'model', ]; @@ -32,6 +33,7 @@ const TC_COMMANDS = [ const FANCY_NAMES = { 'api': 'API', + 'api-url': 'Server URL', 'preset': 'Settings Preset', 'model': 'Model', 'proxy': 'Proxy Preset', @@ -57,6 +59,8 @@ const FANCY_NAMES = { */ const escapeArgument = (a) => a.replace(/"/g, '\\"').replace(/\|/g, '\\|'); +const collapseSpaces = (s) => s.replace(/\s+/g, ' ').trim(); +const makeUnique = (s, f, i) => { if (!f(s)) { return s; } else { while (f(`${s} (${i})`)) { i++; } return `${s} (${i})`; } }; /** * Reads the connection profile from the commands. @@ -107,7 +111,8 @@ async function createConnectionProfile() { const profileForDisplay = makeFancyProfile(profile); const template = await renderExtensionTemplateAsync(MODULE_NAME, 'profile', { profile: profileForDisplay }); - const suggestedName = `${profile.api} ${profile.model} - ${profile.preset}`; + const checkName = (n) => extension_settings.connectionManager.profiles.some(p => p.name === n); + const suggestedName = makeUnique(collapseSpaces(`${profile.api ?? ''} ${profile.model ?? ''} - ${profile.preset ?? ''}`), checkName, 1); const name = await callGenericPopup(template, POPUP_TYPE.INPUT, suggestedName, { rows: 2 }); if (!name) { From c77c3d8f37d976700c977dcc58632425785d6c14 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 7 Sep 2024 13:30:26 +0300 Subject: [PATCH 08/26] Extend quiet effect in /model --- public/scripts/slash-commands.js | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 520572b8e..ef95ca495 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -1532,7 +1532,7 @@ export function initDefaultSlashCommands() { 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)), + enumProvider: () => getModelOptions(true)?.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.', @@ -3383,10 +3383,12 @@ function setBackgroundCallback(_, bg) { /** * Retrieves the available model options based on the currently selected main API and its subtype + * @param {boolean} quiet - Whether to suppress toasts * * @returns {{control: HTMLSelectElement, options: HTMLOptionElement[]}?} An array of objects representing the available model options, or null if not supported */ -function getModelOptions() { +function getModelOptions(quiet) { + const nullResult = { control: null, options: null }; const modelSelectMap = [ { id: 'model_togetherai_select', api: 'textgenerationwebui', type: textgen_types.TOGETHERAI }, { id: 'openrouter_model', api: 'textgenerationwebui', type: textgen_types.OPENROUTER }, @@ -3420,7 +3422,7 @@ function getModelOptions() { case 'openai': return oai_settings.chat_completion_source; default: - return null; + return nullResult; } } @@ -3428,15 +3430,15 @@ function getModelOptions() { const modelSelectItem = modelSelectMap.find(x => x.api == main_api && x.type == apiSubType)?.id; if (!modelSelectItem) { - toastr.info('Setting a model for your API is not supported or not implemented yet.'); - return null; + !quiet && toastr.info('Setting a model for your API is not supported or not implemented yet.'); + return nullResult; } const modelSelectControl = document.getElementById(modelSelectItem); if (!(modelSelectControl instanceof HTMLSelectElement)) { - toastr.error(`Model select control not found: ${main_api}[${apiSubType}]`); - return null; + !quiet && toastr.error(`Model select control not found: ${main_api}[${apiSubType}]`); + return nullResult; } const options = Array.from(modelSelectControl.options); @@ -3450,7 +3452,8 @@ function getModelOptions() { * @returns {string} New or existing model name */ function modelCallback(args, model) { - const { control: modelSelectControl, options } = getModelOptions(); + const quiet = isTrueBoolean(args?.quiet); + const { control: modelSelectControl, options } = getModelOptions(quiet); // If no model was found, the reason was already logged, we just return here if (options === null) { @@ -3458,7 +3461,7 @@ function modelCallback(args, model) { } if (!options.length) { - toastr.warning('No model options found. Check your API settings.'); + !quiet && toastr.warning('No model options found. Check your API settings.'); return ''; } @@ -3489,11 +3492,10 @@ function modelCallback(args, model) { if (newSelectedOption) { modelSelectControl.value = newSelectedOption.value; $(modelSelectControl).trigger('change'); - const quiet = isTrueBoolean(args?.quiet); !quiet && toastr.success(`Model set to "${newSelectedOption.text}"`); return newSelectedOption.value; } else { - toastr.warning(`No model found with name "${model}"`); + !quiet && toastr.warning(`No model found with name "${model}"`); return ''; } } From 6d9c64f38e55ea4f4aa6a77880942eef3124dfa5 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 7 Sep 2024 13:36:19 +0300 Subject: [PATCH 09/26] Add dynamic i18n to profile views --- public/scripts/extensions/connection-manager/profile.html | 2 +- public/scripts/extensions/connection-manager/view.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/scripts/extensions/connection-manager/profile.html b/public/scripts/extensions/connection-manager/profile.html index 1ee5346fe..d8be91561 100644 --- a/public/scripts/extensions/connection-manager/profile.html +++ b/public/scripts/extensions/connection-manager/profile.html @@ -4,7 +4,7 @@
    {{#each profile}} -
  • {{@key}}: {{this}}
  • +
  • {{@key}}: {{this}}
  • {{/each}}

diff --git a/public/scripts/extensions/connection-manager/view.html b/public/scripts/extensions/connection-manager/view.html index 445f3bd0b..42d1ad35f 100644 --- a/public/scripts/extensions/connection-manager/view.html +++ b/public/scripts/extensions/connection-manager/view.html @@ -1,5 +1,5 @@
    {{#each profile}} -
  • {{@key}}: {{this}}
  • +
  • {{@key}}: {{this}}
  • {{/each}}
From bcac8c065b044028937d0482c7255989d00b8b45 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 7 Sep 2024 13:58:46 +0300 Subject: [PATCH 10/26] Add command for switching profiles --- .../extensions/connection-manager/index.js | 64 ++++++++++++++++++- .../SlashCommandCommonEnumsProvider.js | 1 + 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/public/scripts/extensions/connection-manager/index.js b/public/scripts/extensions/connection-manager/index.js index 90cbe7039..d15d1299c 100644 --- a/public/scripts/extensions/connection-manager/index.js +++ b/public/scripts/extensions/connection-manager/index.js @@ -2,9 +2,15 @@ import { 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 { SlashCommandArgument } from '../../slash-commands/SlashCommandArgument.js'; +import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; +import { enumTypes, SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js'; +import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; import { uuidv4 } from '../../utils.js'; const MODULE_NAME = 'connection-manager'; +const NONE = ''; const DEFAULT_SETTINGS = { profiles: [], @@ -43,6 +49,12 @@ const FANCY_NAMES = { 'tokenizer': 'Tokenizer', }; +/** @type {() => SlashCommandEnumValue[]} */ +const profilesProvider = () => [ + new SlashCommandEnumValue(NONE, NONE), + ...extension_settings.connectionManager.profiles.map(p => new SlashCommandEnumValue(p.name, p.name, enumTypes.name, enumIcons.server)), +]; + /** * @typedef {Object} ConnectionProfile * @property {string} id Unique identifier @@ -209,7 +221,7 @@ function renderConnectionProfiles(profiles) { const noneOption = document.createElement('option'); noneOption.value = ''; - noneOption.textContent = ''; + noneOption.textContent = NONE; noneOption.selected = !extension_settings.connectionManager.selectedProfile; profiles.appendChild(noneOption); @@ -333,4 +345,54 @@ async function renderDetailsContent(details, detailsContent) { const details = document.getElementById('connection_profile_details'); const detailsContent = document.getElementById('connection_profile_details_content'); details.addEventListener('toggle', () => renderDetailsContent(details, 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.', + 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 NONE; + } + return profile.name; + } + + if (value === NONE) { + profiles.selectedIndex = 0; + profiles.dispatchEvent(new Event('change')); + return NONE; + } + + // Try to find exact match + const profile = extension_settings.connectionManager.profiles.find(p => p.name === value); + + if (profile) { + profiles.selectedIndex = Array.from(profiles.options).findIndex(o => o.value === profile.id); + profiles.dispatchEvent(new Event('change')); + return profile.name; + } + + // 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 ''; + } + + const bestMatch = results[0]; + profiles.value = bestMatch.item.id; + profiles.dispatchEvent(new Event('change')); + return bestMatch.item.name; + }, + })); })(); 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: '❌', From aa3cb62b4c61db926d95f87f58459c07c433e3d6 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 7 Sep 2024 14:03:00 +0300 Subject: [PATCH 11/26] Fix /echo escapeHtml being false by default --- public/scripts/slash-commands.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index ef95ca495..93721eba4 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -2299,7 +2299,7 @@ async function echoCallback(args, value) { if (args.extendedTimeout && !isNaN(parseInt(args.extendedTimeout))) options.extendedTimeOut = parseInt(args.extendedTimeout); if (isTrueBoolean(args.preventDuplicates)) options.preventDuplicates = true; if (args.cssClass) options.toastClass = args.cssClass; - if (args.escapeHtml !== undefined) options.escapeHtml = isTrueBoolean(args.escapeHtml); + options.escapeHtml = args.escapeHtml !== undefined ? isTrueBoolean(args.escapeHtml) : true; // Prepare possible await handling let awaitDismissal = isTrueBoolean(args.awaitDismissal); From 065daa7599589d627a6827d14a78f6842149a124 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 7 Sep 2024 14:56:30 +0300 Subject: [PATCH 12/26] Add commands to list, create, update profiles --- .../extensions/connection-manager/index.js | 59 ++++++++++++++++++- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/public/scripts/extensions/connection-manager/index.js b/public/scripts/extensions/connection-manager/index.js index d15d1299c..7d9ac16cc 100644 --- a/public/scripts/extensions/connection-manager/index.js +++ b/public/scripts/extensions/connection-manager/index.js @@ -3,7 +3,7 @@ import { extension_settings, renderExtensionTemplateAsync } from '../../extensio import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js'; import { executeSlashCommandsWithOptions } from '../../slash-commands.js'; import { SlashCommand } from '../../slash-commands/SlashCommand.js'; -import { SlashCommandArgument } from '../../slash-commands/SlashCommandArgument.js'; +import { ARGUMENT_TYPE, SlashCommandArgument } from '../../slash-commands/SlashCommandArgument.js'; import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; import { enumTypes, SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js'; import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; @@ -109,9 +109,10 @@ async function readProfileFromCommands(mode, profile, cleanUp = false) { /** * Creates a new connection profile. + * @param {string} [forceName] Name of the connection profile * @returns {Promise} Created connection profile */ -async function createConnectionProfile() { +async function createConnectionProfile(forceName = null) { const mode = main_api === 'openai' ? 'cc' : 'tc'; const id = uuidv4(); const profile = { @@ -125,7 +126,7 @@ async function createConnectionProfile() { const template = await renderExtensionTemplateAsync(MODULE_NAME, 'profile', { profile: profileForDisplay }); const checkName = (n) => extension_settings.connectionManager.profiles.some(p => p.name === n); const suggestedName = makeUnique(collapseSpaces(`${profile.api ?? ''} ${profile.model ?? ''} - ${profile.preset ?? ''}`), checkName, 1); - const name = await callGenericPopup(template, POPUP_TYPE.INPUT, suggestedName, { rows: 2 }); + const name = forceName ?? await callGenericPopup(template, POPUP_TYPE.INPUT, suggestedName, { rows: 2 }); if (!name) { return null; @@ -349,6 +350,7 @@ async function renderDetailsContent(details, 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', @@ -395,4 +397,55 @@ async function renderDetailsContent(details, detailsContent) { return bestMatch.item.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(details, 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) { + return ''; + } + await updateConnectionProfile(profile); + await renderDetailsContent(details, detailsContent); + saveSettingsDebounced(); + }, + })); })(); From 4fd7828a9bb8cb9aff530d8db5dea03333b9b693 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 7 Sep 2024 17:13:32 +0300 Subject: [PATCH 13/26] Add /profile-get command --- .../extensions/connection-manager/index.js | 75 +++++++++++++++---- 1 file changed, 59 insertions(+), 16 deletions(-) diff --git a/public/scripts/extensions/connection-manager/index.js b/public/scripts/extensions/connection-manager/index.js index 7d9ac16cc..f54000b5d 100644 --- a/public/scripts/extensions/connection-manager/index.js +++ b/public/scripts/extensions/connection-manager/index.js @@ -74,6 +74,31 @@ const escapeArgument = (a) => a.replace(/"/g, '\\"').replace(/\|/g, '\\|'); const collapseSpaces = (s) => s.replace(/\s+/g, ' ').trim(); const makeUnique = (s, f, i) => { if (!f(s)) { return s; } else { while (f(`${s} (${i})`)) { i++; } return `${s} (${i})`; } }; +/** + * 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 @@ -374,27 +399,16 @@ async function renderDetailsContent(details, detailsContent) { return NONE; } - // Try to find exact match - const profile = extension_settings.connectionManager.profiles.find(p => p.name === value); + const profile = findProfileByName(value); - if (profile) { - profiles.selectedIndex = Array.from(profiles.options).findIndex(o => o.value === profile.id); - profiles.dispatchEvent(new Event('change')); - return profile.name; - } - - // Try to find fuzzy match - const fuse = new Fuse(extension_settings.connectionManager.profiles, { keys: ['name'] }); - const results = fuse.search(value); - - if (results.length === 0) { + if (!profile) { return ''; } - const bestMatch = results[0]; - profiles.value = bestMatch.item.id; + profiles.selectedIndex = Array.from(profiles.options).findIndex(o => o.value === profile.id); profiles.dispatchEvent(new Event('change')); - return bestMatch.item.name; + + return profiles.name; }, })); @@ -448,4 +462,33 @@ async function renderDetailsContent(details, detailsContent) { saveSettingsDebounced(); }, })); + + 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); + }, + })); })(); From 7e64d216c31fbf72fd8cdb4fdbc161b1383ec8a4 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sat, 7 Sep 2024 19:32:58 +0200 Subject: [PATCH 14/26] Fix type not returning profile name --- public/scripts/extensions/connection-manager/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/extensions/connection-manager/index.js b/public/scripts/extensions/connection-manager/index.js index f54000b5d..572152fba 100644 --- a/public/scripts/extensions/connection-manager/index.js +++ b/public/scripts/extensions/connection-manager/index.js @@ -408,7 +408,7 @@ async function renderDetailsContent(details, detailsContent) { profiles.selectedIndex = Array.from(profiles.options).findIndex(o => o.value === profile.id); profiles.dispatchEvent(new Event('change')); - return profiles.name; + return profile.name; }, })); From 00f6941e936a68e7c9cbdc3acb879a577eab2297 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 7 Sep 2024 21:08:37 +0300 Subject: [PATCH 15/26] Fix review comments --- public/scripts/extensions/connection-manager/index.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/public/scripts/extensions/connection-manager/index.js b/public/scripts/extensions/connection-manager/index.js index 572152fba..483a9d97d 100644 --- a/public/scripts/extensions/connection-manager/index.js +++ b/public/scripts/extensions/connection-manager/index.js @@ -52,7 +52,7 @@ const FANCY_NAMES = { /** @type {() => SlashCommandEnumValue[]} */ const profilesProvider = () => [ new SlashCommandEnumValue(NONE, NONE), - ...extension_settings.connectionManager.profiles.map(p => new SlashCommandEnumValue(p.name, p.name, enumTypes.name, enumIcons.server)), + ...extension_settings.connectionManager.profiles.map(p => new SlashCommandEnumValue(p.name, null, enumTypes.name, enumIcons.server)), ]; /** @@ -309,6 +309,11 @@ async function renderDetailsContent(details, detailsContent) { saveSettingsDebounced(); await renderDetailsContent(details, detailsContent); + // None option selected + if (!profileId) { + return; + } + const profile = extension_settings.connectionManager.profiles.find(p => p.id === profileId); if (!profile) { @@ -455,11 +460,13 @@ async function renderDetailsContent(details, detailsContent) { 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(details, detailsContent); saveSettingsDebounced(); + return profile.name; }, })); From 70ff35b4c3d141aade727405d870a7e1bb2e3e80 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 7 Sep 2024 21:11:06 +0300 Subject: [PATCH 16/26] Use consistent icons --- public/scripts/extensions/connection-manager/settings.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/scripts/extensions/connection-manager/settings.html b/public/scripts/extensions/connection-manager/settings.html index 3921859bc..0cf9acb67 100644 --- a/public/scripts/extensions/connection-manager/settings.html +++ b/public/scripts/extensions/connection-manager/settings.html @@ -7,10 +7,10 @@
- + - - + +
From 827fce4542c646a401575b2c0da816d5d661af9d Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 7 Sep 2024 21:19:33 +0300 Subject: [PATCH 17/26] Move string utils to shared module --- .../extensions/connection-manager/index.js | 6 ++---- public/scripts/utils.js | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/public/scripts/extensions/connection-manager/index.js b/public/scripts/extensions/connection-manager/index.js index 483a9d97d..3b356f2f7 100644 --- a/public/scripts/extensions/connection-manager/index.js +++ b/public/scripts/extensions/connection-manager/index.js @@ -7,7 +7,7 @@ import { ARGUMENT_TYPE, SlashCommandArgument } from '../../slash-commands/SlashC import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; import { enumTypes, SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js'; import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; -import { uuidv4 } from '../../utils.js'; +import { collapseSpaces, getUniqueName, uuidv4 } from '../../utils.js'; const MODULE_NAME = 'connection-manager'; const NONE = ''; @@ -71,8 +71,6 @@ const profilesProvider = () => [ */ const escapeArgument = (a) => a.replace(/"/g, '\\"').replace(/\|/g, '\\|'); -const collapseSpaces = (s) => s.replace(/\s+/g, ' ').trim(); -const makeUnique = (s, f, i) => { if (!f(s)) { return s; } else { while (f(`${s} (${i})`)) { i++; } return `${s} (${i})`; } }; /** * Finds the best match for the search value. @@ -150,7 +148,7 @@ async function createConnectionProfile(forceName = null) { const profileForDisplay = makeFancyProfile(profile); const template = await renderExtensionTemplateAsync(MODULE_NAME, 'profile', { profile: profileForDisplay }); const checkName = (n) => extension_settings.connectionManager.profiles.some(p => p.name === n); - const suggestedName = makeUnique(collapseSpaces(`${profile.api ?? ''} ${profile.model ?? ''} - ${profile.preset ?? ''}`), checkName, 1); + const suggestedName = getUniqueName(collapseSpaces(`${profile.api ?? ''} ${profile.model ?? ''} - ${profile.preset ?? ''}`), checkName); const name = forceName ?? await callGenericPopup(template, POPUP_TYPE.INPUT, suggestedName, { rows: 2 }); if (!name) { 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); From 739d0c95c3b1b2d6565a58aff06ba378b3fc5428 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 7 Sep 2024 21:21:46 +0300 Subject: [PATCH 18/26] Require unique names for profiles --- public/scripts/extensions/connection-manager/index.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/public/scripts/extensions/connection-manager/index.js b/public/scripts/extensions/connection-manager/index.js index 3b356f2f7..77f6cf6e4 100644 --- a/public/scripts/extensions/connection-manager/index.js +++ b/public/scripts/extensions/connection-manager/index.js @@ -147,14 +147,19 @@ async function createConnectionProfile(forceName = null) { const profileForDisplay = makeFancyProfile(profile); const template = await renderExtensionTemplateAsync(MODULE_NAME, 'profile', { profile: profileForDisplay }); - const checkName = (n) => extension_settings.connectionManager.profiles.some(p => p.name === n); - const suggestedName = getUniqueName(collapseSpaces(`${profile.api ?? ''} ${profile.model ?? ''} - ${profile.preset ?? ''}`), checkName); + 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)) { + toastr.error('A profile with the same name already exists.'); + return null; + } + profile.name = name; return profile; } From 10ddf77948e0d93af1f14a8ea9801ce8a4b5d397 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 7 Sep 2024 21:33:31 +0300 Subject: [PATCH 19/26] Await for profiles loading before continuing --- public/script.js | 1 + .../extensions/connection-manager/index.js | 39 +++++++++++++++---- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/public/script.js b/public/script.js index 099651236..a227c6e35 100644 --- a/public/script.js +++ b/public/script.js @@ -462,6 +462,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(); diff --git a/public/scripts/extensions/connection-manager/index.js b/public/scripts/extensions/connection-manager/index.js index 77f6cf6e4..3814be945 100644 --- a/public/scripts/extensions/connection-manager/index.js +++ b/public/scripts/extensions/connection-manager/index.js @@ -1,13 +1,13 @@ -import { main_api, saveSettingsDebounced } from '../../../script.js'; +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 } from '../../slash-commands/SlashCommandArgument.js'; -import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.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, uuidv4 } from '../../utils.js'; +import { collapseSpaces, getUniqueName, isFalseBoolean, uuidv4 } from '../../utils.js'; const MODULE_NAME = 'connection-manager'; const NONE = ''; @@ -51,7 +51,7 @@ const FANCY_NAMES = { /** @type {() => SlashCommandEnumValue[]} */ const profilesProvider = () => [ - new SlashCommandEnumValue(NONE, NONE), + new SlashCommandEnumValue(NONE), ...extension_settings.connectionManager.profiles.map(p => new SlashCommandEnumValue(p.name, null, enumTypes.name, enumIcons.server)), ]; @@ -155,7 +155,7 @@ async function createConnectionProfile(forceName = null) { return null; } - if (isNameTaken(name)) { + if (isNameTaken(name) || name === NONE) { toastr.error('A profile with the same name already exists.'); return null; } @@ -304,6 +304,8 @@ async function renderDetailsContent(details, detailsContent) { 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; } @@ -314,6 +316,7 @@ async function renderDetailsContent(details, detailsContent) { // None option selected if (!profileId) { + await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, NONE); return; } @@ -325,6 +328,7 @@ async function renderDetailsContent(details, detailsContent) { } await applyConnectionProfile(profile); + await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name); }); const reloadButton = document.getElementById('reload_connection_profile'); @@ -337,6 +341,7 @@ async function renderDetailsContent(details, detailsContent) { } await applyConnectionProfile(profile); await renderDetailsContent(details, detailsContent); + await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name); toastr.success('Connection profile reloaded', '', { timeOut: 1500 }); }); @@ -351,6 +356,7 @@ async function renderDetailsContent(details, detailsContent) { saveSettingsDebounced(); renderConnectionProfiles(profiles); await renderDetailsContent(details, detailsContent); + await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name); }); const updateButton = document.getElementById('update_connection_profile'); @@ -364,6 +370,7 @@ async function renderDetailsContent(details, detailsContent) { await updateConnectionProfile(profile); await renderDetailsContent(details, detailsContent); saveSettingsDebounced(); + await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name); toastr.success('Connection profile updated', '', { timeOut: 1500 }); }); @@ -372,6 +379,7 @@ async function renderDetailsContent(details, detailsContent) { await deleteConnectionProfile(); renderConnectionProfiles(profiles); await renderDetailsContent(details, detailsContent); + await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, NONE); }); /** @type {HTMLDetailsElement} */ @@ -391,7 +399,17 @@ async function renderDetailsContent(details, detailsContent) { isRequired: false, }), ], - callback: async (_args, value) => { + 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); @@ -413,9 +431,16 @@ async function renderDetailsContent(details, detailsContent) { 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; }, })); From d99dfb91682a07e9fbc4d14ffd140161d83805ef Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 7 Sep 2024 23:53:22 +0300 Subject: [PATCH 20/26] Add spinner to indicate profile application --- .../extensions/connection-manager/index.js | 55 ++++++++++++++++++- .../connection-manager/settings.html | 5 +- .../extensions/connection-manager/style.css | 4 ++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/public/scripts/extensions/connection-manager/index.js b/public/scripts/extensions/connection-manager/index.js index 3814be945..7bd5f0fe1 100644 --- a/public/scripts/extensions/connection-manager/index.js +++ b/public/scripts/extensions/connection-manager/index.js @@ -49,6 +49,48 @@ const FANCY_NAMES = { '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), @@ -214,10 +256,19 @@ async function applyConnectionProfile(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; @@ -226,9 +277,11 @@ async function applyConnectionProfile(profile) { try { await executeSlashCommandsWithOptions(commandText, { handleParserErrors: false, handleExecutionErrors: false }); } catch (error) { - console.warn(`Failed to execute command: ${commandText}`, error); + console.error(`Failed to execute command: ${commandText}`, error); } } + + spinner.stop(); } /** diff --git a/public/scripts/extensions/connection-manager/settings.html b/public/scripts/extensions/connection-manager/settings.html index 0cf9acb67..598e660be 100644 --- a/public/scripts/extensions/connection-manager/settings.html +++ b/public/scripts/extensions/connection-manager/settings.html @@ -13,8 +13,9 @@
- - Profile Details + + Profile Details +
diff --git a/public/scripts/extensions/connection-manager/style.css b/public/scripts/extensions/connection-manager/style.css index 153a14767..a3255d032 100644 --- a/public/scripts/extensions/connection-manager/style.css +++ b/public/scripts/extensions/connection-manager/style.css @@ -7,3 +7,7 @@ #connection_profile_details ul { margin: 5px 0; } + +#connection_profile_spinner { + margin-lefT: 5px; +} From 7952b5f2c9a255d0f18a145a653a425d2e3ab13d Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sat, 7 Sep 2024 23:27:46 +0200 Subject: [PATCH 21/26] Handle aborting status check gracefully --- public/script.js | 16 +++++++++++----- public/scripts/util/AbortReason.js | 9 +++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 public/scripts/util/AbortReason.js diff --git a/public/script.js b/public/script.js index 214a27c34..1b2537aac 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 { @@ -978,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'); } @@ -1229,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'); } @@ -9317,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; @@ -10076,7 +10082,7 @@ jQuery(async function () { }); $('#main_api').change(function () { - cancelStatusCheck(); + cancelStatusCheck("Canceled because main api changed"); changeMainAPI(); saveSettingsDebounced(); }); 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; + } +} From 38751d4fe20b0aa1ef3a3e0ff2952569dd5da220 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 8 Sep 2024 01:19:52 +0300 Subject: [PATCH 22/26] Replace details view toggle with a button --- public/script.js | 2 +- .../extensions/connection-manager/index.js | 51 ++++++++++--------- .../connection-manager/settings.html | 9 +--- .../extensions/connection-manager/style.css | 16 +++--- 4 files changed, 37 insertions(+), 41 deletions(-) diff --git a/public/script.js b/public/script.js index 1b2537aac..69069cb3f 100644 --- a/public/script.js +++ b/public/script.js @@ -10082,7 +10082,7 @@ jQuery(async function () { }); $('#main_api').change(function () { - cancelStatusCheck("Canceled because main api changed"); + cancelStatusCheck('Canceled because main api changed'); changeMainAPI(); saveSettingsDebounced(); }); diff --git a/public/scripts/extensions/connection-manager/index.js b/public/scripts/extensions/connection-manager/index.js index 7bd5f0fe1..583738f72 100644 --- a/public/scripts/extensions/connection-manager/index.js +++ b/public/scripts/extensions/connection-manager/index.js @@ -67,7 +67,7 @@ class ConnectionManagerSpinner { constructor() { // @ts-ignore this.spinnerElement = document.getElementById('connection_profile_spinner'); - this.abortController = new AbortController(); + this.abortController = new AbortController(); } start() { @@ -318,21 +318,21 @@ function renderConnectionProfiles(profiles) { /** * Renders the content of the details element. - * @param {HTMLDetailsElement} details Details element * @param {HTMLElement} detailsContent Content element of the details */ -async function renderDetailsContent(details, detailsContent) { +async function renderDetailsContent(detailsContent) { detailsContent.innerHTML = ''; - if (details.open) { - 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'; - } + 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'; } } @@ -365,7 +365,7 @@ async function renderDetailsContent(details, detailsContent) { const profileId = selectedProfile.value; extension_settings.connectionManager.selectedProfile = profileId; saveSettingsDebounced(); - await renderDetailsContent(details, detailsContent); + await renderDetailsContent(detailsContent); // None option selected if (!profileId) { @@ -393,7 +393,7 @@ async function renderDetailsContent(details, detailsContent) { return; } await applyConnectionProfile(profile); - await renderDetailsContent(details, detailsContent); + await renderDetailsContent(detailsContent); await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name); toastr.success('Connection profile reloaded', '', { timeOut: 1500 }); }); @@ -408,7 +408,7 @@ async function renderDetailsContent(details, detailsContent) { extension_settings.connectionManager.selectedProfile = profile.id; saveSettingsDebounced(); renderConnectionProfiles(profiles); - await renderDetailsContent(details, detailsContent); + await renderDetailsContent(detailsContent); await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name); }); @@ -421,7 +421,7 @@ async function renderDetailsContent(details, detailsContent) { return; } await updateConnectionProfile(profile); - await renderDetailsContent(details, detailsContent); + await renderDetailsContent(detailsContent); saveSettingsDebounced(); await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name); toastr.success('Connection profile updated', '', { timeOut: 1500 }); @@ -431,15 +431,18 @@ async function renderDetailsContent(details, detailsContent) { deleteButton.addEventListener('click', async () => { await deleteConnectionProfile(); renderConnectionProfiles(profiles); - await renderDetailsContent(details, detailsContent); + await renderDetailsContent(detailsContent); await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, NONE); }); - /** @type {HTMLDetailsElement} */ - // @ts-ignore - const details = document.getElementById('connection_profile_details'); + /** @type {HTMLElement} */ + const viewDetails = document.getElementById('view_connection_profile'); const detailsContent = document.getElementById('connection_profile_details_content'); - details.addEventListener('toggle', () => renderDetailsContent(details, detailsContent)); + viewDetails.addEventListener('click', async () => { + viewDetails.classList.toggle('active'); + detailsContent.classList.toggle('hidden'); + await renderDetailsContent(detailsContent); + }); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'profile', @@ -529,7 +532,7 @@ async function renderDetailsContent(details, detailsContent) { extension_settings.connectionManager.selectedProfile = profile.id; saveSettingsDebounced(); renderConnectionProfiles(profiles); - await renderDetailsContent(details, detailsContent); + await renderDetailsContent(detailsContent); return profile.name; }, })); @@ -545,7 +548,7 @@ async function renderDetailsContent(details, detailsContent) { return ''; } await updateConnectionProfile(profile); - await renderDetailsContent(details, detailsContent); + await renderDetailsContent(detailsContent); saveSettingsDebounced(); return profile.name; }, diff --git a/public/scripts/extensions/connection-manager/settings.html b/public/scripts/extensions/connection-manager/settings.html index 598e660be..c3419d1d5 100644 --- a/public/scripts/extensions/connection-manager/settings.html +++ b/public/scripts/extensions/connection-manager/settings.html @@ -7,16 +7,11 @@
+
-
- - Profile Details - - -
-
+ diff --git a/public/scripts/extensions/connection-manager/style.css b/public/scripts/extensions/connection-manager/style.css index a3255d032..efd046895 100644 --- a/public/scripts/extensions/connection-manager/style.css +++ b/public/scripts/extensions/connection-manager/style.css @@ -1,13 +1,11 @@ -#connection_profile_details>summary { - cursor: pointer; - font-weight: bold; - font-size: 1.1em; -} - -#connection_profile_details ul { +#connection_profile_details_content { margin: 5px 0; } -#connection_profile_spinner { - margin-lefT: 5px; +#connection_profile_details_content ul { + margin: 0; +} + +#connection_profile_spinner { + margin-left: 5px; } From 6aa9608b8e05b457eee5aee37594f50e1b84bee4 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 8 Sep 2024 01:21:04 +0300 Subject: [PATCH 23/26] Forgor the spinner --- public/scripts/extensions/connection-manager/settings.html | 1 + 1 file changed, 1 insertion(+) diff --git a/public/scripts/extensions/connection-manager/settings.html b/public/scripts/extensions/connection-manager/settings.html index c3419d1d5..b77b91416 100644 --- a/public/scripts/extensions/connection-manager/settings.html +++ b/public/scripts/extensions/connection-manager/settings.html @@ -4,6 +4,7 @@ Connection Profile

+
From 44ddec88febe0ef433153546938058b1658ad869 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 8 Sep 2024 01:35:59 +0300 Subject: [PATCH 24/26] Rename extension manifest --- public/scripts/extensions/connection-manager/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/extensions/connection-manager/manifest.json b/public/scripts/extensions/connection-manager/manifest.json index 69c78eab9..601f8970c 100644 --- a/public/scripts/extensions/connection-manager/manifest.json +++ b/public/scripts/extensions/connection-manager/manifest.json @@ -1,5 +1,5 @@ { - "display_name": "Connection Manager", + "display_name": "Connection Profiles", "loading_order": 1, "requires": [], "optional": [], From 4ba99412af473695225bfccf10c8bc8930479f82 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sun, 8 Sep 2024 01:00:50 +0200 Subject: [PATCH 25/26] Refactor args for context/instruct select --- public/script.js | 4 ++-- public/scripts/instruct-mode.js | 24 ++++++++++++++---------- public/scripts/power-user.js | 2 +- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/public/script.js b/public/script.js index 69069cb3f..3a8d5f525 100644 --- a/public/script.js +++ b/public/script.js @@ -8526,7 +8526,7 @@ async function selectContextCallback(args, name) { } const foundName = result[0].item; - selectContextPreset(foundName, quiet); + selectContextPreset(foundName, { quiet: quiet }); return foundName; } @@ -8546,7 +8546,7 @@ async function selectInstructCallback(args, name) { } const foundName = result[0].item; - selectInstructPreset(foundName, quiet); + selectInstructPreset(foundName, { quiet: quiet }); return foundName; } 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; } } From c7650576845dca33ee3e4ef2beb44e0ad635f6c0 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sun, 8 Sep 2024 01:18:44 +0200 Subject: [PATCH 26/26] Disable buttons on option --- public/scripts/extensions/connection-manager/index.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/public/scripts/extensions/connection-manager/index.js b/public/scripts/extensions/connection-manager/index.js index 583738f72..7f30a8cc7 100644 --- a/public/scripts/extensions/connection-manager/index.js +++ b/public/scripts/extensions/connection-manager/index.js @@ -354,6 +354,13 @@ async function renderDetailsContent(detailsContent) { 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) { @@ -367,6 +374,8 @@ async function renderDetailsContent(detailsContent) { saveSettingsDebounced(); await renderDetailsContent(detailsContent); + toggleProfileSpecificButtons(); + // None option selected if (!profileId) { await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, NONE);