Blame Raw
Cohee · e3f41666 · · 1243 lines (44.8 KB)
1 contributor
1import { Fuse, lodash } from '../lib.js';
2
3import {
4 amount_gen,
5 characters,
6 eventSource,
7 event_types,
8 getRequestHeaders,
9 koboldai_setting_names,
10 koboldai_settings,
11 main_api,
12 max_context,
13 nai_settings,
14 novelai_setting_names,
15 novelai_settings,
16 online_status,
17 saveSettings,
18 saveSettingsDebounced,
19 this_chid,
20} from '../script.js';
21import { groups, selected_group } from './group-chats.js';
22import { t } from './i18n.js';
23import { instruct_presets } from './instruct-mode.js';
24import { kai_settings } from './kai-settings.js';
25import { convertNovelPreset } from './nai-settings.js';
26import { oai_settings, openai_setting_names, openai_settings } from './openai.js';
27import { POPUP_RESULT, POPUP_TYPE, Popup } from './popup.js';
28import { context_presets, getContextSettings, power_user } from './power-user.js';
29import { reasoning_templates } from './reasoning.js';
30import { SlashCommand } from './slash-commands/SlashCommand.js';
31import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
32import { enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
33import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
34import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
35import { checkForSystemPromptInInstructTemplate, system_prompts } from './sysprompt.js';
36import { renderTemplateAsync } from './templates.js';
37import {
38 textgenerationwebui_settings as textgen_settings,
39 textgenerationwebui_preset_names,
40 textgenerationwebui_presets,
41} from './textgen-settings.js';
42import { download, ensurePlainObject, equalsIgnoreCaseAndAccents, getSanitizedFilename, parseJsonFile, waitUntilCondition } from './utils.js';
43
44const presetManagers = {};
45
46/**
47 * Automatically select a preset for current API based on character or group name.
48 */
49function autoSelectPreset() {
50 const presetManager = getPresetManager();
51
52 if (!presetManager) {
53 console.debug(`Preset Manager not found for API: ${main_api}`);
54 return;
55 }
56
57 const name = selected_group ? groups.find(x => x.id == selected_group)?.name : characters[this_chid]?.name;
58
59 if (!name) {
60 console.debug(`Preset candidate not found for API: ${main_api}`);
61 return;
62 }
63
64 const preset = presetManager.findPreset(name);
65 const selectedPreset = presetManager.getSelectedPreset();
66
67 if (preset === selectedPreset) {
68 console.debug(`Preset already selected for API: ${main_api}, name: ${name}`);
69 return;
70 }
71
72 if (preset !== undefined && preset !== null) {
73 console.log(`Preset found for API: ${main_api}, name: ${name}`);
74 presetManager.selectPreset(preset);
75 }
76}
77
78/**
79 * Gets a preset manager by API id.
80 * @param {string} apiId API id
81 * @returns {PresetManager} Preset manager
82 */
83export function getPresetManager(apiId = '') {
84 if (apiId === 'koboldhorde') {
85 apiId = 'kobold';
86 }
87 if (!apiId) {
88 apiId = main_api == 'koboldhorde' ? 'kobold' : main_api;
89 }
90
91 if (!Object.keys(presetManagers).includes(apiId)) {
92 return null;
93 }
94
95 return presetManagers[apiId];
96}
97
98/**
99 * Registers preset managers for all select elements with data-preset-manager-for attribute.
100 */
101function registerPresetManagers() {
102 $('select[data-preset-manager-for]').each((_, e) => {
103 const forData = $(e).data('preset-manager-for');
104 for (const apiId of forData.split(',')) {
105 console.debug(`Registering preset manager for API: ${apiId}`);
106 presetManagers[apiId] = new PresetManager($(e), apiId);
107 }
108 });
109}
110
111class PresetManager {
112 constructor(select, apiId) {
113 this.select = select;
114 this.apiId = apiId;
115 }
116
117 static masterSections = {
118 'instruct': {
119 name: 'Instruct Template',
120 getData: () => {
121 const manager = getPresetManager('instruct');
122 const name = manager.getSelectedPresetName();
123 return manager.getPresetSettings(name);
124 },
125 setData: (data) => {
126 const manager = getPresetManager('instruct');
127 const name = data.name;
128 return manager.savePreset(name, data);
129 },
130 isValid: (data) => PresetManager.isPossiblyInstructData(data),
131 },
132 'context': {
133 name: 'Context Template',
134 getData: () => {
135 const manager = getPresetManager('context');
136 const name = manager.getSelectedPresetName();
137 return manager.getPresetSettings(name);
138 },
139 setData: (data) => {
140 const manager = getPresetManager('context');
141 const name = data.name;
142 return manager.savePreset(name, data);
143 },
144 isValid: (data) => PresetManager.isPossiblyContextData(data),
145 },
146 'sysprompt': {
147 name: 'System Prompt',
148 getData: () => {
149 const manager = getPresetManager('sysprompt');
150 const name = manager.getSelectedPresetName();
151 return manager.getPresetSettings(name);
152 },
153 setData: (data) => {
154 const manager = getPresetManager('sysprompt');
155 const name = data.name;
156 return manager.savePreset(name, data);
157 },
158 isValid: (data) => PresetManager.isPossiblySystemPromptData(data),
159 },
160 'preset': {
161 name: 'Text Completion Preset',
162 getData: () => {
163 const manager = getPresetManager('textgenerationwebui');
164 const name = manager.getSelectedPresetName();
165 const data = manager.getPresetSettings(name);
166 data.name = name;
167 return data;
168 },
169 setData: (data) => {
170 const manager = getPresetManager('textgenerationwebui');
171 const name = data.name;
172 return manager.savePreset(name, data);
173 },
174 isValid: (data) => PresetManager.isPossiblyTextCompletionData(data),
175 },
176 'reasoning': {
177 name: 'Reasoning Formatting',
178 getData: () => {
179 const manager = getPresetManager('reasoning');
180 const name = manager.getSelectedPresetName();
181 return manager.getPresetSettings(name);
182 },
183 setData: (data) => {
184 const manager = getPresetManager('reasoning');
185 const name = data.name;
186 return manager.savePreset(name, data);
187 },
188 isValid: (data) => PresetManager.isPossiblyReasoningData(data),
189 },
190 'srw': {
191 name: 'Start Reply With',
192 getData: () => {
193 return {
194 value: power_user.user_prompt_bias ?? '',
195 show: power_user.show_user_prompt_bias ?? false,
196 };
197 },
198 setData: (data) => {
199 power_user.user_prompt_bias = data.value ?? '';
200 power_user.show_user_prompt_bias = data.show ?? false;
201 $('#start_reply_with').val(power_user.user_prompt_bias);
202 $('#chat-show-reply-prefix-checkbox').prop('checked', power_user.show_user_prompt_bias);
203 return saveSettingsDebounced();
204 },
205 isValid: (data) => PresetManager.isPossiblyStartReplyWithData(data),
206 },
207 };
208
209 static isPossiblyInstructData(data) {
210 const instructProps = ['name', 'input_sequence', 'output_sequence'];
211 return data && instructProps.every(prop => Object.keys(data).includes(prop));
212 }
213
214 static isPossiblyContextData(data) {
215 const contextProps = ['name', 'story_string'];
216 return data && contextProps.every(prop => Object.keys(data).includes(prop));
217 }
218
219 static isPossiblySystemPromptData(data) {
220 const sysPromptProps = ['name', 'content'];
221 return data && sysPromptProps.every(prop => Object.keys(data).includes(prop));
222 }
223
224 static isPossiblyTextCompletionData(data) {
225 const textCompletionProps = ['temp', 'top_k', 'top_p', 'rep_pen'];
226 return data && textCompletionProps.every(prop => Object.keys(data).includes(prop));
227 }
228
229 static isPossiblyReasoningData(data) {
230 const reasoningProps = ['name', 'prefix', 'suffix', 'separator'];
231 return data && reasoningProps.every(prop => Object.keys(data).includes(prop));
232 }
233
234 static isPossiblyStartReplyWithData(data) {
235 return data && 'value' in data && 'show' in data;
236 }
237
238 /**
239 * Imports master settings from JSON data.
240 * @param {object} data Data to import
241 * @param {string} fileName File name
242 * @returns {Promise<void>}
243 */
244 static async performMasterImport(data, fileName) {
245 if (!data || typeof data !== 'object') {
246 toastr.error(t`Invalid data provided for master import`);
247 return;
248 }
249
250 // Check for legacy file imports
251 // 1. Instruct Template
252 if (this.isPossiblyInstructData(data)) {
253 toastr.info(t`Importing instruct template...`, t`Instruct template detected`);
254 return await getPresetManager('instruct').savePreset(data.name, data);
255 }
256
257 // 2. Context Template
258 if (this.isPossiblyContextData(data)) {
259 toastr.info(t`Importing as context template...`, t`Context template detected`);
260 return await getPresetManager('context').savePreset(data.name, data);
261 }
262
263 // 3. System Prompt
264 if (this.isPossiblySystemPromptData(data)) {
265 toastr.info(t`Importing as system prompt...`, t`System prompt detected`);
266 return await getPresetManager('sysprompt').savePreset(data.name, data);
267 }
268
269 // 4. Text Completion settings
270 if (this.isPossiblyTextCompletionData(data)) {
271 toastr.info(t`Importing as settings preset...`, t`Text Completion settings detected`);
272 return await getPresetManager('textgenerationwebui').savePreset(fileName, data);
273 }
274
275 // 5. Reasoning Template
276 if (this.isPossiblyReasoningData(data)) {
277 toastr.info(t`Importing as reasoning template...`, t`Reasoning template detected`);
278 return await getPresetManager('reasoning').savePreset(data.name, data);
279 }
280
281 const validSections = [];
282 for (const [key, section] of Object.entries(this.masterSections)) {
283 if (key in data && section.isValid(data[key])) {
284 validSections.push(key);
285 }
286 }
287
288 if (validSections.length === 0) {
289 toastr.error(t`No valid sections found in imported data`);
290 return;
291 }
292
293 const sectionNames = validSections.reduce((acc, key) => {
294 acc[key] = { key: key, name: this.masterSections[key].name, preset: data[key]?.name || '' };
295 return acc;
296 }, {});
297
298 const html = $(await renderTemplateAsync('masterImport', { sections: sectionNames }));
299 const popup = new Popup(html, POPUP_TYPE.CONFIRM, '', {
300 okButton: t`Import`,
301 cancelButton: t`Cancel`,
302 });
303
304 const result = await popup.show();
305
306 // Import cancelled
307 if (result !== POPUP_RESULT.AFFIRMATIVE) {
308 return;
309 }
310
311 const importedSections = [];
312 const confirmedSections = html.find('input:checked').map((_, el) => el instanceof HTMLInputElement && el.value).get();
313
314 if (confirmedSections.length === 0) {
315 toastr.info(t`No sections selected for import`);
316 return;
317 }
318
319 for (const section of confirmedSections) {
320 const sectionData = data[section];
321 const masterSection = this.masterSections[section];
322 if (sectionData && masterSection) {
323 await masterSection.setData(sectionData);
324 importedSections.push(masterSection.name);
325 }
326 }
327
328 toastr.success(t`Imported ${importedSections.length} settings: ${importedSections.join(', ')}`);
329 }
330
331 /**
332 * Exports master settings to JSON data.
333 * @returns {Promise<string>} JSON data
334 */
335 static async performMasterExport() {
336 const sectionNames = Object.entries(this.masterSections).reduce((acc, [key, section]) => {
337 acc[key] = { key: key, name: section.name, checked: !['preset', 'srw'].includes(key) };
338 return acc;
339 }, {});
340 const html = $(await renderTemplateAsync('masterExport', { sections: sectionNames }));
341
342 const popup = new Popup(html, POPUP_TYPE.CONFIRM, '', {
343 okButton: t`Export`,
344 cancelButton: t`Cancel`,
345 });
346
347 const result = await popup.show();
348
349 // Export cancelled
350 if (result !== POPUP_RESULT.AFFIRMATIVE) {
351 return;
352 }
353
354 const confirmedSections = html.find('input:checked').map((_, el) => el instanceof HTMLInputElement && el.value).get();
355 const data = {};
356
357 if (confirmedSections.length === 0) {
358 toastr.info(t`No sections selected for export`);
359 return;
360 }
361
362 for (const section of confirmedSections) {
363 const masterSection = this.masterSections[section];
364 if (masterSection) {
365 data[section] = masterSection.getData();
366 }
367 }
368
369 return JSON.stringify(data, null, 4);
370 }
371
372 /**
373 * Gets all preset names.
374 * @returns {string[]} List of preset names
375 */
376 getAllPresets() {
377 return $(this.select).find('option').map((_, el) => el.text).toArray();
378 }
379
380 /**
381 * Finds a preset by name.
382 * @param {string} name Preset name
383 * @returns {any} Preset value
384 */
385 findPreset(name) {
386 return $(this.select).find('option').filter(function () {
387 return $(this).text() === name;
388 }).val();
389 }
390
391 /**
392 * Gets the selected preset value.
393 * @returns {any} Selected preset value
394 */
395 getSelectedPreset() {
396 return $(this.select).find('option:selected').val();
397 }
398
399 /**
400 * Gets the selected preset name.
401 * @returns {string} Selected preset name
402 */
403 getSelectedPresetName() {
404 return $(this.select).find('option:selected').text();
405 }
406
407 /**
408 * Selects a preset by option value.
409 * @param {string} value Preset option value
410 */
411 selectPreset(value) {
412 const option = $(this.select).filter(function () {
413 return $(this).val() === value;
414 });
415 option.prop('selected', true);
416 $(this.select).val(value).trigger('change');
417 }
418
419 /**
420 * Updates the preset select element with the current API presets.
421 * @param {object} [options] Options for saving the preset
422 * @param {boolean} [options.skipUpdate=false] If true, skips updating the preset list after saving.
423 */
424 async updatePreset(option = { skipUpdate: false }) {
425 const selected = $(this.select).find('option:selected');
426 console.log(selected);
427
428 if (selected.val() == 'gui') {
429 toastr.info(t`Cannot update GUI preset`);
430 return;
431 }
432
433 const name = selected.text();
434 await this.savePreset(name, null, option);
435
436 const successToast = !this.isAdvancedFormatting() ? t`Preset updated` : t`Template updated`;
437 toastr.success(successToast);
438 }
439
440 /**
441 * Saves the currently selected preset with a new name.
442 */
443 async savePresetAs() {
444 const inputValue = this.getSelectedPresetName();
445 const popupText = !this.isAdvancedFormatting() ? '<h4>' + t`Hint: Use a character/group name to bind preset to a specific chat.` + '</h4>' : '';
446 const headerText = !this.isAdvancedFormatting() ? t`Preset name:` : t`Template name:`;
447 const name = await Popup.show.input(headerText, popupText, inputValue);
448 if (!name) {
449 console.log('Preset name not provided');
450 return;
451 }
452
453 await this.savePreset(name);
454
455 const successToast = !this.isAdvancedFormatting() ? t`Preset saved` : t`Template saved`;
456 toastr.success(successToast);
457 }
458
459 /**
460 * Saves a preset with the given name and settings.
461 * @param {string} name Name of the preset to save
462 * @param {object} [settings] Settings to save as the preset. If not provided, uses the current preset settings.
463 * @param {object} [options] Options for saving the preset
464 * @param {boolean} [options.skipUpdate=false] If true, skips updating the preset list after saving.
465 */
466 async savePreset(name, settings, { skipUpdate = false } = {}) {
467 if (this.apiId === 'instruct' && settings) {
468 await checkForSystemPromptInInstructTemplate(name, settings);
469 }
470
471 if (this.apiId === 'novel' && settings) {
472 settings = convertNovelPreset(settings);
473 }
474
475 const preset = settings ?? this.getPresetSettings(name);
476
477 const response = await fetch('/api/presets/save', {
478 method: 'POST',
479 headers: getRequestHeaders(),
480 body: JSON.stringify({ preset, name, apiId: this.apiId }),
481 });
482
483 if (!response.ok) {
484 toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Preset could not be saved`);
485 console.error('Preset could not be saved', response);
486 throw new Error('Preset could not be saved');
487 }
488
489 const data = await response.json();
490 name = data.name;
491
492 if (skipUpdate) {
493 console.debug(`Preset ${name} saved, but not updating the list`);
494 return;
495 }
496
497 this.updateList(name, preset);
498 }
499
500 /**
501 * Renames the currently selected preset.
502 * @param {string} newName New name for the preset
503 */
504 async renamePreset(newName) {
505 const oldName = this.getSelectedPresetName();
506 if (equalsIgnoreCaseAndAccents(oldName, newName)) {
507 throw new Error('New name must be different from old name');
508 }
509 try {
510 await this.savePreset(newName);
511 await this.deletePreset(oldName);
512 } catch (error) {
513 toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Preset could not be renamed`);
514 console.error('Preset could not be renamed', error);
515 throw new Error('Preset could not be renamed');
516 }
517 }
518
519 /**
520 * Gets a list of presets for the API.
521 * @param {string} [api] API ID. If not specified, uses the current API ID.
522 * @returns {{presets: any[], preset_names: object, settings: object}}
523 */
524 getPresetList(api) {
525 let presets = [];
526 let preset_names = {};
527 let settings = {};
528
529 // If no API specified, use the current API
530 if (api === undefined) {
531 api = this.apiId;
532 }
533
534 switch (api) {
535 case 'koboldhorde':
536 case 'kobold':
537 presets = koboldai_settings;
538 preset_names = koboldai_setting_names;
539 settings = kai_settings;
540 break;
541 case 'novel':
542 presets = novelai_settings;
543 preset_names = novelai_setting_names;
544 settings = nai_settings;
545 break;
546 case 'textgenerationwebui':
547 presets = textgenerationwebui_presets;
548 preset_names = textgenerationwebui_preset_names;
549 settings = textgen_settings;
550 break;
551 case 'openai':
552 presets = openai_settings;
553 preset_names = openai_setting_names;
554 settings = oai_settings;
555 break;
556 case 'context':
557 presets = context_presets;
558 preset_names = context_presets.map(x => x.name);
559 settings = power_user.context;
560 break;
561 case 'instruct':
562 presets = instruct_presets;
563 preset_names = instruct_presets.map(x => x.name);
564 settings = power_user.instruct;
565 break;
566 case 'sysprompt':
567 presets = system_prompts;
568 preset_names = system_prompts.map(x => x.name);
569 settings = power_user.sysprompt;
570 break;
571 case 'reasoning':
572 presets = reasoning_templates;
573 preset_names = reasoning_templates.map(x => x.name);
574 settings = power_user.reasoning;
575 break;
576 default:
577 console.warn(`Unknown API ID ${api}`);
578 }
579
580 return { presets, preset_names, settings };
581 }
582
583 /**
584 * Returns true if the API is keyed, meaning it uses a name to identify presets.
585 */
586 isKeyedApi() {
587 return this.apiId == 'textgenerationwebui' || this.isAdvancedFormatting();
588 }
589
590 /**
591 * Returns true if the API is from Advanced Formatting group.
592 */
593 isAdvancedFormatting() {
594 return ['context', 'instruct', 'sysprompt', 'reasoning'].includes(this.apiId);
595 }
596
597 /**
598 * Updates the preset list with a new or existing preset.
599 * @param {string} name Name of the preset
600 * @param {object} preset Preset object
601 */
602 updateList(name, preset) {
603 const { presets, preset_names } = this.getPresetList();
604 const presetExists = this.isKeyedApi() ? preset_names.includes(name) : Object.keys(preset_names).includes(name);
605
606 if (presetExists) {
607 if (this.isKeyedApi()) {
608 presets[preset_names.indexOf(name)] = preset;
609 $(this.select).find(`option[value="${name}"]`).prop('selected', true);
610 $(this.select).val(name).trigger('change');
611 } else {
612 const value = preset_names[name];
613 presets[value] = preset;
614 $(this.select).find(`option[value="${value}"]`).prop('selected', true);
615 $(this.select).val(value).trigger('change');
616 }
617 } else {
618 presets.push(preset);
619 const value = presets.length - 1;
620
621 if (this.isKeyedApi()) {
622 preset_names[value] = name;
623 const option = $('<option></option>', { value: name, text: name, selected: true });
624 $(this.select).append(option);
625 $(this.select).val(name).trigger('change');
626 } else {
627 preset_names[name] = value;
628 const option = $('<option></option>', { value: value, text: name, selected: true });
629 $(this.select).append(option);
630 $(this.select).val(value).trigger('change');
631 }
632 }
633 }
634
635 /**
636 * Gets the preset settings for the given name.
637 * @param {string} name Name of the preset
638 * @returns {object} Preset settings object for the given name
639 */
640 getPresetSettings(name) {
641 function getSettingsByApiId(apiId) {
642 switch (apiId) {
643 case 'koboldhorde':
644 case 'kobold':
645 return kai_settings;
646 case 'novel':
647 return nai_settings;
648 case 'textgenerationwebui':
649 return textgen_settings;
650 case 'context': {
651 const context_preset = getContextSettings();
652 context_preset.name = name || power_user.context.preset;
653 return context_preset;
654 }
655 case 'instruct': {
656 const instruct_preset = structuredClone(power_user.instruct);
657 instruct_preset.name = name || power_user.instruct.preset;
658 return instruct_preset;
659 }
660 case 'sysprompt': {
661 const sysprompt_preset = structuredClone(power_user.sysprompt);
662 sysprompt_preset.name = name || power_user.sysprompt.preset;
663 return sysprompt_preset;
664 }
665 case 'reasoning': {
666 const reasoning_preset = structuredClone(power_user.reasoning);
667 reasoning_preset.name = name || power_user.reasoning.preset;
668 return reasoning_preset;
669 }
670 default:
671 console.warn(`Unknown API ID ${apiId}`);
672 return {};
673 }
674 }
675
676 const filteredKeys = [
677 'api_server',
678 'preset',
679 'streaming',
680 'truncation_length',
681 'n',
682 'streaming_url',
683 'stopping_strings',
684 'can_use_tokenization',
685 'can_use_streaming',
686 'preset_settings_novel',
687 'preset_settings',
688 'streaming_novel',
689 'nai_preamble',
690 'model_novel',
691 'streaming_kobold',
692 'enabled',
693 'bind_to_context',
694 'seed',
695 'legacy_api',
696 'mancer_model',
697 'togetherai_model',
698 'ollama_model',
699 'vllm_model',
700 'aphrodite_model',
701 'llamacpp_model',
702 'server_urls',
703 'type',
704 'custom_model',
705 'bypass_status_check',
706 'infermaticai_model',
707 'dreamgen_model',
708 'openrouter_model',
709 'featherless_model',
710 'max_tokens_second',
711 'openrouter_providers',
712 'openrouter_quantizations',
713 'openrouter_allow_fallbacks',
714 'tabby_model',
715 'derived',
716 'generic_model',
717 'include_reasoning',
718 'global_banned_tokens',
719 'send_banned_tokens',
720
721 // Reasoning exclusions
722 'auto_parse',
723 'add_to_prompts',
724 'auto_expand',
725 'show_hidden',
726 'max_additions',
727 ];
728 /** @type {Record<string, any>} */
729 const settings = Object.assign({}, getSettingsByApiId(this.apiId));
730
731 for (const key of filteredKeys) {
732 if (Object.hasOwn(settings, key)) {
733 delete settings[key];
734 }
735 }
736
737 if (!this.isAdvancedFormatting() && this.apiId !== 'openai') {
738 settings.genamt = amount_gen;
739 settings.max_length = max_context;
740 }
741
742 return settings;
743 }
744
745 /**
746 * Retrieves a completion preset by name.
747 * @param {string} name Name of the preset to retrieve
748 * @returns {any} Preset object if found, otherwise undefined
749 */
750 getCompletionPresetByName(name) {
751 // Retrieve a completion preset by name. Return undefined if not found.
752 let { presets, preset_names } = this.getPresetList();
753 let preset;
754
755 // Some APIs use an array of names, others use an object of {name: index}
756 if (Array.isArray(preset_names)) { // array of names
757 if (preset_names.includes(name)) {
758 preset = presets[preset_names.indexOf(name)];
759 }
760 } else { // object of {names: index}
761 if (preset_names[name] !== undefined) {
762 preset = presets[preset_names[name]];
763 }
764 }
765
766 if (preset === undefined) {
767 console.error(`Preset ${name} not found`);
768 }
769
770 // if the preset isn't found, returns undefined
771 return preset;
772 }
773
774 /**
775 * Deletes a preset by name. If not provided, deletes the currently selected preset.
776 * @param {string} [name] Name of the preset to delete.
777 */
778 async deletePreset(name) {
779 const { preset_names, presets } = this.getPresetList();
780 const value = name ? (this.isKeyedApi() ? this.findPreset(name) : name) : this.getSelectedPreset();
781 const nameToDelete = name || this.getSelectedPresetName();
782
783 if (value == 'gui') {
784 toastr.info(t`Cannot delete GUI preset`);
785 return;
786 }
787
788 if (this.isKeyedApi()) {
789 $(this.select).find(`option[value="${value}"]`).remove();
790 const index = preset_names.indexOf(nameToDelete);
791 preset_names.splice(index, 1);
792 presets.splice(index, 1);
793 } else {
794 const index = preset_names[nameToDelete];
795 $(this.select).find(`option[value="${index}"]`).remove();
796 delete preset_names[nameToDelete];
797 }
798
799 // switch in UI only when deleting currently selected preset
800 const switchPresets = !name || this.getSelectedPresetName() == name;
801
802 if (Object.keys(preset_names).length && switchPresets) {
803 const nextPresetName = Object.keys(preset_names)[0];
804 const newValue = preset_names[nextPresetName];
805 $(this.select).find(`option[value="${newValue}"]`).attr('selected', 'true');
806 $(this.select).trigger('change');
807 }
808
809 const response = await fetch('/api/presets/delete', {
810 method: 'POST',
811 headers: getRequestHeaders(),
812 body: JSON.stringify({ name: nameToDelete, apiId: this.apiId }),
813 });
814
815 return response.ok;
816 }
817
818 /**
819 * Retrieves the default preset for the API from the server.
820 * @param {string} name Name of the preset to restore
821 * @returns {Promise<any>} Default preset object, or undefined if the request fails
822 */
823 async getDefaultPreset(name) {
824 const response = await fetch('/api/presets/restore', {
825 method: 'POST',
826 headers: getRequestHeaders(),
827 body: JSON.stringify({ name, apiId: this.apiId }),
828 });
829
830 if (!response.ok) {
831 const errorToast = !this.isAdvancedFormatting() ? t`Failed to restore default preset` : t`Failed to restore default template`;
832 toastr.error(errorToast);
833 return;
834 }
835
836 return await response.json();
837 }
838
839 /**
840 * Reads a preset extension field from the preset.
841 * @param {object} options
842 * @param {string} [options.name] Name of the preset. If not provided, uses the currently selected preset name.
843 * @param {string} options.path Path to the preset extension field, e.g. 'myextension.data'. If empty, reads the entire extensions object.
844 * @return {any} The value of the preset extension field, or null if not found.
845 */
846 readPresetExtensionField({ name, path }) {
847 const { settings } = this.getPresetList();
848 const selectedName = this.getSelectedPresetName();
849 const presetName = name || selectedName;
850
851 // Read from settings if the selected preset is the same as the provided name
852 if (settings && selectedName === presetName) {
853 const settingsExtensions = ensurePlainObject(settings.extensions || {});
854 return path ? lodash.get(settingsExtensions, path, null) : settingsExtensions;
855 }
856
857 // Otherwise, read from the preset by name
858 const preset = this.getCompletionPresetByName(presetName);
859 if (!preset) {
860 return null;
861 }
862
863 const presetExtensions = ensurePlainObject(preset.extensions || {});
864 const value = path ? lodash.get(presetExtensions, path, null) : presetExtensions;
865 return value;
866 }
867
868 /**
869 * Writes a value to a preset extension field.
870 * @param {object} options
871 * @param {string} [options.name] Name of the preset. If not provided, uses the currently selected preset name.
872 * @param {string} options.path Path to the preset extension field, e.g. 'myextension.data'. If empty, writes to the root of the extensions object.
873 * @param {any} options.value Value to write to the preset extension field.
874 * @return {Promise<void>} Resolves when the preset is saved.
875 */
876 async writePresetExtensionField({ name, path, value }) {
877 const { settings } = this.getPresetList();
878 const selectedName = this.getSelectedPresetName();
879 const presetName = name || selectedName;
880
881 // Write to settings if the selected preset is the same as the provided name
882 if (settings && selectedName === presetName) {
883 // Set the value at the specified path
884 settings.extensions = ensurePlainObject(settings.extensions || {});
885 path ? lodash.set(settings.extensions, path, value) : (settings.extensions = value);
886 await saveSettings();
887 }
888
889 // Also update the preset by name
890 const preset = this.getCompletionPresetByName(presetName);
891 if (!preset) {
892 return;
893 }
894
895 // Set the value at the specified path
896 preset.extensions = ensurePlainObject(preset.extensions || {});
897 path ? lodash.set(preset.extensions, path, value) : (preset.extensions = value);
898
899 // Save the updated preset
900 await this.savePreset(presetName, preset, { skipUpdate: true });
901 }
902}
903
904/**
905 * Selects a preset by name for current API.
906 * @param {any} _ Named arguments
907 * @param {string} name Unnamed arguments
908 * @returns {Promise<string>} Selected or current preset name
909 */
910async function presetCommandCallback(_, name) {
911 const shouldReconnect = online_status !== 'no_connection';
912 const presetManager = getPresetManager();
913 const allPresets = presetManager.getAllPresets();
914 const currentPreset = presetManager.getSelectedPresetName();
915
916 if (!presetManager) {
917 console.debug(`Preset Manager not found for API: ${main_api}`);
918 return '';
919 }
920
921 if (!name) {
922 console.log('No name provided for /preset command, using current preset');
923 return currentPreset;
924 }
925
926 if (!Array.isArray(allPresets) || allPresets.length === 0) {
927 console.log(`No presets found for API: ${main_api}`);
928 return currentPreset;
929 }
930
931 // Find exact match
932 const exactMatch = allPresets.find(p => p.toLowerCase().trim() === name.toLowerCase().trim());
933
934 if (exactMatch) {
935 console.log('Found exact preset match', exactMatch);
936
937 if (currentPreset !== exactMatch) {
938 const presetValue = presetManager.findPreset(exactMatch);
939
940 if (presetValue) {
941 presetManager.selectPreset(presetValue);
942 shouldReconnect && await waitForConnection();
943 }
944 }
945
946 return exactMatch;
947 } else {
948 // Find fuzzy match
949 const fuse = new Fuse(allPresets);
950 const fuzzyMatch = fuse.search(name);
951
952 if (!fuzzyMatch.length) {
953 console.warn(`WARN: Preset found with name ${name}`);
954 return currentPreset;
955 }
956
957 const fuzzyPresetName = fuzzyMatch[0].item;
958 const fuzzyPresetValue = presetManager.findPreset(fuzzyPresetName);
959
960 if (fuzzyPresetValue) {
961 console.log('Found fuzzy preset match', fuzzyPresetName);
962
963 if (currentPreset !== fuzzyPresetName) {
964 presetManager.selectPreset(fuzzyPresetValue);
965 shouldReconnect && await waitForConnection();
966 }
967 }
968
969 return fuzzyPresetName;
970 }
971}
972
973/**
974 * Waits for API connection to be established.
975 */
976async function waitForConnection() {
977 try {
978 await waitUntilCondition(() => online_status !== 'no_connection', 10000, 100);
979 } catch {
980 console.log('Timeout waiting for API to connect');
981 }
982}
983
984export async function initPresetManager() {
985 eventSource.on(event_types.CHAT_CHANGED, autoSelectPreset);
986 registerPresetManagers();
987 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
988 name: 'preset',
989 callback: presetCommandCallback,
990 returns: 'current preset',
991 unnamedArgumentList: [
992 SlashCommandArgument.fromProps({
993 description: 'name',
994 typeList: [ARGUMENT_TYPE.STRING],
995 enumProvider: () => getPresetManager().getAllPresets().map(preset => new SlashCommandEnumValue(preset, null, enumTypes.enum, enumIcons.preset)),
996 }),
997 ],
998 helpString: `
999 <div>
1000 Sets a preset by name for the current API. Gets the current preset if no name is provided.
1001 </div>
1002 <div>
1003 <strong>Example:</strong>
1004 <ul>
1005 <li>
1006 <pre><code>/preset myPreset</code></pre>
1007 </li>
1008 <li>
1009 <pre><code>/preset</code></pre>
1010 </li>
1011 </ul>
1012 </div>
1013 `,
1014 }));
1015
1016
1017 $(document).on('click', '[data-preset-manager-update]', async function () {
1018 const apiId = $(this).data('preset-manager-update');
1019 const presetManager = getPresetManager(apiId);
1020
1021 if (!presetManager) {
1022 console.warn(`Preset Manager not found for API: ${apiId}`);
1023 return;
1024 }
1025
1026 await presetManager.updatePreset();
1027 });
1028
1029 $(document).on('click', '[data-preset-manager-new]', async function () {
1030 const apiId = $(this).data('preset-manager-new');
1031 const presetManager = getPresetManager(apiId);
1032
1033 if (!presetManager) {
1034 console.warn(`Preset Manager not found for API: ${apiId}`);
1035 return;
1036 }
1037
1038 await presetManager.savePresetAs();
1039 });
1040
1041 $(document).on('click', '[data-preset-manager-rename]', async function () {
1042 const apiId = $(this).data('preset-manager-rename');
1043 const presetManager = getPresetManager(apiId);
1044
1045 if (!presetManager) {
1046 console.warn(`Preset Manager not found for API: ${apiId}`);
1047 return;
1048 }
1049
1050 const popupHeader = !presetManager.isAdvancedFormatting() ? t`Rename preset` : t`Rename template`;
1051 const oldName = presetManager.getSelectedPresetName();
1052 const newName = await getSanitizedFilename(await Popup.show.input(popupHeader, t`Enter a new name:`, oldName) || '');
1053 if (!newName || oldName === newName) {
1054 console.debug(!presetManager.isAdvancedFormatting() ? 'Preset rename cancelled' : 'Template rename cancelled');
1055 return;
1056 }
1057 if (equalsIgnoreCaseAndAccents(oldName, newName)) {
1058 toastr.warning(t`Name not accepted, as it is the same as before (ignoring case and accents).`, t`Rename Preset`);
1059 return;
1060 }
1061
1062 await eventSource.emit(event_types.PRESET_RENAMED_BEFORE, { apiId: apiId, oldName: oldName, newName: newName });
1063 const extensions = presetManager.readPresetExtensionField({ name: oldName, path: '' });
1064 await presetManager.renamePreset(newName);
1065 await presetManager.writePresetExtensionField({ name: newName, path: '', value: extensions });
1066 await eventSource.emit(event_types.PRESET_RENAMED, { apiId: apiId, oldName: oldName, newName: newName });
1067
1068 if (apiId === 'openai') {
1069 // This is a horrible mess, but prevents the renamed preset from being corrupted.
1070 $('#update_oai_preset').trigger('click');
1071 return;
1072 }
1073
1074 const successToast = !presetManager.isAdvancedFormatting() ? t`Preset renamed` : t`Template renamed`;
1075 toastr.success(successToast);
1076 });
1077
1078 $(document).on('click', '[data-preset-manager-export]', async function () {
1079 const apiId = $(this).data('preset-manager-export');
1080 const presetManager = getPresetManager(apiId);
1081
1082 if (!presetManager) {
1083 console.warn(`Preset Manager not found for API: ${apiId}`);
1084 return;
1085 }
1086
1087 const selected = $(presetManager.select).find('option:selected');
1088 const name = selected.text();
1089 const preset = presetManager.getPresetSettings(name);
1090 const data = JSON.stringify(preset, null, 4);
1091 download(data, `${name}.json`, 'application/json');
1092 });
1093
1094 $(document).on('click', '[data-preset-manager-import]', async function () {
1095 const apiId = $(this).data('preset-manager-import');
1096 $(`[data-preset-manager-file="${apiId}"]`).trigger('click');
1097 });
1098
1099 $(document).on('change', '[data-preset-manager-file]', async function (e) {
1100 const apiId = $(this).data('preset-manager-file');
1101 const presetManager = getPresetManager(apiId);
1102
1103 if (!presetManager) {
1104 console.warn(`Preset Manager not found for API: ${apiId}`);
1105 return;
1106 }
1107
1108 const file = e.target.files[0];
1109
1110 if (!file) {
1111 return;
1112 }
1113
1114 const fileName = file.name.replace('.json', '').replace('.settings', '');
1115 const data = await parseJsonFile(file);
1116 const name = data?.name ?? fileName;
1117 data.name = name;
1118
1119 await presetManager.savePreset(name, data);
1120 const successToast = !presetManager.isAdvancedFormatting() ? t`Preset imported` : t`Template imported`;
1121 toastr.success(successToast);
1122 e.target.value = null;
1123 });
1124
1125 $(document).on('click', '[data-preset-manager-delete]', async function () {
1126 const apiId = $(this).data('preset-manager-delete');
1127 const presetManager = getPresetManager(apiId);
1128
1129 if (!presetManager) {
1130 console.warn(`Preset Manager not found for API: ${apiId}`);
1131 return;
1132 }
1133
1134 const headerText = !presetManager.isAdvancedFormatting() ? t`Delete this preset?` : t`Delete this template?`;
1135 const confirm = await Popup.show.confirm(headerText, t`This action is irreversible and your current settings will be overwritten.`);
1136 if (!confirm) {
1137 return;
1138 }
1139
1140 const name = presetManager.getSelectedPresetName();
1141 const result = await presetManager.deletePreset();
1142
1143 if (result) {
1144 const successToast = !presetManager.isAdvancedFormatting() ? t`Preset deleted` : t`Template deleted`;
1145 toastr.success(successToast);
1146 await eventSource.emit(event_types.PRESET_DELETED, { apiId, name });
1147 } else {
1148 const warningToast = !presetManager.isAdvancedFormatting() ? t`Preset was not deleted from server` : t`Template was not deleted from server`;
1149 toastr.warning(warningToast);
1150 }
1151
1152 saveSettingsDebounced();
1153 });
1154
1155 $(document).on('click', '[data-preset-manager-restore]', async function () {
1156 const apiId = $(this).data('preset-manager-restore');
1157 const presetManager = getPresetManager(apiId);
1158
1159 if (!presetManager) {
1160 console.warn(`Preset Manager not found for API: ${apiId}`);
1161 return;
1162 }
1163
1164 const name = presetManager.getSelectedPresetName();
1165 const data = await presetManager.getDefaultPreset(name);
1166
1167 if (name == 'gui') {
1168 toastr.info(t`Cannot restore GUI preset`);
1169 return;
1170 }
1171
1172 if (!data) {
1173 return;
1174 }
1175
1176 if (data.isDefault) {
1177 if (Object.keys(data.preset).length === 0) {
1178 const errorToast = !presetManager.isAdvancedFormatting() ? t`Default preset cannot be restored` : t`Default template cannot be restored`;
1179 toastr.error(errorToast);
1180 return;
1181 }
1182
1183 const confirmText = !presetManager.isAdvancedFormatting()
1184 ? t`Resetting a <b>default preset</b> will restore the default settings.`
1185 : t`Resetting a <b>default template</b> will restore the default settings.`;
1186 const confirm = await Popup.show.confirm(t`Are you sure?`, confirmText);
1187 if (!confirm) {
1188 return;
1189 }
1190
1191 await presetManager.deletePreset();
1192 await presetManager.savePreset(name, data.preset);
1193 const option = presetManager.findPreset(name);
1194 presetManager.selectPreset(option);
1195 const successToast = !presetManager.isAdvancedFormatting() ? t`Default preset restored` : t`Default template restored`;
1196 toastr.success(successToast);
1197 } else {
1198 const confirmText = !presetManager.isAdvancedFormatting()
1199 ? t`Resetting a <b>custom preset</b> will restore to the last saved state.`
1200 : t`Resetting a <b>custom template</b> will restore to the last saved state.`;
1201 const confirm = await Popup.show.confirm(t`Are you sure?`, confirmText);
1202 if (!confirm) {
1203 return;
1204 }
1205
1206 const option = presetManager.findPreset(name);
1207 presetManager.selectPreset(option);
1208 const successToast = !presetManager.isAdvancedFormatting() ? t`Preset restored` : t`Template restored`;
1209 toastr.success(successToast);
1210 }
1211 });
1212
1213 $('#af_master_import').on('click', () => {
1214 $('#af_master_import_file').trigger('click');
1215 });
1216
1217 $('#af_master_import_file').on('change', async function (e) {
1218 if (!(e.target instanceof HTMLInputElement)) {
1219 return;
1220 }
1221 const file = e.target.files[0];
1222
1223 if (!file) {
1224 return;
1225 }
1226
1227 const data = await parseJsonFile(file);
1228 const fileName = file.name.replace('.json', '');
1229 await PresetManager.performMasterImport(data, fileName);
1230 e.target.value = null;
1231 });
1232
1233 $('#af_master_export').on('click', async () => {
1234 const data = await PresetManager.performMasterExport();
1235
1236 if (!data) {
1237 return;
1238 }
1239
1240 const shortDate = new Date().toISOString().split('T')[0];
1241 download(data, `ST-formatting-${shortDate}.json`, 'application/json');
1242 });
1243}