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 = {
4444 seed: -1,
4545 api_server: '',
4646 preset_settings: 'gui',
47+ extensions: {},
4748};
4849
4950/**
@@ -130,6 +131,11 @@ export function loadKoboldSettings(data, preset, settings) {
130131
131132function loadKoboldSettingsFromPreset(preset) {
132133 for (const name of Object.keys(kai_settings)) {
134+ if (name === 'extensions') {
135+ kai_settings.extensions = preset.extensions || {};
136+ continue;
137+ }
138+
133139 const value = preset[name] ?? defaultValues[name];
134140 const slider = sliders.find(x => x.name === name);
135141
public/scripts/nai-settings.js+4 -0
@@ -56,6 +56,7 @@ export const nai_settings = {
5656 banned_tokens: '',
5757 order: default_order,
5858 logit_bias: [],
59+ extensions: {},
5960};
6061
6162const nai_tiers = {
@@ -140,6 +141,7 @@ export function convertNovelPreset(data) {
140141 math1_quad_entropy_scale: data.parameters.math1_quad_entropy_scale,
141142 min_p: data.parameters.min_p,
142143 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: {},
143145 };
144146}
145147
@@ -205,6 +207,7 @@ export function loadNovelPreset(preset) {
205207 nai_settings.math1_temp = preset.math1_temp || 1;
206208 nai_settings.math1_quad = preset.math1_quad || 0;
207209 nai_settings.math1_quad_entropy_scale = preset.math1_quad_entropy_scale || 0;
210+ nai_settings.extensions = preset.extensions || {};
208211 loadNovelSettingsUi(nai_settings);
209212}
210213
@@ -258,6 +261,7 @@ export function loadNovelSettings(data, settings) {
258261 nai_settings.math1_temp = settings.math1_temp || 1;
259262 nai_settings.math1_quad = settings.math1_quad || 0;
260263 nai_settings.math1_quad_entropy_scale = settings.math1_quad_entropy_scale || 0;
264+ nai_settings.extensions = settings.extensions || {};
261265 loadNovelSettingsUi(nai_settings);
262266}
263267
public/scripts/openai.js+11 -0
@@ -332,6 +332,7 @@ export const settingsToUpdate = {
332332 n: ['#n_openai', 'n', false, false],
333333 bypass_status_check: ['#openai_bypass_status_check', 'bypass_status_check', true, true],
334334 request_images: ['#openai_request_images', 'request_images', true, false],
335+ extensions: ['#NULL_SELECTOR', 'extensions', false, false],
335336};
336337
337338const default_settings = {
@@ -421,6 +422,7 @@ const default_settings = {
421422 seed: -1,
422423 n: 1,
423424 bind_preset_to_connection: true,
425+ extensions: {},
424426};
425427
426428const oai_settings = {
@@ -510,6 +512,7 @@ const oai_settings = {
510512 seed: -1,
511513 n: 1,
512514 bind_preset_to_connection: true,
515+ extensions: {},
513516};
514517
515518export let proxies = [
@@ -3600,6 +3603,7 @@ function loadOpenAISettings(data, settings) {
36003603 oai_settings.function_calling = settings.function_calling ?? default_settings.function_calling;
36013604 oai_settings.openrouter_providers = settings.openrouter_providers ?? default_settings.openrouter_providers;
36023605 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
36043608 // Migrate from old settings
36053609 if (settings.names_in_completion === true) {
@@ -4024,6 +4028,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
40244028 request_images: settings.request_images,
40254029 seed: settings.seed,
40264030 n: settings.n,
4031+ extensions: settings.extensions,
40274032 };
40284033
40294034 const savePresetSettings = await fetch('/api/presets/save', {
@@ -4468,6 +4473,12 @@ function onSettingsPresetChange() {
44684473 continue;
44694474 }
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+
44714482 if (preset[key] !== undefined) {
44724483 if (isCheckbox) {
44734484 updateCheckbox(selector, preset[key]);
public/scripts/preset-manager.js+136 -6
@@ -1,4 +1,4 @@
11import { Fuse, lodash } from '../lib.js';
22
33import {
44 amount_gen,
@@ -14,6 +14,7 @@ import {
1414 novelai_setting_names,
1515 novelai_settings,
1616 online_status,
17+ saveSettings,
1718 saveSettingsDebounced,
1819 this_chid,
1920} from '../script.js';
@@ -36,7 +37,7 @@ import {
3637 textgenerationwebui_presets,
3738 textgenerationwebui_settings as textgen_settings,
3839} from './textgen-settings.js';
3940import { download, ensurePlainObject, equalsIgnoreCaseAndAccents, getSanitizedFilename, parseJsonFile, waitUntilCondition } from './utils.js';
4041import { t } from './i18n.js';
4142import { reasoning_templates } from './reasoning.js';
4243
@@ -391,6 +392,9 @@ class PresetManager {
391392 $(this.select).val(value).trigger('change');
392393 }
393394
395+ /**
396+ * Updates the preset select element with the current API presets.
397+ */
394398 async updatePreset() {
395399 const selected = $(this.select).find('option:selected');
396400 console.log(selected);
@@ -407,6 +411,9 @@ class PresetManager {
407411 toastr.success(successToast);
408412 }
409413
414+ /**
415+ * Saves the currently selected preset with a new name.
416+ */
410417 async savePresetAs() {
411418 const inputValue = this.getSelectedPresetName();
412419 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 {
423430 toastr.success(successToast);
424431 }
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 } = {}) {
427441 if (this.apiId === 'instruct' && settings) {
428442 await checkForSystemPromptInInstructTemplate(name, settings);
429443 }
@@ -449,9 +463,18 @@ class PresetManager {
449463 const data = await response.json();
450464 name = data.name;
451465
466+ if (skipUpdate) {
467+ console.debug(`Preset ${name} saved, but not updating the list`);
468+ return;
469+ }
470+
452471 this.updateList(name, preset);
453472 }
454473
474+ /**
475+ * Renames the currently selected preset.
476+ * @param {string} newName New name for the preset
477+ */
455478 async renamePreset(newName) {
456479 const oldName = this.getSelectedPresetName();
457480 if (equalsIgnoreCaseAndAccents(oldName, newName)) {
@@ -468,9 +491,15 @@ class PresetManager {
468491
469492 }
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+ */
471499 getPresetList(api) {
472500 let presets = [];
473501 let preset_names = {};
502+ let settings = {};
474503
475504 // If no API specified, use the current API
476505 if (api === undefined) {
@@ -482,50 +511,69 @@ class PresetManager {
482511 case 'kobold':
483512 presets = koboldai_settings;
484513 preset_names = koboldai_setting_names;
514+ settings = kai_settings;
485515 break;
486516 case 'novel':
487517 presets = novelai_settings;
488518 preset_names = novelai_setting_names;
519+ settings = nai_settings;
489520 break;
490521 case 'textgenerationwebui':
491522 presets = textgenerationwebui_presets;
492523 preset_names = textgenerationwebui_preset_names;
524+ settings = textgen_settings;
493525 break;
494526 case 'openai':
495527 presets = openai_settings;
496528 preset_names = openai_setting_names;
529+ settings = openai_settings;
497530 break;
498531 case 'context':
499532 presets = context_presets;
500533 preset_names = context_presets.map(x => x.name);
534+ settings = power_user.context;
501535 break;
502536 case 'instruct':
503537 presets = instruct_presets;
504538 preset_names = instruct_presets.map(x => x.name);
539+ settings = power_user.instruct;
505540 break;
506541 case 'sysprompt':
507542 presets = system_prompts;
508543 preset_names = system_prompts.map(x => x.name);
544+ settings = power_user.sysprompt;
509545 break;
510546 case 'reasoning':
511547 presets = reasoning_templates;
512548 preset_names = reasoning_templates.map(x => x.name);
549+ settings = power_user.reasoning;
513550 break;
514551 default:
515552 console.warn(`Unknown API ID ${api}`);
516553 }
517554
518555 return { presets, preset_names, settings };
519556 }
520557
558+ /**
559+ * Returns true if the API is keyed, meaning it uses a name to identify presets.
560+ */
521561 isKeyedApi() {
522562 return this.apiId == 'textgenerationwebui' || this.isAdvancedFormatting();
523563 }
524564
565+ /**
566+ * Returns true if the API is from Advanced Formatting group.
567+ */
525568 isAdvancedFormatting() {
526569 return ['context', 'instruct', 'sysprompt', 'reasoning'].includes(this.apiId);
527570 }
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+ */
529577 updateList(name, preset) {
530578 const { presets, preset_names } = this.getPresetList();
531579 const presetExists = this.isKeyedApi() ? preset_names.includes(name) : Object.keys(preset_names).includes(name);
@@ -561,6 +609,11 @@ class PresetManager {
561609 }
562610 }
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+ */
564617 getPresetSettings(name) {
565618 function getSettingsByApiId(apiId) {
566619 switch (apiId) {
@@ -655,7 +708,7 @@ class PresetManager {
655708 }
656709 }
657710
658711 if (!this.isAdvancedFormatting() && this.apiId !== 'openai') {
659712 settings['genamt'] = amount_gen;
660713 settings['max_length'] = max_context;
661714 }
@@ -663,6 +716,11 @@ class PresetManager {
663716 return settings;
664717 }
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+ */
666724 getCompletionPresetByName(name) {
667725 // Retrieve a completion preset by name. Return undefined if not found.
668726 let { presets, preset_names } = this.getPresetList();
@@ -687,7 +745,10 @@ class PresetManager {
687745 return preset;
688746 }
689747
690- // pass no arguments to delete current preset
748+ /**
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+ */
691752 async deletePreset(name) {
692753 const { preset_names, presets } = this.getPresetList();
693754 const value = name ? (this.isKeyedApi() ? this.findPreset(name) : name) : this.getSelectedPreset();
@@ -728,6 +789,11 @@ class PresetManager {
728789 return response.ok;
729790 }
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+ */
731797 async getDefaultPreset(name) {
732798 const response = await fetch('/api/presets/restore', {
733799 method: 'POST',
@@ -743,6 +809,70 @@ class PresetManager {
743809
744810 return await response.json();
745811 }
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+ }
746876}
747877
748878/**
public/scripts/textgen-settings.js+8 -0
@@ -224,6 +224,7 @@ const settings = {
224224 min_keep: 0,
225225 featherless_model: '',
226226 generic_model: '',
227+ extensions: {},
227228};
228229
229230export {
@@ -306,6 +307,7 @@ export const setting_names = [
306307 'nsigma',
307308 'min_keep',
308309 'generic_model',
310+ 'extensions',
309311];
310312
311313const DYNATEMP_BLOCK = document.getElementById('dynatemp_block_ooba');
@@ -1098,6 +1100,12 @@ function insertMissingArrayItems(source, target) {
10981100}
10991101
11001102function setSettingByName(setting, value, trigger) {
1103+ if ('extensions' === setting) {
1104+ value = value || {};
1105+ settings.extensions = value;
1106+ return;
1107+ }
1108+
11011109 if (value === null || value === undefined) {
11021110 return;
11031111 }
public/scripts/utils.js+13 -0
@@ -102,6 +102,19 @@ export function deepMerge(target, source) {
102102 return output;
103103}
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+ */
110+export function ensurePlainObject(obj) {
111+ if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
112+ return {};
113+ }
114+
115+ return obj;
116+}
117+
105118export function escapeHtml(str) {
106119 return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
107120}