Preset Manager: Add methods for reading/writing custom data (#4246) * Preset Manager: Add methods for reading/writing custom data * Fix loading novel extensions on load * Move ensurePlainObject to utils * Init empty object in convertNovelPreset

ea5181af08f05801e8867c072a25709f8b1e4832

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
6 files changed, +178 -6Showing whitespace changes
public/scripts/kai-settings.js+6 -0
@@ -44,6 +44,7 @@ export const kai_settings = {
44 seed: -1,44 seed: -1,
45 api_server: '',45 api_server: '',
46 preset_settings: 'gui',46 preset_settings: 'gui',
47 extensions: {},
47};48};
4849
49/**50/**
@@ -130,6 +131,11 @@ export function loadKoboldSettings(data, preset, settings) {
130131
131function loadKoboldSettingsFromPreset(preset) {132function loadKoboldSettingsFromPreset(preset) {
132 for (const name of Object.keys(kai_settings)) {133 for (const name of Object.keys(kai_settings)) {
134 if (name === 'extensions') {
135 kai_settings.extensions = preset.extensions || {};
136 continue;
137 }
138
133 const value = preset[name] ?? defaultValues[name];139 const value = preset[name] ?? defaultValues[name];
134 const slider = sliders.find(x => x.name === name);140 const slider = sliders.find(x => x.name === name);
135141
public/scripts/nai-settings.js+4 -0
@@ -56,6 +56,7 @@ export const nai_settings = {
56 banned_tokens: '',56 banned_tokens: '',
57 order: default_order,57 order: default_order,
58 logit_bias: [],58 logit_bias: [],
59 extensions: {},
59};60};
6061
61const nai_tiers = {62const nai_tiers = {
@@ -140,6 +141,7 @@ export function convertNovelPreset(data) {
140 math1_quad_entropy_scale: data.parameters.math1_quad_entropy_scale,141 math1_quad_entropy_scale: data.parameters.math1_quad_entropy_scale,
141 min_p: data.parameters.min_p,142 min_p: data.parameters.min_p,
142 order: Array.isArray(data.parameters.order) ? data.parameters.order.filter(s => s.enabled && Object.keys(samplers).includes(s.id)).map(s => samplers[s.id]) : default_order,143 order: Array.isArray(data.parameters.order) ? data.parameters.order.filter(s => s.enabled && Object.keys(samplers).includes(s.id)).map(s => samplers[s.id]) : default_order,
144 extensions: {},
143 };145 };
144}146}
145147
@@ -205,6 +207,7 @@ export function loadNovelPreset(preset) {
205 nai_settings.math1_temp = preset.math1_temp || 1;207 nai_settings.math1_temp = preset.math1_temp || 1;
206 nai_settings.math1_quad = preset.math1_quad || 0;208 nai_settings.math1_quad = preset.math1_quad || 0;
207 nai_settings.math1_quad_entropy_scale = preset.math1_quad_entropy_scale || 0;209 nai_settings.math1_quad_entropy_scale = preset.math1_quad_entropy_scale || 0;
210 nai_settings.extensions = preset.extensions || {};
208 loadNovelSettingsUi(nai_settings);211 loadNovelSettingsUi(nai_settings);
209}212}
210213
@@ -258,6 +261,7 @@ export function loadNovelSettings(data, settings) {
258 nai_settings.math1_temp = settings.math1_temp || 1;261 nai_settings.math1_temp = settings.math1_temp || 1;
259 nai_settings.math1_quad = settings.math1_quad || 0;262 nai_settings.math1_quad = settings.math1_quad || 0;
260 nai_settings.math1_quad_entropy_scale = settings.math1_quad_entropy_scale || 0;263 nai_settings.math1_quad_entropy_scale = settings.math1_quad_entropy_scale || 0;
264 nai_settings.extensions = settings.extensions || {};
261 loadNovelSettingsUi(nai_settings);265 loadNovelSettingsUi(nai_settings);
262}266}
263267
public/scripts/openai.js+11 -0
@@ -332,6 +332,7 @@ export const settingsToUpdate = {
332 n: ['#n_openai', 'n', false, false],332 n: ['#n_openai', 'n', false, false],
333 bypass_status_check: ['#openai_bypass_status_check', 'bypass_status_check', true, true],333 bypass_status_check: ['#openai_bypass_status_check', 'bypass_status_check', true, true],
334 request_images: ['#openai_request_images', 'request_images', true, false],334 request_images: ['#openai_request_images', 'request_images', true, false],
335 extensions: ['#NULL_SELECTOR', 'extensions', false, false],
335};336};
336337
337const default_settings = {338const default_settings = {
@@ -421,6 +422,7 @@ const default_settings = {
421 seed: -1,422 seed: -1,
422 n: 1,423 n: 1,
423 bind_preset_to_connection: true,424 bind_preset_to_connection: true,
425 extensions: {},
424};426};
425427
426const oai_settings = {428const oai_settings = {
@@ -510,6 +512,7 @@ const oai_settings = {
510 seed: -1,512 seed: -1,
511 n: 1,513 n: 1,
512 bind_preset_to_connection: true,514 bind_preset_to_connection: true,
515 extensions: {},
513};516};
514517
515export let proxies = [518export let proxies = [
@@ -3600,6 +3603,7 @@ function loadOpenAISettings(data, settings) {
3600 oai_settings.function_calling = settings.function_calling ?? default_settings.function_calling;3603 oai_settings.function_calling = settings.function_calling ?? default_settings.function_calling;
3601 oai_settings.openrouter_providers = settings.openrouter_providers ?? default_settings.openrouter_providers;3604 oai_settings.openrouter_providers = settings.openrouter_providers ?? default_settings.openrouter_providers;
3602 oai_settings.bind_preset_to_connection = settings.bind_preset_to_connection ?? default_settings.bind_preset_to_connection;3605 oai_settings.bind_preset_to_connection = settings.bind_preset_to_connection ?? default_settings.bind_preset_to_connection;
3606 oai_settings.extensions = settings.extensions ?? default_settings.extensions;
36033607
3604 // Migrate from old settings3608 // Migrate from old settings
3605 if (settings.names_in_completion === true) {3609 if (settings.names_in_completion === true) {
@@ -4024,6 +4028,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
4024 request_images: settings.request_images,4028 request_images: settings.request_images,
4025 seed: settings.seed,4029 seed: settings.seed,
4026 n: settings.n,4030 n: settings.n,
4031 extensions: settings.extensions,
4027 };4032 };
40284033
4029 const savePresetSettings = await fetch('/api/presets/save', {4034 const savePresetSettings = await fetch('/api/presets/save', {
@@ -4468,6 +4473,12 @@ function onSettingsPresetChange() {
4468 continue;4473 continue;
4469 }4474 }
44704475
4476 // Extensions don't need UI updates and shouldn't fallback to current settings
4477 if (key === 'extensions') {
4478 oai_settings.extensions = preset.extensions || {};
4479 continue;
4480 }
4481
4471 if (preset[key] !== undefined) {4482 if (preset[key] !== undefined) {
4472 if (isCheckbox) {4483 if (isCheckbox) {
4473 updateCheckbox(selector, preset[key]);4484 updateCheckbox(selector, preset[key]);
public/scripts/preset-manager.js+136 -6
@@ -1,4 +1,4 @@
1import { Fuse } from '../lib.js';1import { Fuse, lodash } from '../lib.js';
22
3import {3import {
4 amount_gen,4 amount_gen,
@@ -14,6 +14,7 @@ import {
14 novelai_setting_names,14 novelai_setting_names,
15 novelai_settings,15 novelai_settings,
16 online_status,16 online_status,
17 saveSettings,
17 saveSettingsDebounced,18 saveSettingsDebounced,
18 this_chid,19 this_chid,
19} from '../script.js';20} from '../script.js';
@@ -36,7 +37,7 @@ import {
36 textgenerationwebui_presets,37 textgenerationwebui_presets,
37 textgenerationwebui_settings as textgen_settings,38 textgenerationwebui_settings as textgen_settings,
38} from './textgen-settings.js';39} from './textgen-settings.js';
39import { download, equalsIgnoreCaseAndAccents, getSanitizedFilename, parseJsonFile, waitUntilCondition } from './utils.js';40import { download, ensurePlainObject, equalsIgnoreCaseAndAccents, getSanitizedFilename, parseJsonFile, waitUntilCondition } from './utils.js';
40import { t } from './i18n.js';41import { t } from './i18n.js';
41import { reasoning_templates } from './reasoning.js';42import { reasoning_templates } from './reasoning.js';
4243
@@ -391,6 +392,9 @@ class PresetManager {
391 $(this.select).val(value).trigger('change');392 $(this.select).val(value).trigger('change');
392 }393 }
393394
395 /**
396 * Updates the preset select element with the current API presets.
397 */
394 async updatePreset() {398 async updatePreset() {
395 const selected = $(this.select).find('option:selected');399 const selected = $(this.select).find('option:selected');
396 console.log(selected);400 console.log(selected);
@@ -407,6 +411,9 @@ class PresetManager {
407 toastr.success(successToast);411 toastr.success(successToast);
408 }412 }
409413
414 /**
415 * Saves the currently selected preset with a new name.
416 */
410 async savePresetAs() {417 async savePresetAs() {
411 const inputValue = this.getSelectedPresetName();418 const inputValue = this.getSelectedPresetName();
412 const popupText = !this.isAdvancedFormatting() ? '<h4>' + t`Hint: Use a character/group name to bind preset to a specific chat.` + '</h4>' : '';419 const popupText = !this.isAdvancedFormatting() ? '<h4>' + t`Hint: Use a character/group name to bind preset to a specific chat.` + '</h4>' : '';
@@ -423,7 +430,14 @@ class PresetManager {
423 toastr.success(successToast);430 toastr.success(successToast);
424 }431 }
425432
426 async savePreset(name, settings) {433 /**
434 * Saves a preset with the given name and settings.
435 * @param {string} name Name of the preset to save
436 * @param {object} [settings] Settings to save as the preset. If not provided, uses the current preset settings.
437 * @param {object} [options] Options for saving the preset
438 * @param {boolean} [options.skipUpdate=false] If true, skips updating the preset list after saving.
439 */
440 async savePreset(name, settings, { skipUpdate = false } = {}) {
427 if (this.apiId === 'instruct' && settings) {441 if (this.apiId === 'instruct' && settings) {
428 await checkForSystemPromptInInstructTemplate(name, settings);442 await checkForSystemPromptInInstructTemplate(name, settings);
429 }443 }
@@ -449,9 +463,18 @@ class PresetManager {
449 const data = await response.json();463 const data = await response.json();
450 name = data.name;464 name = data.name;
451465
466 if (skipUpdate) {
467 console.debug(`Preset ${name} saved, but not updating the list`);
468 return;
469 }
470
452 this.updateList(name, preset);471 this.updateList(name, preset);
453 }472 }
454473
474 /**
475 * Renames the currently selected preset.
476 * @param {string} newName New name for the preset
477 */
455 async renamePreset(newName) {478 async renamePreset(newName) {
456 const oldName = this.getSelectedPresetName();479 const oldName = this.getSelectedPresetName();
457 if (equalsIgnoreCaseAndAccents(oldName, newName)) {480 if (equalsIgnoreCaseAndAccents(oldName, newName)) {
@@ -468,9 +491,15 @@ class PresetManager {
468491
469 }492 }
470493
494 /**
495 * Gets a list of presets for the API.
496 * @param {string} [api] API ID. If not specified, uses the current API ID.
497 * @returns {{presets: any[], preset_names: object, settings: object}}
498 */
471 getPresetList(api) {499 getPresetList(api) {
472 let presets = [];500 let presets = [];
473 let preset_names = {};501 let preset_names = {};
502 let settings = {};
474503
475 // If no API specified, use the current API504 // If no API specified, use the current API
476 if (api === undefined) {505 if (api === undefined) {
@@ -482,50 +511,69 @@ class PresetManager {
482 case 'kobold':511 case 'kobold':
483 presets = koboldai_settings;512 presets = koboldai_settings;
484 preset_names = koboldai_setting_names;513 preset_names = koboldai_setting_names;
514 settings = kai_settings;
485 break;515 break;
486 case 'novel':516 case 'novel':
487 presets = novelai_settings;517 presets = novelai_settings;
488 preset_names = novelai_setting_names;518 preset_names = novelai_setting_names;
519 settings = nai_settings;
489 break;520 break;
490 case 'textgenerationwebui':521 case 'textgenerationwebui':
491 presets = textgenerationwebui_presets;522 presets = textgenerationwebui_presets;
492 preset_names = textgenerationwebui_preset_names;523 preset_names = textgenerationwebui_preset_names;
524 settings = textgen_settings;
493 break;525 break;
494 case 'openai':526 case 'openai':
495 presets = openai_settings;527 presets = openai_settings;
496 preset_names = openai_setting_names;528 preset_names = openai_setting_names;
529 settings = openai_settings;
497 break;530 break;
498 case 'context':531 case 'context':
499 presets = context_presets;532 presets = context_presets;
500 preset_names = context_presets.map(x => x.name);533 preset_names = context_presets.map(x => x.name);
534 settings = power_user.context;
501 break;535 break;
502 case 'instruct':536 case 'instruct':
503 presets = instruct_presets;537 presets = instruct_presets;
504 preset_names = instruct_presets.map(x => x.name);538 preset_names = instruct_presets.map(x => x.name);
539 settings = power_user.instruct;
505 break;540 break;
506 case 'sysprompt':541 case 'sysprompt':
507 presets = system_prompts;542 presets = system_prompts;
508 preset_names = system_prompts.map(x => x.name);543 preset_names = system_prompts.map(x => x.name);
544 settings = power_user.sysprompt;
509 break;545 break;
510 case 'reasoning':546 case 'reasoning':
511 presets = reasoning_templates;547 presets = reasoning_templates;
512 preset_names = reasoning_templates.map(x => x.name);548 preset_names = reasoning_templates.map(x => x.name);
549 settings = power_user.reasoning;
513 break;550 break;
514 default:551 default:
515 console.warn(`Unknown API ID ${api}`);552 console.warn(`Unknown API ID ${api}`);
516 }553 }
517554
518 return { presets, preset_names };555 return { presets, preset_names, settings };
519 }556 }
520557
558 /**
559 * Returns true if the API is keyed, meaning it uses a name to identify presets.
560 */
521 isKeyedApi() {561 isKeyedApi() {
522 return this.apiId == 'textgenerationwebui' || this.isAdvancedFormatting();562 return this.apiId == 'textgenerationwebui' || this.isAdvancedFormatting();
523 }563 }
524564
565 /**
566 * Returns true if the API is from Advanced Formatting group.
567 */
525 isAdvancedFormatting() {568 isAdvancedFormatting() {
526 return ['context', 'instruct', 'sysprompt', 'reasoning'].includes(this.apiId);569 return ['context', 'instruct', 'sysprompt', 'reasoning'].includes(this.apiId);
527 }570 }
528571
572 /**
573 * Updates the preset list with a new or existing preset.
574 * @param {string} name Name of the preset
575 * @param {object} preset Preset object
576 */
529 updateList(name, preset) {577 updateList(name, preset) {
530 const { presets, preset_names } = this.getPresetList();578 const { presets, preset_names } = this.getPresetList();
531 const presetExists = this.isKeyedApi() ? preset_names.includes(name) : Object.keys(preset_names).includes(name);579 const presetExists = this.isKeyedApi() ? preset_names.includes(name) : Object.keys(preset_names).includes(name);
@@ -561,6 +609,11 @@ class PresetManager {
561 }609 }
562 }610 }
563611
612 /**
613 * Gets the preset settings for the given name.
614 * @param {string} name Name of the preset
615 * @returns {object} Preset settings object for the given name
616 */
564 getPresetSettings(name) {617 getPresetSettings(name) {
565 function getSettingsByApiId(apiId) {618 function getSettingsByApiId(apiId) {
566 switch (apiId) {619 switch (apiId) {
@@ -655,7 +708,7 @@ class PresetManager {
655 }708 }
656 }709 }
657710
658 if (!this.isAdvancedFormatting()) {711 if (!this.isAdvancedFormatting() && this.apiId !== 'openai') {
659 settings['genamt'] = amount_gen;712 settings['genamt'] = amount_gen;
660 settings['max_length'] = max_context;713 settings['max_length'] = max_context;
661 }714 }
@@ -663,6 +716,11 @@ class PresetManager {
663 return settings;716 return settings;
664 }717 }
665718
719 /**
720 * Retrieves a completion preset by name.
721 * @param {string} name Name of the preset to retrieve
722 * @returns {any} Preset object if found, otherwise undefined
723 */
666 getCompletionPresetByName(name) {724 getCompletionPresetByName(name) {
667 // Retrieve a completion preset by name. Return undefined if not found.725 // Retrieve a completion preset by name. Return undefined if not found.
668 let { presets, preset_names } = this.getPresetList();726 let { presets, preset_names } = this.getPresetList();
@@ -687,7 +745,10 @@ class PresetManager {
687 return preset;745 return preset;
688 }746 }
689747
690 // pass no arguments to delete current preset748 /**
749 * Deletes a preset by name. If not provided, deletes the currently selected preset.
750 * @param {string} [name] Name of the preset to delete.
751 */
691 async deletePreset(name) {752 async deletePreset(name) {
692 const { preset_names, presets } = this.getPresetList();753 const { preset_names, presets } = this.getPresetList();
693 const value = name ? (this.isKeyedApi() ? this.findPreset(name) : name) : this.getSelectedPreset();754 const value = name ? (this.isKeyedApi() ? this.findPreset(name) : name) : this.getSelectedPreset();
@@ -728,6 +789,11 @@ class PresetManager {
728 return response.ok;789 return response.ok;
729 }790 }
730791
792 /**
793 * Retrieves the default preset for the API from the server.
794 * @param {string} name Name of the preset to restore
795 * @returns {Promise<any>} Default preset object, or undefined if the request fails
796 */
731 async getDefaultPreset(name) {797 async getDefaultPreset(name) {
732 const response = await fetch('/api/presets/restore', {798 const response = await fetch('/api/presets/restore', {
733 method: 'POST',799 method: 'POST',
@@ -743,6 +809,70 @@ class PresetManager {
743809
744 return await response.json();810 return await response.json();
745 }811 }
812
813 /**
814 * Reads a preset extension field from the preset.
815 * @param {object} options
816 * @param {string} [options.name] Name of the preset. If not provided, uses the currently selected preset name.
817 * @param {string} options.path Path to the preset extension field, e.g. 'myextension.data'.
818 * @return {any} The value of the preset extension field, or null if not found.
819 */
820 readPresetExtensionField({ name, path }) {
821 const { settings } = this.getPresetList();
822 const selectedName = this.getSelectedPresetName();
823 const presetName = name || selectedName;
824
825 // Read from settings if the selected preset is the same as the provided name
826 if (settings && selectedName === presetName) {
827 const settingsExtensions = ensurePlainObject(settings.extensions || {});
828 return lodash.get(settingsExtensions, path, null);
829 }
830
831 // Otherwise, read from the preset by name
832 const preset = this.getCompletionPresetByName(presetName);
833 if (!preset) {
834 return null;
835 }
836
837 const presetExtensions = ensurePlainObject(preset.extensions || {});
838 const value = lodash.get(presetExtensions, path, null);
839 return value;
840 }
841
842 /**
843 * Writes a value to a preset extension field.
844 * @param {object} options
845 * @param {string} [options.name] Name of the preset. If not provided, uses the currently selected preset name.
846 * @param {string} options.path Path to the preset extension field, e.g. 'myextension.data'.
847 * @param {any} options.value Value to write to the preset extension field.
848 * @return {Promise<void>} Resolves when the preset is saved.
849 */
850 async writePresetExtensionField({ name, path, value }) {
851 const { settings } = this.getPresetList();
852 const selectedName = this.getSelectedPresetName();
853 const presetName = name || selectedName;
854
855 // Write to settings if the selected preset is the same as the provided name
856 if (settings && selectedName === presetName) {
857 // Set the value at the specified path
858 settings.extensions = ensurePlainObject(settings.extensions || {});
859 lodash.set(settings.extensions, path, value);
860 await saveSettings();
861 }
862
863 // Also update the preset by name
864 const preset = this.getCompletionPresetByName(presetName);
865 if (!preset) {
866 return;
867 }
868
869 // Set the value at the specified path
870 preset.extensions = ensurePlainObject(preset.extensions || {});
871 lodash.set(preset.extensions, path, value);
872
873 // Save the updated preset
874 await this.savePreset(presetName, preset, { skipUpdate: true });
875 }
746}876}
747877
748/**878/**
public/scripts/textgen-settings.js+8 -0
@@ -224,6 +224,7 @@ const settings = {
224 min_keep: 0,224 min_keep: 0,
225 featherless_model: '',225 featherless_model: '',
226 generic_model: '',226 generic_model: '',
227 extensions: {},
227};228};
228229
229export {230export {
@@ -306,6 +307,7 @@ export const setting_names = [
306 'nsigma',307 'nsigma',
307 'min_keep',308 'min_keep',
308 'generic_model',309 'generic_model',
310 'extensions',
309];311];
310312
311const DYNATEMP_BLOCK = document.getElementById('dynatemp_block_ooba');313const DYNATEMP_BLOCK = document.getElementById('dynatemp_block_ooba');
@@ -1098,6 +1100,12 @@ function insertMissingArrayItems(source, target) {
1098}1100}
10991101
1100function setSettingByName(setting, value, trigger) {1102function setSettingByName(setting, value, trigger) {
1103 if ('extensions' === setting) {
1104 value = value || {};
1105 settings.extensions = value;
1106 return;
1107 }
1108
1101 if (value === null || value === undefined) {1109 if (value === null || value === undefined) {
1102 return;1110 return;
1103 }1111 }
public/scripts/utils.js+13 -0
@@ -102,6 +102,19 @@ export function deepMerge(target, source) {
102 return output;102 return output;
103}103}
104104
105/**
106 * Ensures that the provided object is a plain object.
107 * @param {object} obj Object to ensure is a plain object
108 * @return {object} A plain object, or an empty object if the input is not an object.
109 */
110export function ensurePlainObject(obj) {
111 if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
112 return {};
113 }
114
115 return obj;
116}
117
105export function escapeHtml(str) {118export function escapeHtml(str) {
106 return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');119 return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
107}120}