mirror of
				https://github.com/SillyTavern/SillyTavern.git
				synced 2025-06-05 21:59:27 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			1019 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1019 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import { Fuse } from '../lib.js';
 | |
| 
 | |
| import {
 | |
|     amount_gen,
 | |
|     characters,
 | |
|     eventSource,
 | |
|     event_types,
 | |
|     getRequestHeaders,
 | |
|     koboldai_setting_names,
 | |
|     koboldai_settings,
 | |
|     main_api,
 | |
|     max_context,
 | |
|     nai_settings,
 | |
|     novelai_setting_names,
 | |
|     novelai_settings,
 | |
|     online_status,
 | |
|     saveSettingsDebounced,
 | |
|     this_chid,
 | |
| } from '../script.js';
 | |
| import { groups, selected_group } from './group-chats.js';
 | |
| import { instruct_presets } from './instruct-mode.js';
 | |
| import { kai_settings } from './kai-settings.js';
 | |
| import { convertNovelPreset } from './nai-settings.js';
 | |
| import { openai_settings, openai_setting_names, oai_settings } from './openai.js';
 | |
| import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
 | |
| import { context_presets, getContextSettings, power_user } from './power-user.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 { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
 | |
| import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
 | |
| import { checkForSystemPromptInInstructTemplate, system_prompts } from './sysprompt.js';
 | |
| import { renderTemplateAsync } from './templates.js';
 | |
| import {
 | |
|     textgenerationwebui_preset_names,
 | |
|     textgenerationwebui_presets,
 | |
|     textgenerationwebui_settings as textgen_settings,
 | |
| } from './textgen-settings.js';
 | |
| import { download, parseJsonFile, waitUntilCondition } from './utils.js';
 | |
| import { t } from './i18n.js';
 | |
| 
 | |
| const presetManagers = {};
 | |
| 
 | |
| /**
 | |
|  * Automatically select a preset for current API based on character or group name.
 | |
|  */
 | |
