mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Adds validation to prevent renaming presets to names that are identical when ignoring case and accents Avoids accidental duplicates by ensuring meaningful name changes during preset renames
1073 lines
37 KiB
JavaScript
1073 lines
37 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 } 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, equalsIgnoreCaseAndAccents, parseJsonFile, waitUntilCondition } from './utils.js';
|
|
import { t } from './i18n.js';
|
|
import { reasoning_templates } from './reasoning.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),
|
|
},
|
|
'reasoning': {
|
|
name: 'Reasoning Formatting',
|
|
getData: () => {
|
|
const manager = getPresetManager('reasoning');
|
|
const name = manager.getSelectedPresetName();
|
|
return manager.getPresetSettings(name);
|
|
},
|
|
setData: (data) => {
|
|
const manager = getPresetManager('reasoning');
|
|
const name = data.name;
|
|
return manager.savePreset(name, data);
|
|
},
|
|
isValid: (data) => PresetManager.isPossiblyReasoningData(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));
|
|
}
|
|
|
|
static isPossiblyReasoningData(data) {
|
|
const reasoningProps = ['name', 'prefix', 'suffix', 'separator'];
|
|
return data && reasoningProps.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);
|
|
}
|
|
|
|
// 5. Reasoning Template
|
|
if (this.isPossiblyReasoningData(data)) {
|
|
toastr.info(t`Importing as reasoning template...`, t`Reasoning template detected`);
|
|
return await getPresetManager('reasoning').savePreset(data.name, 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();
|
|
if (equalsIgnoreCaseAndAccents(oldName, newName)) {
|
|
throw new Error('New name must be different from old name');
|
|
}
|
|
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(api) {
|
|
let presets = [];
|
|
let preset_names = {};
|
|
|
|
// If no API specified, use the current API
|
|
if (api === undefined) {
|
|
api = this.apiId;
|
|
}
|
|
|
|
switch (api) {
|
|
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;
|
|
case 'reasoning':
|
|
presets = reasoning_templates;
|
|
preset_names = reasoning_templates.map(x => x.name);
|
|
break;
|
|
default:
|
|
console.warn(`Unknown API ID ${api}`);
|
|
}
|
|
|
|
return { presets, preset_names };
|
|
}
|
|
|
|
isKeyedApi() {
|
|
return this.apiId == 'textgenerationwebui' || this.isAdvancedFormatting();
|
|
}
|
|
|
|
isAdvancedFormatting() {
|
|
return ['context', 'instruct', 'sysprompt', 'reasoning'].includes(this.apiId);
|
|
}
|
|
|
|
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;
|
|
}
|
|
case 'reasoning': {
|
|
const reasoning_preset = structuredClone(power_user.reasoning);
|
|
reasoning_preset['name'] = name || power_user.reasoning.preset;
|
|
return reasoning_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',
|
|
|
|
// Reasoning exclusions
|
|
'auto_parse',
|
|
'add_to_prompts',
|
|
'auto_expand',
|
|
'show_hidden',
|
|
'max_additions',
|
|
];
|
|
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;
|
|
}
|
|
if (equalsIgnoreCaseAndAccents(oldName, newName)) {
|
|
console.warn('Preset name is the same (ignoring case and accents)');
|
|
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');
|
|
});
|
|
}
|