mirror of
				https://github.com/SillyTavern/SillyTavern.git
				synced 2025-06-05 21:59:27 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			735 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			735 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import { Fuse } from '../../../lib.js';
 | |
| 
 | |
| import { event_types, eventSource, main_api, saveSettingsDebounced } from '../../../script.js';
 | |
| import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';
 | |
| import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js';
 | |
| import { SlashCommand } from '../../slash-commands/SlashCommand.js';
 | |
| import { SlashCommandAbortController } from '../../slash-commands/SlashCommandAbortController.js';
 | |
| import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
 | |
| import { commonEnumProviders, enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
 | |
| import { SlashCommandDebugController } from '../../slash-commands/SlashCommandDebugController.js';
 | |
| import { enumTypes, SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
 | |
| import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
 | |
| import { SlashCommandScope } from '../../slash-commands/SlashCommandScope.js';
 | |
| import { collapseSpaces, getUniqueName, isFalseBoolean, uuidv4 } from '../../utils.js';
 | |
| import { t } from '../../i18n.js';
 | |
| 
 | |
| const MODULE_NAME = 'connection-manager';
 | |
| const NONE = '<None>';
 | |
| 
 | |
| const DEFAULT_SETTINGS = {
 | |
|     profiles: [],
 | |
|     selectedProfile: null,
 | |
| };
 | |
| 
 | |
| const CC_COMMANDS = [
 | |
|     'api',
 | |
|     'preset',
 | |
|     // Do not fix; CC needs to set the API twice because it could be overridden by the preset
 | |
|     'api',
 | |
|     'api-url',
 | |
|     'model',
 | |
|     'proxy',
 | |
|     'stop-strings',
 | |
| ];
 | |
| 
 | |
| const TC_COMMANDS = [
 | |
|     'api',
 | |
|     'preset',
 | |
|     'api-url',
 | |
|     'model',
 | |
|     'sysprompt',
 | |
|     'sysprompt-state',
 | |
|     'instruct',
 | |
|     'context',
 | |
|     'instruct-state',
 | |
|     'tokenizer',
 | |
|     'stop-strings',
 | |
| ];
 | |
| 
 | |
| const FANCY_NAMES = {
 | |
|     'api': 'API',
 | |
|     'api-url': 'Server URL',
 | |
|     'preset': 'Settings Preset',
 | |
|     'model': 'Model',
 | |
|     'proxy': 'Proxy Preset',
 | |
|     'sysprompt-state': 'Use System Prompt',
 | |
|     'sysprompt': 'System Prompt Name',
 | |
|     'instruct-state': 'Instruct Mode',
 | |
|     'instruct': 'Instruct Template',
 | |
|     'context': 'Context Template',
 | |
|     'tokenizer': 'Tokenizer',
 | |
|     'stop-strings': 'Custom Stopping Strings',
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * 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 = [];
 | |
|     }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Get named arguments for the command callback.
 | |
|  * @param {object} [args] Additional named arguments
 | |
|  * @returns {object} Named arguments
 | |
|  */
 | |
| function getNamedArguments(args = {}) {
 | |
|     // None of the commands here use underscored args, but better safe than sorry
 | |
|     return {
 | |
|         _scope: new SlashCommandScope(),
 | |
|         _abortController: new SlashCommandAbortController(),
 | |
|         _debugController: new SlashCommandDebugController(),
 | |
|         _parserFlags: {},
 | |
|         _hasUnnamedArgument: false,
 | |
|         quiet: 'true',
 | |
|         ...args,
 | |
|     };
 | |
| }
 | |
| 
 | |
| /** @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
 | |
|  * @property {string} [stop-strings] Custom Stopping Strings
 | |
|  * @property {string[]} [exclude] Commands to exclude
 | |
|  */
 | |
| 
 | |
| /**
 | |
|  * 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;
 | |
|     const excludeList = Array.isArray(profile.exclude) ? profile.exclude : [];
 | |
|     for (const command of commands) {
 | |
|         try {
 | |
|             if (excludeList.includes(command)) {
 | |
|                 continue;
 | |
|             }
 | |
| 
 | |
|             const args = getNamedArguments();
 | |
|             const result = await SlashCommandParser.commands[command].callback(args, '');
 | |
|             if (result) {
 | |
|                 profile[command] = result;
 | |
|                 continue;
 | |
|             }
 | |
|         } catch (error) {
 | |
|             console.error(`Failed to execute command: ${command}`, error);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     if (cleanUp) {
 | |
|         for (const command of commands) {
 | |
|             if (command.endsWith('-state') && profile[command] === 'false') {
 | |
|                 delete profile[command.replace('-state', '')];
 | |
|             }
 | |
|         }
 | |
|         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<ConnectionProfile>} Created connection profile
 | |
|  */
 | |
| async function createConnectionProfile(forceName = null) {
 | |
|     const mode = main_api === 'openai' ? 'cc' : 'tc';
 | |
|     const id = uuidv4();
 | |
|     /** @type {ConnectionProfile} */
 | |
|     const profile = {
 | |
|         id,
 | |
|         mode,
 | |
|         exclude: [],
 | |
|     };
 | |
| 
 | |
|     await readProfileFromCommands(mode, profile);
 | |
| 
 | |
|     const profileForDisplay = makeFancyProfile(profile);
 | |
|     const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'profile', { profile: profileForDisplay }));
 | |
|     template.find('input[name="exclude"]').on('input', function () {
 | |
|         const fancyName = String($(this).val());
 | |
|         const keyName = Object.entries(FANCY_NAMES).find(x => x[1] === fancyName)?.[0];
 | |
|         if (!keyName) {
 | |
|             console.warn('Key not found for fancy name:', fancyName);
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         if (!Array.isArray(profile.exclude)) {
 | |
|             profile.exclude = [];
 | |
|         }
 | |
| 
 | |
|         const excludeState = !$(this).prop('checked');
 | |
|         if (excludeState) {
 | |
|             profile.exclude.push(keyName);
 | |
|         } else {
 | |
|             const index = profile.exclude.indexOf(keyName);
 | |
|             index !== -1 && profile.exclude.splice(index, 1);
 | |
|         }
 | |
|     });
 | |
|     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;
 | |
|     }
 | |
| 
 | |
|     if (Array.isArray(profile.exclude)) {
 | |
|         for (const command of profile.exclude) {
 | |
|             delete profile[command];
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     profile.name = String(name);
 | |
|     return profile;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Deletes the selected connection profile.
 | |
|  * @returns {Promise<void>}
 | |
|  */
 | |
| 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(t`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<void>}
 | |
|  */
 | |
| 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;
 | |
|         }
 | |
|         try {
 | |
|             const args = getNamedArguments();
 | |
|             await SlashCommandParser.commands[command].callback(args, argument);
 | |
|         } catch (error) {
 | |
|             console.error(`Failed to execute command: ${command} ${argument}`, error);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     spinner.stop();
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Updates the selected connection profile.
 | |
|  * @param {ConnectionProfile} profile Connection profile
 | |
|  * @returns {Promise<void>}
 | |
|  */
 | |
| 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.sort((a, b) => a.name.localeCompare(b.name))) {
 | |
|         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 templateParams = { profile: profileForDisplay };
 | |
|         if (Array.isArray(profile.exclude) && profile.exclude.length > 0) {
 | |
|             templateParams.omitted = profile.exclude.map(e => FANCY_NAMES[e]).join(', ');
 | |
|         }
 | |
|         const template = await renderExtensionTemplateAsync(MODULE_NAME, 'view', templateParams);
 | |
|         detailsContent.innerHTML = template;
 | |
|     } else {
 | |
|         detailsContent.textContent = t`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);
 | |
|     });
 | |
| 
 | |
|     const editButton = document.getElementById('edit_connection_profile');
 | |
|     editButton.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;
 | |
|         }
 | |