| function autoSelectPreset() {
 | |
|     const presetManager = getPresetManager();
 | |
| 
 | |
|     if (!presetManager) {
 | |
|         console.debug(`Preset Manager not found for API: ${main_api}`);
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     const name = selected_group ? groups.find(x => x.id == selected_group)?.name : characters[this_chid]?.name;
 | |
| 
 | |
|     if (!name) {
 | |
|         console.debug(`Preset candidate not found for API: ${main_api}`);
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     const preset = presetManager.findPreset(name);
 | |
|     const selectedPreset = presetManager.getSelectedPreset();
 | |
| 
 | |
|     if (preset === selectedPreset) {
 | |
|         console.debug(`Preset already selected for API: ${main_api}, name: ${name}`);
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     if (preset !== undefined && preset !== null) {
 | |
|         console.log(`Preset found for API: ${main_api}, name: ${name}`);
 | |
|         presetManager.selectPreset(preset);
 | |
|     }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Gets a preset manager by API id.
 | |
|  * @param {string} apiId API id
 | |
|  * @returns {PresetManager} Preset manager
 | |
|  */
 | |
| export function getPresetManager(apiId = '') {
 | |
|     if (!apiId) {
 | |
|         apiId = main_api == 'koboldhorde' ? 'kobold' : main_api;
 | |
|     }
 | |
| 
 | |
|     if (!Object.keys(presetManagers).includes(apiId)) {
 | |
|         return null;
 | |
|     }
 | |
| 
 | |
|     return presetManagers[apiId];
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Registers preset managers for all select elements with data-preset-manager-for attribute.
 | |
|  */
 | |
| function registerPresetManagers() {
 | |
|     $('select[data-preset-manager-for]').each((_, e) => {
 | |
|         const forData = $(e).data('preset-manager-for');
 | |
|         for (const apiId of forData.split(',')) {
 | |
|             console.debug(`Registering preset manager for API: ${apiId}`);
 | |
|             presetManagers[apiId] = new PresetManager($(e), apiId);
 | |
|         }
 | |
|     });
 | |
| }
 | |
| 
 | |
| class PresetManager {
 | |
|     constructor(select, apiId) {
 | |
|         this.select = select;
 | |
|         this.apiId = apiId;
 | |
|     }
 | |
| 
 | |
|     static masterSections = {
 | |
|         'instruct': {
 | |
|             name: 'Instruct Template',
 | |
|             getData: () => {
 | |
|                 const manager = getPresetManager('instruct');
 | |
|                 const name = manager.getSelectedPresetName();
 | |
|                 return manager.getPresetSettings(name);
 | |
|             },
 | |
|             setData: (data) => {
 | |
|                 const manager = getPresetManager('instruct');
 | |
|                 const name = data.name;
 | |
|                 return manager.savePreset(name, data);
 | |
|             },
 | |
|             isValid: (data) => PresetManager.isPossiblyInstructData(data),
 | |
|         },
 | |
|         'context': {
 | |
|             name: 'Context Template',
 | |
|             getData: () => {
 | |
|                 const manager = getPresetManager('context');
 | |
|                 const name = manager.getSelectedPresetName();
 | |
|                 return manager.getPresetSettings(name);
 | |
|             },
 | |
|             setData: (data) => {
 | |
|                 const manager = getPresetManager('context');
 | |
|                 const name = data.name;
 | |
|                 return manager.savePreset(name, data);
 | |
|             },
 | |
|             isValid: (data) => PresetManager.isPossiblyContextData(data),
 | |
|         },
 | |
|         'sysprompt': {
 | |
|             name: 'System Prompt',
 | |
|             getData: () => {
 | |
|                 const manager = getPresetManager('sysprompt');
 | |
|                 const name = manager.getSelectedPresetName();
 | |
|                 return manager.getPresetSettings(name);
 | |
|             },
 | |
|             setData: (data) => {
 | |
|                 const manager = getPresetManager('sysprompt');
 | |
|                 const name = data.name;
 | |
|                 return manager.savePreset(name, data);
 | |
|             },
 | |
|             isValid: (data) => PresetManager.isPossiblySystemPromptData(data),
 | |
|         },
 | |
|         'preset': {
 | |
|             name: 'Text Completion Preset',
 | |
|             getData: () => {
 | |
|                 const manager = getPresetManager('textgenerationwebui');
 | |
|                 const name = manager.getSelectedPresetName();
 | |
|                 const data = manager.getPresetSettings(name);
 | |
|                 data['name'] = name;
 | |
|                 return data;
 | |
|             },
 | |
|             setData: (data) => {
 | |
|                 const manager = getPresetManager('textgenerationwebui');
 | |
|                 const name = data.name;
 | |
|                 return manager.savePreset(name, data);
 | |
|             },
 | |
|             isValid: (data) => PresetManager.isPossiblyTextCompletionData(data),
 | |
|         },
 | |
|     };
 | |
| 
 | |
|     static isPossiblyInstructData(data) {
 | |
|         const instructProps = ['name', 'input_sequence', 'output_sequence'];
 | |
|         return data && instructProps.every(prop => Object.keys(data).includes(prop));
 | |
|     }
 | |
| 
 | |
|     static isPossiblyContextData(data) {
 | |
|         const contextProps = ['name', 'story_string'];
 | |
|         return data && contextProps.every(prop => Object.keys(data).includes(prop));
 | |
|     }
 | |
| 
 | |
|     static isPossiblySystemPromptData(data) {
 | |
|         const sysPromptProps = ['name', 'content'];
 | |
|         return data && sysPromptProps.every(prop => Object.keys(data).includes(prop));
 | |
|     }
 | |
| 
 | |
|     static isPossiblyTextCompletionData(data) {
 | |
|         const textCompletionProps = ['temp', 'top_k', 'top_p', 'rep_pen'];
 | |
|         return data && textCompletionProps.every(prop => Object.keys(data).includes(prop));
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Imports master settings from JSON data.
 | |
|      * @param {object} data Data to import
 | |
|      * @param {string} fileName File name
 | |
|      * @returns {Promise<void>}
 | |
|      */
 | |
|     static async performMasterImport(data, fileName) {
 | |
|         if (!data || typeof data !== 'object') {
 | |
|             toastr.error(t`Invalid data provided for master import`);
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         // Check for legacy file imports
 | |
|         // 1. Instruct Template
 | |
|         if (this.isPossiblyInstructData(data)) {
 | |
|             toastr.info(t`Importing instruct template...`, t`Instruct template detected`);
 | |
|             return await getPresetManager('instruct').savePreset(data.name, data);
 | |
|         }
 | |
| 
 | |
|         // 2. Context Template
 | |
|         if (this.isPossiblyContextData(data)) {
 | |
|             toastr.info(t`Importing as context template...`, t`Context template detected`);
 | |
|             return await getPresetManager('context').savePreset(data.name, data);
 | |
|         }
 | |
| 
 | |
|         // 3. System Prompt
 | |
|         if (this.isPossiblySystemPromptData(data)) {
 | |
|             toastr.info(t`Importing as system prompt...`, t`System prompt detected`);
 | |
|             return await getPresetManager('sysprompt').savePreset(data.name, data);
 | |
|         }
 | |
| 
 | |
|         // 4. Text Completion settings
 | |
|         if (this.isPossiblyTextCompletionData(data)) {
 | |
|             toastr.info(t`Importing as settings preset...`, t`Text Completion settings detected`);
 | |
|             return await getPresetManager('textgenerationwebui').savePreset(fileName, data);
 | |
|         }
 | |
| 
 | |
|         const validSections = [];
 | |
|         for (const [key, section] of Object.entries(this.masterSections)) {
 | |
|             if (key in data && section.isValid(data[key])) {
 | |
|                 validSections.push(key);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         if (validSections.length === 0) {
 | |
|             toastr.error(t`No valid sections found in imported data`);
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         const sectionNames = validSections.reduce((acc, key) => {
 | |
|             acc[key] = { key: key, name: this.masterSections[key].name, preset: data[key]?.name || '' };
 | |
|             return acc;
 | |
|         }, {});
 | |
| 
 | |
|         const html = $(await renderTemplateAsync('masterImport', { sections: sectionNames }));
 | |
|         const popup = new Popup(html, POPUP_TYPE.CONFIRM, '', {
 | |
|             okButton: t`Import`,
 | |
|             cancelButton: t`Cancel`,
 | |
|         });
 | |
| 
 | |
|         const result = await popup.show();
 | |
| 
 | |
|         // Import cancelled
 | |
|         if (result !== POPUP_RESULT.AFFIRMATIVE) {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         const importedSections = [];
 | |
|         const confirmedSections = html.find('input:checked').map((_, el) => el instanceof HTMLInputElement && el.value).get();
 | |
| 
 | |
|         if (confirmedSections.length === 0) {
 | |
|             toastr.info(t`No sections selected for import`);
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         for (const section of confirmedSections) {
 | |
|             const sectionData = data[section];
 | |
|             const masterSection = this.masterSections[section];
 | |
|             if (sectionData && masterSection) {
 | |
|                 await masterSection.setData(sectionData);
 | |
|                 importedSections.push(masterSection.name);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         toastr.success(t`Imported ${importedSections.length} settings: ${importedSections.join(', ')}`);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Exports master settings to JSON data.
 | |
|      * @returns {Promise<string>} JSON data
 | |
|      */
 | |
|     static async performMasterExport() {
 | |
|         const sectionNames = Object.entries(this.masterSections).reduce((acc, [key, section]) => {
 | |
|             acc[key] = { key: key, name: section.name, checked: key !== 'preset' };
 | |
|             return acc;
 | |
|         }, {});
 | |
|         const html = $(await renderTemplateAsync('masterExport', { sections: sectionNames }));
 | |
| 
 | |
|         const popup = new Popup(html, POPUP_TYPE.CONFIRM, '', {
 | |
|             okButton: t`Export`,
 | |
|             cancelButton: t`Cancel`,
 | |
|         });
 | |
| 
 | |
|         const result = await popup.show();
 | |
| 
 | |
|         // Export cancelled
 | |
|         if (result !== POPUP_RESULT.AFFIRMATIVE) {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         const confirmedSections = html.find('input:checked').map((_, el) => el instanceof HTMLInputElement && el.value).get();
 | |
|         const data = {};
 | |
| 
 | |
|         if (confirmedSections.length === 0) {
 | |
|             toastr.info(t`No sections selected for export`);
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         for (const section of confirmedSections) {
 | |
|             const masterSection = this.masterSections[section];
 | |
|             if (masterSection) {
 | |
|                 data[section] = masterSection.getData();
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return JSON.stringify(data, null, 4);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Gets all preset names.
 | |
|      * @returns {string[]} List of preset names
 | |
|      */
 | |
|     getAllPresets() {
 | |
|         return $(this.select).find('option').map((_, el) => el.text).toArray();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Finds a preset by name.
 | |
|      * @param {string} name Preset name
 | |
|      * @returns {any} Preset value
 | |
|      */
 | |
|     findPreset(name) {
 | |
|         return $(this.select).find('option').filter(function () {
 | |
|             return $(this).text() === name;
 | |
|         }).val();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Gets the selected preset value.
 | |
|      * @returns {any} Selected preset value
 | |
|      */
 | |
|     getSelectedPreset() {
 | |
|         return $(this.select).find('option:selected').val();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Gets the selected preset name.
 | |
|      * @returns {string} Selected preset name
 | |
|      */
 | |
|     getSelectedPresetName() {
 | |
|         return $(this.select).find('option:selected').text();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Selects a preset by option value.
 | |
|      * @param {string} value Preset option value
 | |
|      */
 | |
|     selectPreset(value) {
 | |
|         const option = $(this.select).filter(function () {
 | |
|             return $(this).val() === value;
 | |
|         });
 | |
|         option.prop('selected', true);
 | |
|         $(this.select).val(value).trigger('change');
 | |
|     }
 | |
| 
 | |
|     async updatePreset() {
 | |
|         const selected = $(this.select).find('option:selected');
 | |
|         console.log(selected);
 | |
| 
 | |
|         if (selected.val() == 'gui') {
 | |
|             toastr.info(t`Cannot update GUI preset`);
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         const name = selected.text();
 | |
|         await this.savePreset(name);
 | |
| 
 | |
|         const successToast = !this.isAdvancedFormatting() ? t`Preset updated` : t`Template updated`;
 | |
|         toastr.success(successToast);
 | |
|     }
 | |
| 
 | |
|     async savePresetAs() {
 | |
|         const inputValue = this.getSelectedPresetName();
 | |
|         const popupText = !this.isAdvancedFormatting() ? '<h4>' + t`Hint: Use a character/group name to bind preset to a specific chat.` + '</h4>' : '';
 | |
|         const headerText = !this.isAdvancedFormatting() ? t`Preset name:` : t`Template name:`;
 | |
|         const name = await Popup.show.input(headerText, popupText, inputValue);
 | |
|         if (!name) {
 | |
|             console.log('Preset name not provided');
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         await this.savePreset(name);
 | |
| 
 | |
|         const successToast = !this.isAdvancedFormatting() ? t`Preset saved` : t`Template saved`;
 | |
|         toastr.success(successToast);
 | |
|     }
 | |
| 
 | |
|     async savePreset(name, settings) {
 | |
|         if (this.apiId === 'instruct' && settings) {
 | |
|             await checkForSystemPromptInInstructTemplate(name, settings);
 | |
|         }
 | |
| 
 | |
|         if (this.apiId === 'novel' && settings) {
 | |
|             settings = convertNovelPreset(settings);
 | |
|         }
 | |
| 
 | |
|         const preset = settings ?? this.getPresetSettings(name);
 | |
| 
 | |
|         const response = await fetch('/api/presets/save', {
 | |
|             method: 'POST',
 | |
|             headers: getRequestHeaders(),
 | |
|             body: JSON.stringify({ preset, name, apiId: this.apiId }),
 | |
|         });
 | |
| 
 | |
|         if (!response.ok) {
 | |
|             toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Preset could not be saved`);
 | |
|             console.error('Preset could not be saved', response);
 | |
|             throw new Error('Preset could not be saved');
 | |
|         }
 | |
| 
 | |
|         const data = await response.json();
 | |
|         name = data.name;
 | |
| 
 | |
|         this.updateList(name, preset);
 | |
|     }
 | |
| 
 | |
|     async renamePreset(newName) {
 | |
|         const oldName = this.getSelectedPresetName();
 | |
|         try {
 | |
|             await this.savePreset(newName);
 | |
|             await this.deletePreset(oldName);
 | |
|         } catch (error) {
 | |
|             toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Preset could not be renamed`);
 | |
|             console.error('Preset could not be renamed', error);
 | |
|             throw new Error('Preset could not be renamed');
 | |
|         }
 | |
| 
 | |
|     }
 | |
| 
 | |
|     getPresetList() {
 | |
|         let presets = [];
 | |
|         let preset_names = {};
 | |
| 
 | |
|         switch (this.apiId) {
 | |
|             case 'koboldhorde':
 | |
|             case 'kobold':
 | |
|                 presets = koboldai_settings;
 | |
|                 preset_names = koboldai_setting_names;
 | |
|                 break;
 | |
|             case 'novel':
 | |
|                 presets = novelai_settings;
 | |
|                 preset_names = novelai_setting_names;
 | |
|                 break;
 | |
|             case 'textgenerationwebui':
 | |
|                 presets = textgenerationwebui_presets;
 | |
|                 preset_names = textgenerationwebui_preset_names;
 | |
|                 break;
 | |
|             case 'openai':
 | |
|                 presets = openai_settings;
 | |
|                 preset_names = openai_setting_names;
 | |
|                 break;
 | |
|             case 'context':
 | |
|                 presets = context_presets;
 | |
|                 preset_names = context_presets.map(x => x.name);
 | |
|                 break;
 | |
|             case 'instruct':
 | |
|                 presets = instruct_presets;
 | |
|                 preset_names = instruct_presets.map(x => x.name);
 | |
|                 break;
 | |
|             case 'sysprompt':
 | |
|                 presets = system_prompts;
 | |
|                 preset_names = system_prompts.map(x => x.name);
 | |
|                 break;
 | |
|             default:
 | |
|                 console.warn(`Unknown API ID ${this.apiId}`);
 | |
|         }
 | |
| 
 | |
|         return { presets, preset_names };
 | |
|     }
 | |
| 
 | |
|     isKeyedApi() {
 | |
|         return this.apiId == 'textgenerationwebui' || this.isAdvancedFormatting();
 | |
|     }
 | |
| 
 | |
|     isAdvancedFormatting() {
 | |
|         return this.apiId == 'context' || this.apiId == 'instruct' || this.apiId == 'sysprompt';
 | |
|     }
 | |
| 
 | |
|     updateList(name, preset) {
 | |
|         const { presets, preset_names } = this.getPresetList();
 | |
|         const presetExists = this.isKeyedApi() ? preset_names.includes(name) : Object.keys(preset_names).includes(name);
 | |
| 
 | |
|         if (presetExists) {
 | |
|             if (this.isKeyedApi()) {
 | |
|                 presets[preset_names.indexOf(name)] = preset;
 | |
|                 $(this.select).find(`option[value="${name}"]`).prop('selected', true);
 | |
|                 $(this.select).val(name).trigger('change');
 | |
|             }
 | |
|             else {
 | |
|                 const value = preset_names[name];
 | |
|                 presets[value] = preset;
 | |
|                 $(this.select).find(`option[value="${value}"]`).prop('selected', true);
 | |
|                 $(this.select).val(value).trigger('change');
 | |
|             }
 | |
|         }
 | |
|         else {
 | |
|             presets.push(preset);
 | |
|             const value = presets.length - 1;
 | |
| 
 | |
|             if (this.isKeyedApi()) {
 | |
|                 preset_names[value] = name;
 | |
|                 const option = $('<option></option>', { value: name, text: name, selected: true });
 | |
|                 $(this.select).append(option);
 | |
|                 $(this.select).val(name).trigger('change');
 | |
|             } else {
 | |
|                 preset_names[name] = value;
 | |
|                 const option = $('<option></option>', { value: value, text: name, selected: true });
 | |
|                 $(this.select).append(option);
 | |
|                 $(this.select).val(value).trigger('change');
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     getPresetSettings(name) {
 | |
|         function getSettingsByApiId(apiId) {
 | |
|             switch (apiId) {
 | |
|                 case 'koboldhorde':
 | |
|                 case 'kobold':
 | |
|                     return kai_settings;
 | |
|                 case 'novel':
 | |
|                     return nai_settings;
 | |
|                 case 'textgenerationwebui':
 | |
|                     return textgen_settings;
 | |
|                 case 'context': {
 | |
|                     const context_preset = getContextSettings();
 | |
|                     context_preset['name'] = name || power_user.context.preset;
 | |
|                     return context_preset;
 | |
|                 }
 | |
|                 case 'instruct': {
 | |
|                     const instruct_preset = structuredClone(power_user.instruct);
 | |
|                     instruct_preset['name'] = name || power_user.instruct.preset;
 | |
|                     return instruct_preset;
 | |
|                 }
 | |
|                 case 'sysprompt': {
 | |
|                     const sysprompt_preset = structuredClone(power_user.sysprompt);
 | |
|                     sysprompt_preset['name'] = name || power_user.sysprompt.preset;
 | |
|                     return sysprompt_preset;
 | |
|                 }
 | |
|                 default:
 | |
|                     console.warn(`Unknown API ID ${apiId}`);
 | |
|                     return {};
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         const filteredKeys = [
 | |
|             'preset',
 | |
|             'streaming',
 | |
|             'truncation_length',
 | |
|             'n',
 | |
|             'streaming_url',
 | |
|             'stopping_strings',
 | |
|             'can_use_tokenization',
 | |
|             'can_use_streaming',
 | |
|             'preset_settings_novel',
 | |
|             'streaming_novel',
 | |
|             'nai_preamble',
 | |
|             'model_novel',
 | |
|             'streaming_kobold',
 | |
|             'enabled',
 | |
|             'bind_to_context',
 | |
|             'seed',
 | |
|             'legacy_api',
 | |
|             'mancer_model',
 | |
|             'togetherai_model',
 | |
|             'ollama_model',
 | |
|             'vllm_model',
 | |
|             'aphrodite_model',
 | |
|             'server_urls',
 | |
|             'type',
 | |
|             'custom_model',
 | |
|             'bypass_status_check',
 | |
|             'infermaticai_model',
 | |
|             'dreamgen_model',
 | |
|             'openrouter_model',
 | |
|             'featherless_model',
 | |
|             'max_tokens_second',
 | |
|             'openrouter_providers',
 | |
|             'openrouter_allow_fallbacks',
 | |
|             'tabby_model',
 | |
|             'derived',
 | |
|             'generic_model',
 | |
|             'include_reasoning',
 | |
|             'global_banned_tokens',
 | |
|             'send_banned_tokens',
 | |
|         ];
 | |
|         const settings = Object.assign({}, getSettingsByApiId(this.apiId));
 | |
| 
 | |
|         for (const key of filteredKeys) {
 | |
|             if (Object.hasOwn(settings, key)) {
 | |
|                 delete settings[key];
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         if (!this.isAdvancedFormatting()) {
 | |
|             settings['genamt'] = amount_gen;
 | |
|             settings['max_length'] = max_context;
 | |
|         }
 | |
| 
 | |
|         return settings;
 | |
|     }
 | |
| 
 | |
|     getCompletionPresetByName(name) {
 | |
|         // Retrieve a completion preset by name. Return undefined if not found.
 | |
|         let { presets, preset_names } = this.getPresetList();
 | |
|         let preset;
 | |
| 
 | |
|         // Some APIs use an array of names, others use an object of {name: index}
 | |
|         if (Array.isArray(preset_names)) {  // array of names
 | |
|             if (preset_names.includes(name)) {
 | |
|                 preset = presets[preset_names.indexOf(name)];
 | |
|             }
 | |
|         } else {  // object of {names: index}
 | |
|             if (preset_names[name] !== undefined) {
 | |
|                 preset = presets[preset_names[name]];
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         if (preset === undefined) {
 | |
|             console.error(`Preset ${name} not found`);
 | |
|         }
 | |
| 
 | |
|         // if the preset isn't found, returns undefined
 | |
|         return preset;
 | |
|     }
 | |
| 
 | |
|     // pass no arguments to delete current preset
 | |
|     async deletePreset(name) {
 | |
|         const { preset_names, presets } = this.getPresetList();
 | |
|         const value = name ? (this.isKeyedApi() ? this.findPreset(name) : name) : this.getSelectedPreset();
 | |
|         const nameToDelete = name || this.getSelectedPresetName();
 | |
| 
 | |
|         if (value == 'gui') {
 | |
|             toastr.info(t`Cannot delete GUI preset`);
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         $(this.select).find(`option[value="${value}"]`).remove();
 | |
| 
 | |
|         if (this.isKeyedApi()) {
 | |
|             const index = preset_names.indexOf(nameToDelete);
 | |
|             preset_names.splice(index, 1);
 | |
|             presets.splice(index, 1);
 | |
|         } else {
 | |
|             delete preset_names[nameToDelete];
 | |
|         }
 | |
| 
 | |
|         // switch in UI only when deleting currently selected preset
 | |
|         const switchPresets = !name || this.getSelectedPresetName() == name;
 | |
| 
 | |
|         if (Object.keys(preset_names).length && switchPresets) {
 | |
|             const nextPresetName = Object.keys(preset_names)[0];
 | |
|             const newValue = preset_names[nextPresetName];
 | |
|             $(this.select).find(`option[value="${newValue}"]`).attr('selected', 'true');
 | |
|             $(this.select).trigger('change');
 | |
|         }
 | |
| 
 | |
|         const response = await fetch('/api/presets/delete', {
 | |
|             method: 'POST',
 | |
|             headers: getRequestHeaders(),
 | |
|             body: JSON.stringify({ name: nameToDelete, apiId: this.apiId }),
 | |
|         });
 | |
| 
 | |
|         return response.ok;
 | |
|     }
 | |
| 
 | |
|     async getDefaultPreset(name) {
 | |
|         const response = await fetch('/api/presets/restore', {
 | |
|             method: 'POST',
 | |
|             headers: getRequestHeaders(),
 | |
|             body: JSON.stringify({ name, apiId: this.apiId }),
 | |
|         });
 | |
| 
 | |
|         if (!response.ok) {
 | |
|             const errorToast = !this.isAdvancedFormatting() ? t`Failed to restore default preset` : t`Failed to restore default template`;
 | |
|             toastr.error(errorToast);
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         return await response.json();
 | |
|     }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Selects a preset by name for current API.
 | |
|  * @param {any} _ Named arguments
 | |
|  * @param {string} name Unnamed arguments
 | |
|  * @returns {Promise<string>} Selected or current preset name
 | |
|  */
 | |
| async function presetCommandCallback(_, name) {
 | |
|     const shouldReconnect = online_status !== 'no_connection';
 | |
|     const presetManager = getPresetManager();
 | |
|     const allPresets = presetManager.getAllPresets();
 | |
|     const currentPreset = presetManager.getSelectedPresetName();
 | |
| 
 | |
|     if (!presetManager) {
 | |
|         console.debug(`Preset Manager not found for API: ${main_api}`);
 | |
|         return '';
 | |
|     }
 | |
| 
 | |
|     if (!name) {
 | |
|         console.log('No name provided for /preset command, using current preset');
 | |
|         return currentPreset;
 | |
|     }
 | |
| 
 | |
|     if (!Array.isArray(allPresets) || allPresets.length === 0) {
 | |
|         console.log(`No presets found for API: ${main_api}`);
 | |
|         return currentPreset;
 | |
|     }
 | |
| 
 | |
|     // Find exact match
 | |
|     const exactMatch = allPresets.find(p => p.toLowerCase().trim() === name.toLowerCase().trim());
 | |
| 
 | |
|     if (exactMatch) {
 | |
|         console.log('Found exact preset match', exactMatch);
 | |
| 
 | |
|         if (currentPreset !== exactMatch) {
 | |
|             const presetValue = presetManager.findPreset(exactMatch);
 | |
| 
 | |
|             if (presetValue) {
 | |
|                 presetManager.selectPreset(presetValue);
 | |
|                 shouldReconnect && await waitForConnection();
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return exactMatch;
 | |
|     } else {
 | |
|         // Find fuzzy match
 | |
|         const fuse = new Fuse(allPresets);
 | |
|         const fuzzyMatch = fuse.search(name);
 | |
| 
 | |
|         if (!fuzzyMatch.length) {
 | |
|             console.warn(`WARN: Preset found with name ${name}`);
 | |
|             return currentPreset;
 | |
|         }
 | |
| 
 | |
|         const fuzzyPresetName = fuzzyMatch[0].item;
 | |
|         const fuzzyPresetValue = presetManager.findPreset(fuzzyPresetName);
 | |
| 
 | |
|         if (fuzzyPresetValue) {
 | |
|             console.log('Found fuzzy preset match', fuzzyPresetName);
 | |
| 
 | |
|             if (currentPreset !== fuzzyPresetName) {
 | |
|                 presetManager.selectPreset(fuzzyPresetValue);
 | |
|                 shouldReconnect && await waitForConnection();
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return fuzzyPresetName;
 | |
|     }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Waits for API connection to be established.
 | |
|  */
 | |
| async function waitForConnection() {
 | |
|     try {
 | |
|         await waitUntilCondition(() => online_status !== 'no_connection', 10000, 100);
 | |
|     } catch {
 | |
|         console.log('Timeout waiting for API to connect');
 | |
|     }
 | |
| }
 | |
| 
 | |
| export async function initPresetManager() {
 | |
|     eventSource.on(event_types.CHAT_CHANGED, autoSelectPreset);
 | |
|     registerPresetManagers();
 | |
|     SlashCommandParser.addCommandObject(SlashCommand.fromProps({
 | |
|         name: 'preset',
 | |
|         callback: presetCommandCallback,
 | |
|         returns: 'current preset',
 | |
|         unnamedArgumentList: [
 | |
|             SlashCommandArgument.fromProps({
 | |
|                 description: 'name',
 | |
|                 typeList: [ARGUMENT_TYPE.STRING],
 | |
|                 enumProvider: () => getPresetManager().getAllPresets().map(preset => new SlashCommandEnumValue(preset, null, enumTypes.enum, enumIcons.preset)),
 | |
|             }),
 | |
|         ],
 | |
|         helpString: `
 | |
|             <div>
 | |
|                 Sets a preset by name for the current API. Gets the current preset if no name is provided.
 | |
|             </div>
 | |
|             <div>
 | |
|                 <strong>Example:</strong>
 | |
|                 <ul>
 | |
|                     <li>
 | |
|                         <pre><code>/preset myPreset</code></pre>
 | |
|                     </li>
 | |
|                     <li>
 | |
|                         <pre><code>/preset</code></pre>
 | |
|                     </li>
 | |
|                 </ul>
 | |
|             </div>
 | |
|         `,
 | |
|     }));
 | |
| 
 | |
| 
 | |
|     $(document).on('click', '[data-preset-manager-update]', async function () {
 | |
|         const apiId = $(this).data('preset-manager-update');
 | |
|         const presetManager = getPresetManager(apiId);
 | |
| 
 | |
|         if (!presetManager) {
 | |
|             console.warn(`Preset Manager not found for API: ${apiId}`);
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         await presetManager.updatePreset();
 | |
|     });
 | |
| 
 | |
|     $(document).on('click', '[data-preset-manager-new]', async function () {
 | |
|         const apiId = $(this).data('preset-manager-new');
 | |
|         const presetManager = getPresetManager(apiId);
 | |
| 
 | |
|         if (!presetManager) {
 | |
|             console.warn(`Preset Manager not found for API: ${apiId}`);
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         await presetManager.savePresetAs();
 | |
|     });
 | |
| 
 | |
|     $(document).on('click', '[data-preset-manager-rename]', async function () {
 | |
|         const apiId = $(this).data('preset-manager-rename');
 | |
|         const presetManager = getPresetManager(apiId);
 | |
| 
 | |
|         if (!presetManager) {
 | |
|             console.warn(`Preset Manager not found for API: ${apiId}`);
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         const popupHeader = !presetManager.isAdvancedFormatting() ? t`Rename preset` : t`Rename template`;
 | |
|         const oldName = presetManager.getSelectedPresetName();
 | |
|         const newName = await Popup.show.input(popupHeader, t`Enter a new name:`, oldName);
 | |
|         if (!newName || oldName === newName) {
 | |
|             console.debug(!presetManager.isAdvancedFormatting() ? 'Preset rename cancelled' : 'Template rename cancelled');
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         await presetManager.renamePreset(newName);
 | |
| 
 | |
|         const successToast = !presetManager.isAdvancedFormatting() ? t`Preset renamed` : t`Template renamed`;
 | |
|         toastr.success(successToast);
 | |
|     });
 | |
| 
 | |
|     $(document).on('click', '[data-preset-manager-export]', async function () {
 | |
|         const apiId = $(this).data('preset-manager-export');
 | |
|         const presetManager = getPresetManager(apiId);
 | |
| 
 | |
|         if (!presetManager) {
 | |
|             console.warn(`Preset Manager not found for API: ${apiId}`);
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         const selected = $(presetManager.select).find('option:selected');
 | |
|         const name = selected.text();
 | |
|         const preset = presetManager.getPresetSettings(name);
 | |
|         const data = JSON.stringify(preset, null, 4);
 | |
|         download(data, `${name}.json`, 'application/json');
 | |
|     });
 | |
| 
 | |
|     $(document).on('click', '[data-preset-manager-import]', async function () {
 | |
|         const apiId = $(this).data('preset-manager-import');
 | |
|         $(`[data-preset-manager-file="${apiId}"]`).trigger('click');
 | |
|     });
 | |
| 
 | |
|     $(document).on('change', '[data-preset-manager-file]', async function (e) {
 | |
|         const apiId = $(this).data('preset-manager-file');
 | |
|         const presetManager = getPresetManager(apiId);
 | |
| 
 | |
|         if (!presetManager) {
 | |
|             console.warn(`Preset Manager not found for API: ${apiId}`);
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         const file = e.target.files[0];
 | |
| 
 | |
|         if (!file) {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         const fileName = file.name.replace('.json', '').replace('.settings', '');
 | |
|         const data = await parseJsonFile(file);
 | |
|         const name = data?.name ?? fileName;
 | |
|         data['name'] = name;
 | |
| 
 | |
|         await presetManager.savePreset(name, data);
 | |
|         const successToast = !presetManager.isAdvancedFormatting() ? t`Preset imported` : t`Template imported`;
 | |
|         toastr.success(successToast);
 | |
|         e.target.value = null;
 | |
|     });
 | |
| 
 | |
|     $(document).on('click', '[data-preset-manager-delete]', async function () {
 | |
|         const apiId = $(this).data('preset-manager-delete');
 | |
|         const presetManager = getPresetManager(apiId);
 | |
| 
 | |
|         if (!presetManager) {
 | |
|             console.warn(`Preset Manager not found for API: ${apiId}`);
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         const headerText = !presetManager.isAdvancedFormatting() ? t`Delete this preset?` : t`Delete this template?`;
 | |
|         const confirm = await Popup.show.confirm(headerText, t`This action is irreversible and your current settings will be overwritten.`);
 | |
|         if (!confirm) {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         const result = await presetManager.deletePreset();
 | |
| 
 | |
|         if (result) {
 | |
|             const successToast = !presetManager.isAdvancedFormatting() ? t`Preset deleted` : t`Template deleted`;
 | |
|             toastr.success(successToast);
 | |
|         } else {
 | |
|             const warningToast = !presetManager.isAdvancedFormatting() ? t`Preset was not deleted from server` : t`Template was not deleted from server`;
 | |
|             toastr.warning(warningToast);
 | |
|         }
 | |
| 
 | |
|         saveSettingsDebounced();
 | |
|     });
 | |
| 
 | |
|     $(document).on('click', '[data-preset-manager-restore]', async function () {
 | |
|         const apiId = $(this).data('preset-manager-restore');
 | |
|         const presetManager = getPresetManager(apiId);
 | |
| 
 | |
|         if (!presetManager) {
 | |
|             console.warn(`Preset Manager not found for API: ${apiId}`);
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         const name = presetManager.getSelectedPresetName();
 | |
|         const data = await presetManager.getDefaultPreset(name);
 | |
| 
 | |
|         if (name == 'gui') {
 | |
|             toastr.info(t`Cannot restore GUI preset`);
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         if (!data) {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         if (data.isDefault) {
 | |
|             if (Object.keys(data.preset).length === 0) {
 | |
|                 const errorToast = !presetManager.isAdvancedFormatting() ? t`Default preset cannot be restored` : t`Default template cannot be restored`;
 | |
|                 toastr.error(errorToast);
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             const confirmText = !presetManager.isAdvancedFormatting()
 | |
|                 ? t`Resetting a <b>default preset</b> will restore the default settings.`
 | |
|                 : t`Resetting a <b>default template</b> will restore the default settings.`;
 | |
|             const confirm = await Popup.show.confirm(t`Are you sure?`, confirmText);
 | |
|             if (!confirm) {
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             await presetManager.deletePreset();
 | |
|             await presetManager.savePreset(name, data.preset);
 | |
|             const option = presetManager.findPreset(name);
 | |
|             presetManager.selectPreset(option);
 | |
|             const successToast = !presetManager.isAdvancedFormatting() ? t`Default preset restored` : t`Default template restored`;
 | |
|             toastr.success(successToast);
 | |
|         } else {
 | |
|             const confirmText = !presetManager.isAdvancedFormatting()
 | |
|                 ? t`Resetting a <b>custom preset</b> will restore to the last saved state.`
 | |
|                 : t`Resetting a <b>custom template</b> will restore to the last saved state.`;
 | |
|             const confirm = await Popup.show.confirm(t`Are you sure?`, confirmText);
 | |
|             if (!confirm) {
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             const option = presetManager.findPreset(name);
 | |
|             presetManager.selectPreset(option);
 | |
|             const successToast = !presetManager.isAdvancedFormatting() ? t`Preset restored` : t`Template restored`;
 | |
|             toastr.success(successToast);
 | |
|         }
 | |
|     });
 | |
| 
 | |
|     $('#af_master_import').on('click', () => {
 | |
|         $('#af_master_import_file').trigger('click');
 | |
|     });
 | |
| 
 | |
|     $('#af_master_import_file').on('change', async function (e) {
 | |
|         if (!(e.target instanceof HTMLInputElement)) {
 | |
|             return;
 | |
|         }
 | |
|         const file = e.target.files[0];
 | |
| 
 | |
|         if (!file) {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         const data = await parseJsonFile(file);
 | |
|         const fileName = file.name.replace('.json', '');
 | |
|         await PresetManager.performMasterImport(data, fileName);
 | |
|         e.target.value = null;
 | |
|     });
 | |
| 
 | |
|     $('#af_master_export').on('click', async () => {
 | |
|         const data = await PresetManager.performMasterExport();
 | |
| 
 | |
|         if (!data) {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         const shortDate = new Date().toISOString().split('T')[0];
 | |
|         download(data, `ST-formatting-${shortDate}.json`, 'application/json');
 | |
|     });
 | |
| }
 |