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
Signed| @@ -44,6 +44,7 @@ export const kai_settings = { | ||
| 44 | 44 | seed: -1, |
| 45 | 45 | api_server: '', |
| 46 | 46 | preset_settings: 'gui', |
| 47 | + extensions: {}, | |
| 47 | 48 | }; |
| 48 | 49 | |
| 49 | 50 | /** |
| @@ -130,6 +131,11 @@ export function loadKoboldSettings(data, preset, settings) { | ||
| 130 | 131 | |
| 131 | 132 | function loadKoboldSettingsFromPreset(preset) { |
| 132 | 133 | for (const name of Object.keys(kai_settings)) { |
| 134 | + if (name === 'extensions') { | |
| 135 | + kai_settings.extensions = preset.extensions || {}; | |
| 136 | + continue; | |
| 137 | + } | |
| 138 | + | |
| 133 | 139 | const value = preset[name] ?? defaultValues[name]; |
| 134 | 140 | const slider = sliders.find(x => x.name === name); |
| 135 | 141 | |
| @@ -56,6 +56,7 @@ export const nai_settings = { | ||
| 56 | 56 | banned_tokens: '', |
| 57 | 57 | order: default_order, |
| 58 | 58 | logit_bias: [], |
| 59 | + extensions: {}, | |
| 59 | 60 | }; |
| 60 | 61 | |
| 61 | 62 | const nai_tiers = { |
| @@ -140,6 +141,7 @@ export function convertNovelPreset(data) { | ||
| 140 | 141 | math1_quad_entropy_scale: data.parameters.math1_quad_entropy_scale, |
| 141 | 142 | min_p: data.parameters.min_p, |
| 142 | 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 | } |
| 145 | 147 | |
| @@ -205,6 +207,7 @@ export function loadNovelPreset(preset) { | ||
| 205 | 207 | nai_settings.math1_temp = preset.math1_temp || 1; |
| 206 | 208 | nai_settings.math1_quad = preset.math1_quad || 0; |
| 207 | 209 | nai_settings.math1_quad_entropy_scale = preset.math1_quad_entropy_scale || 0; |
| 210 | + nai_settings.extensions = preset.extensions || {}; | |
| 208 | 211 | loadNovelSettingsUi(nai_settings); |
| 209 | 212 | } |
| 210 | 213 | |
| @@ -258,6 +261,7 @@ export function loadNovelSettings(data, settings) { | ||
| 258 | 261 | nai_settings.math1_temp = settings.math1_temp || 1; |
| 259 | 262 | nai_settings.math1_quad = settings.math1_quad || 0; |
| 260 | 263 | nai_settings.math1_quad_entropy_scale = settings.math1_quad_entropy_scale || 0; |
| 264 | + nai_settings.extensions = settings.extensions || {}; | |
| 261 | 265 | loadNovelSettingsUi(nai_settings); |
| 262 | 266 | } |
| 263 | 267 | |
| @@ -332,6 +332,7 @@ export const settingsToUpdate = { | ||
| 332 | 332 | n: ['#n_openai', 'n', false, false], |
| 333 | 333 | bypass_status_check: ['#openai_bypass_status_check', 'bypass_status_check', true, true], |
| 334 | 334 | request_images: ['#openai_request_images', 'request_images', true, false], |
| 335 | + extensions: ['#NULL_SELECTOR', 'extensions', false, false], | |
| 335 | 336 | }; |
| 336 | 337 | |
| 337 | 338 | const default_settings = { |
| @@ -421,6 +422,7 @@ const default_settings = { | ||
| 421 | 422 | seed: -1, |
| 422 | 423 | n: 1, |
| 423 | 424 | bind_preset_to_connection: true, |
| 425 | + extensions: {}, | |
| 424 | 426 | }; |
| 425 | 427 | |
| 426 | 428 | const oai_settings = { |
| @@ -510,6 +512,7 @@ const oai_settings = { | ||
| 510 | 512 | seed: -1, |
| 511 | 513 | n: 1, |
| 512 | 514 | bind_preset_to_connection: true, |
| 515 | + extensions: {}, | |
| 513 | 516 | }; |
| 514 | 517 | |
| 515 | 518 | export let proxies = [ |
| @@ -3600,6 +3603,7 @@ function loadOpenAISettings(data, settings) { | ||
| 3600 | 3603 | oai_settings.function_calling = settings.function_calling ?? default_settings.function_calling; |
| 3601 | 3604 | oai_settings.openrouter_providers = settings.openrouter_providers ?? default_settings.openrouter_providers; |
| 3602 | 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; | |
| 3603 | 3607 | |
| 3604 | 3608 | // Migrate from old settings |
| 3605 | 3609 | if (settings.names_in_completion === true) { |
| @@ -4024,6 +4028,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) { | ||
| 4024 | 4028 | request_images: settings.request_images, |
| 4025 | 4029 | seed: settings.seed, |
| 4026 | 4030 | n: settings.n, |
| 4031 | + extensions: settings.extensions, | |
| 4027 | 4032 | }; |
| 4028 | 4033 | |
| 4029 | 4034 | const savePresetSettings = await fetch('/api/presets/save', { |
| @@ -4468,6 +4473,12 @@ function onSettingsPresetChange() { | ||
| 4468 | 4473 | continue; |
| 4469 | 4474 | } |
| 4470 | 4475 | |
| 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 | 4482 | if (preset[key] !== undefined) { |
| 4472 | 4483 | if (isCheckbox) { |
| 4473 | 4484 | updateCheckbox(selector, preset[key]); |
| @@ -1,4 +1,4 @@ | ||
| 1 | 1 | import { Fuse, lodash } from '../lib.js'; |
| 2 | 2 | |
| 3 | 3 | import { |
| 4 | 4 | amount_gen, |
| @@ -14,6 +14,7 @@ import { | ||
| 14 | 14 | novelai_setting_names, |
| 15 | 15 | novelai_settings, |
| 16 | 16 | online_status, |
| 17 | + saveSettings, | |
| 17 | 18 | saveSettingsDebounced, |
| 18 | 19 | this_chid, |
| 19 | 20 | } from '../script.js'; |
| @@ -36,7 +37,7 @@ import { | ||
| 36 | 37 | textgenerationwebui_presets, |
| 37 | 38 | textgenerationwebui_settings as textgen_settings, |
| 38 | 39 | } from './textgen-settings.js'; |
| 39 | 40 | import { download, ensurePlainObject, equalsIgnoreCaseAndAccents, getSanitizedFilename, parseJsonFile, waitUntilCondition } from './utils.js'; |
| 40 | 41 | import { t } from './i18n.js'; |
| 41 | 42 | import { reasoning_templates } from './reasoning.js'; |
| 42 | 43 | |
| @@ -391,6 +392,9 @@ class PresetManager { | ||
| 391 | 392 | $(this.select).val(value).trigger('change'); |
| 392 | 393 | } |
| 393 | 394 | |
| 395 | + /** | |
| 396 | + * Updates the preset select element with the current API presets. | |
| 397 | + */ | |
| 394 | 398 | async updatePreset() { |
| 395 | 399 | const selected = $(this.select).find('option:selected'); |
| 396 | 400 | console.log(selected); |
| @@ -407,6 +411,9 @@ class PresetManager { | ||
| 407 | 411 | toastr.success(successToast); |
| 408 | 412 | } |
| 409 | 413 | |
| 414 | + /** | |
| 415 | + * Saves the currently selected preset with a new name. | |
| 416 | + */ | |
| 410 | 417 | async savePresetAs() { |
| 411 | 418 | const inputValue = this.getSelectedPresetName(); |
| 412 | 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 | 430 | toastr.success(successToast); |
| 424 | 431 | } |
| 425 | 432 | |
| 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 | 441 | if (this.apiId === 'instruct' && settings) { |
| 428 | 442 | await checkForSystemPromptInInstructTemplate(name, settings); |
| 429 | 443 | } |
| @@ -449,9 +463,18 @@ class PresetManager { | ||
| 449 | 463 | const data = await response.json(); |
| 450 | 464 | name = data.name; |
| 451 | 465 | |
| 466 | + if (skipUpdate) { | |
| 467 | + console.debug(`Preset ${name} saved, but not updating the list`); | |
| 468 | + return; | |
| 469 | + } | |
| 470 | + | |
| 452 | 471 | this.updateList(name, preset); |
| 453 | 472 | } |
| 454 | 473 | |
| 474 | + /** | |
| 475 | + * Renames the currently selected preset. | |
| 476 | + * @param {string} newName New name for the preset | |
| 477 | + */ | |
| 455 | 478 | async renamePreset(newName) { |
| 456 | 479 | const oldName = this.getSelectedPresetName(); |
| 457 | 480 | if (equalsIgnoreCaseAndAccents(oldName, newName)) { |
| @@ -468,9 +491,15 @@ class PresetManager { | ||
| 468 | 491 | |
| 469 | 492 | } |
| 470 | 493 | |
| 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 | 499 | getPresetList(api) { |
| 472 | 500 | let presets = []; |
| 473 | 501 | let preset_names = {}; |
| 502 | + let settings = {}; | |
| 474 | 503 | |
| 475 | 504 | // If no API specified, use the current API |
| 476 | 505 | if (api === undefined) { |
| @@ -482,50 +511,69 @@ class PresetManager { | ||
| 482 | 511 | case 'kobold': |
| 483 | 512 | presets = koboldai_settings; |
| 484 | 513 | preset_names = koboldai_setting_names; |
| 514 | + settings = kai_settings; | |
| 485 | 515 | break; |
| 486 | 516 | case 'novel': |
| 487 | 517 | presets = novelai_settings; |
| 488 | 518 | preset_names = novelai_setting_names; |
| 519 | + settings = nai_settings; | |
| 489 | 520 | break; |
| 490 | 521 | case 'textgenerationwebui': |
| 491 | 522 | presets = textgenerationwebui_presets; |
| 492 | 523 | preset_names = textgenerationwebui_preset_names; |
| 524 | + settings = textgen_settings; | |
| 493 | 525 | break; |
| 494 | 526 | case 'openai': |
| 495 | 527 | presets = openai_settings; |
| 496 | 528 | preset_names = openai_setting_names; |
| 529 | + settings = openai_settings; | |
| 497 | 530 | break; |
| 498 | 531 | case 'context': |
| 499 | 532 | presets = context_presets; |
| 500 | 533 | preset_names = context_presets.map(x => x.name); |
| 534 | + settings = power_user.context; | |
| 501 | 535 | break; |
| 502 | 536 | case 'instruct': |
| 503 | 537 | presets = instruct_presets; |
| 504 | 538 | preset_names = instruct_presets.map(x => x.name); |
| 539 | + settings = power_user.instruct; | |
| 505 | 540 | break; |
| 506 | 541 | case 'sysprompt': |
| 507 | 542 | presets = system_prompts; |
| 508 | 543 | preset_names = system_prompts.map(x => x.name); |
| 544 | + settings = power_user.sysprompt; | |
| 509 | 545 | break; |
| 510 | 546 | case 'reasoning': |
| 511 | 547 | presets = reasoning_templates; |
| 512 | 548 | preset_names = reasoning_templates.map(x => x.name); |
| 549 | + settings = power_user.reasoning; | |
| 513 | 550 | break; |
| 514 | 551 | default: |
| 515 | 552 | console.warn(`Unknown API ID ${api}`); |
| 516 | 553 | } |
| 517 | 554 | |
| 518 | 555 | return { presets, preset_names, settings }; |
| 519 | 556 | } |
| 520 | 557 | |
| 558 | + /** | |
| 559 | + * Returns true if the API is keyed, meaning it uses a name to identify presets. | |
| 560 | + */ | |
| 521 | 561 | isKeyedApi() { |
| 522 | 562 | return this.apiId == 'textgenerationwebui' || this.isAdvancedFormatting(); |
| 523 | 563 | } |
| 524 | 564 | |
| 565 | + /** | |
| 566 | + * Returns true if the API is from Advanced Formatting group. | |
| 567 | + */ | |
| 525 | 568 | isAdvancedFormatting() { |
| 526 | 569 | return ['context', 'instruct', 'sysprompt', 'reasoning'].includes(this.apiId); |
| 527 | 570 | } |
| 528 | 571 | |
| 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 | 577 | updateList(name, preset) { |
| 530 | 578 | const { presets, preset_names } = this.getPresetList(); |
| 531 | 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 | } |
| 563 | 611 | |
| 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 | 617 | getPresetSettings(name) { |
| 565 | 618 | function getSettingsByApiId(apiId) { |
| 566 | 619 | switch (apiId) { |
| @@ -655,7 +708,7 @@ class PresetManager { | ||
| 655 | 708 | } |
| 656 | 709 | } |
| 657 | 710 | |
| 658 | 711 | if (!this.isAdvancedFormatting() && this.apiId !== 'openai') { |
| 659 | 712 | settings['genamt'] = amount_gen; |
| 660 | 713 | settings['max_length'] = max_context; |
| 661 | 714 | } |
| @@ -663,6 +716,11 @@ class PresetManager { | ||
| 663 | 716 | return settings; |
| 664 | 717 | } |
| 665 | 718 | |
| 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 | 724 | getCompletionPresetByName(name) { |
| 667 | 725 | // Retrieve a completion preset by name. Return undefined if not found. |
| 668 | 726 | let { presets, preset_names } = this.getPresetList(); |
| @@ -687,7 +745,10 @@ class PresetManager { | ||
| 687 | 745 | return preset; |
| 688 | 746 | } |
| 689 | 747 | |
| 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 | + */ | |
| 691 | 752 | async deletePreset(name) { |
| 692 | 753 | const { preset_names, presets } = this.getPresetList(); |
| 693 | 754 | const value = name ? (this.isKeyedApi() ? this.findPreset(name) : name) : this.getSelectedPreset(); |
| @@ -728,6 +789,11 @@ class PresetManager { | ||
| 728 | 789 | return response.ok; |
| 729 | 790 | } |
| 730 | 791 | |
| 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 | 797 | async getDefaultPreset(name) { |
| 732 | 798 | const response = await fetch('/api/presets/restore', { |
| 733 | 799 | method: 'POST', |
| @@ -743,6 +809,70 @@ class PresetManager { | ||
| 743 | 809 | |
| 744 | 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 | } |
| 747 | 877 | |
| 748 | 878 | /** |
| @@ -224,6 +224,7 @@ const settings = { | ||
| 224 | 224 | min_keep: 0, |
| 225 | 225 | featherless_model: '', |
| 226 | 226 | generic_model: '', |
| 227 | + extensions: {}, | |
| 227 | 228 | }; |
| 228 | 229 | |
| 229 | 230 | export { |
| @@ -306,6 +307,7 @@ export const setting_names = [ | ||
| 306 | 307 | 'nsigma', |
| 307 | 308 | 'min_keep', |
| 308 | 309 | 'generic_model', |
| 310 | + 'extensions', | |
| 309 | 311 | ]; |
| 310 | 312 | |
| 311 | 313 | const DYNATEMP_BLOCK = document.getElementById('dynatemp_block_ooba'); |
| @@ -1098,6 +1100,12 @@ function insertMissingArrayItems(source, target) { | ||
| 1098 | 1100 | } |
| 1099 | 1101 | |
| 1100 | 1102 | function setSettingByName(setting, value, trigger) { |
| 1103 | + if ('extensions' === setting) { | |
| 1104 | + value = value || {}; | |
| 1105 | + settings.extensions = value; | |
| 1106 | + return; | |
| 1107 | + } | |
| 1108 | + | |
| 1101 | 1109 | if (value === null || value === undefined) { |
| 1102 | 1110 | return; |
| 1103 | 1111 | } |
| @@ -102,6 +102,19 @@ export function deepMerge(target, source) { | ||
| 102 | 102 | return output; |
| 103 | 103 | } |
| 104 | 104 | |
| 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 | + | |
| 105 | 118 | export function escapeHtml(str) { |
| 106 | 119 | return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); |
| 107 | 120 | } |