|         if (!Array.isArray(profile.exclude)) {
 | |
|             profile.exclude = [];
 | |
|         }
 | |
| 
 | |
|         let saveChanges = false;
 | |
|         const sortByViewOrder = (a, b) => Object.keys(FANCY_NAMES).indexOf(a) - Object.keys(FANCY_NAMES).indexOf(b);
 | |
|         const commands = profile.mode === 'cc' ? CC_COMMANDS : TC_COMMANDS;
 | |
|         const settings = commands.slice().sort(sortByViewOrder).reduce((acc, command) => {
 | |
|             const fancyName = FANCY_NAMES[command];
 | |
|             acc[fancyName] = !profile.exclude.includes(command);
 | |
|             return acc;
 | |
|         }, {});
 | |
|         const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'edit', { name: profile.name, settings }));
 | |
|         const newName = await callGenericPopup(template, POPUP_TYPE.INPUT, profile.name, {
 | |
|             customButtons: [{
 | |
|                 text: t`Save and Update`,
 | |
|                 classes: ['popup-button-ok'],
 | |
|                 result: POPUP_RESULT.AFFIRMATIVE,
 | |
|                 action: () => {
 | |
|                     saveChanges = true;
 | |
|                 },
 | |
|             }],
 | |
|         });
 | |
| 
 | |
|         if (!newName) {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         if (profile.name !== newName && extension_settings.connectionManager.profiles.some(p => p.name === newName)) {
 | |
|             toastr.error('A profile with the same name already exists.');
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         const newExcludeList = template.find('input[name="exclude"]:not(:checked)').map(function () {
 | |
|             return Object.entries(FANCY_NAMES).find(x => x[1] === String($(this).val()))?.[0];
 | |
|         }).get();
 | |
| 
 | |
|         if (newExcludeList.length !== profile.exclude.length || !newExcludeList.every(e => profile.exclude.includes(e))) {
 | |
|             profile.exclude = newExcludeList;
 | |
|             for (const command of newExcludeList) {
 | |
|                 delete profile[command];
 | |
|             }
 | |
|             if (saveChanges) {
 | |
|                 await updateConnectionProfile(profile);
 | |
|             } else {
 | |
|                 toastr.info('Press "Update" to record them into the profile.', 'Included settings list updated');
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         if (profile.name !== newName) {
 | |
|             toastr.success('Connection profile renamed.');
 | |
|             profile.name = String(newName);
 | |
|         }
 | |
| 
 | |
|         saveSettingsDebounced();
 | |
|         renderConnectionProfiles(profiles);
 | |
|         await renderDetailsContent(detailsContent);
 | |
|     });
 | |
| 
 | |
|     /** @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 <code><None></code> 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);
 | |
|         },
 | |
|     }));
 | |
| })();
 |