| 1 | import { Fuse, lodash } from '../lib.js'; |
| 2 | |
| 3 | import { |
| 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'; |
| 21 | import { groups, selected_group } from './group-chats.js'; |
| 22 | import { t } from './i18n.js'; |
| 23 | import { instruct_presets } from './instruct-mode.js'; |
| 24 | import { kai_settings } from './kai-settings.js'; |
| 25 | import { convertNovelPreset } from './nai-settings.js'; |
| 26 | import { oai_settings, openai_setting_names, openai_settings } from './openai.js'; |
| 27 | import { POPUP_RESULT, POPUP_TYPE, Popup } from './popup.js'; |
| 28 | import { context_presets, getContextSettings, power_user } from './power-user.js'; |
| 29 | import { reasoning_templates } from './reasoning.js'; |
| 30 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| 31 | import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js'; |
| 32 | import { enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 33 | import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js'; |
| 34 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 35 | import { checkForSystemPromptInInstructTemplate, system_prompts } from './sysprompt.js'; |
| 36 | import { renderTemplateAsync } from './templates.js'; |
| 37 | import { |
| 38 | textgenerationwebui_settings as textgen_settings, |
| 39 | textgenerationwebui_preset_names, |
| 40 | textgenerationwebui_presets, |
| 41 | } from './textgen-settings.js'; |
| 42 | import { download, ensurePlainObject, equalsIgnoreCaseAndAccents, getSanitizedFilename, parseJsonFile, waitUntilCondition } from './utils.js'; |
| 43 | |
| 44 | const presetManagers = {}; |
| 45 | |
| 46 | /** |
| 47 | * Automatically select a preset for current API based on character or group name. |
| 48 | */ |
| 49 | function 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 | */ |
| 83 | export 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 | */ |
| 101 | function 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 | |
| 111 | class 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 | */ |
| 910 | async 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 | */ |
| 976 | async 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 | |
| 984 | export 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 | } |