import { amount_gen, characters, eventSource, event_types, getRequestHeaders, koboldai_setting_names, koboldai_settings, main_api, max_context, nai_settings, novelai_setting_names, novelai_settings, online_status, saveSettingsDebounced, this_chid, } from '../script.js'; import { groups, selected_group } from './group-chats.js'; import { instruct_presets } from './instruct-mode.js'; import { kai_settings } from './kai-settings.js'; import { convertNovelPreset } from './nai-settings.js'; import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js'; import { context_presets, getContextSettings, power_user } from './power-user.js'; import { SlashCommand } from './slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js'; import { enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js'; import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { checkForSystemPromptInInstructTemplate, system_prompts } from './sysprompt.js'; import { renderTemplateAsync } from './templates.js'; import { textgenerationwebui_preset_names, textgenerationwebui_presets, textgenerationwebui_settings as textgen_settings, } from './textgen-settings.js'; import { download, parseJsonFile, waitUntilCondition } from './utils.js'; import { t } from './i18n.js'; const presetManagers = {}; /** * Automatically select a preset for current API based on character or group name. */ function autoSelectPreset() { const presetManager = getPresetManager(); if (!presetManager) { console.debug(`Preset Manager not found for API: ${main_api}`); return; } const name = selected_group ? groups.find(x => x.id == selected_group)?.name : characters[this_chid]?.name; if (!name) { console.debug(`Preset candidate not found for API: ${main_api}`); return; } const preset = presetManager.findPreset(name); const selectedPreset = presetManager.getSelectedPreset(); if (preset === selectedPreset) { console.debug(`Preset already selected for API: ${main_api}, name: ${name}`); return; } if (preset !== undefined && preset !== null) { console.log(`Preset found for API: ${main_api}, name: ${name}`); presetManager.selectPreset(preset); } } /** * Gets a preset manager by API id. * @param {string} apiId API id * @returns {PresetManager} Preset manager */ export function getPresetManager(apiId = '') { if (!apiId) { apiId = main_api == 'koboldhorde' ? 'kobold' : main_api; } if (!Object.keys(presetManagers).includes(apiId)) { return null; } return presetManagers[apiId]; } /** * Registers preset managers for all select elements with data-preset-manager-for attribute. */ function registerPresetManagers() { $('select[data-preset-manager-for]').each((_, e) => { const forData = $(e).data('preset-manager-for'); for (const apiId of forData.split(',')) { console.debug(`Registering preset manager for API: ${apiId}`); presetManagers[apiId] = new PresetManager($(e), apiId); } }); } class PresetManager { constructor(select, apiId) { this.select = select; this.apiId = apiId; } static masterSections = { 'instruct': { name: 'Instruct Template', getData: () => { const manager = getPresetManager('instruct'); const name = manager.getSelectedPresetName(); return manager.getPresetSettings(name); }, setData: (data) => { const manager = getPresetManager('instruct'); const name = data.name; return manager.savePreset(name, data); }, isValid: (data) => PresetManager.isPossiblyInstructData(data), }, 'context': { name: 'Context Template', getData: () => { const manager = getPresetManager('context'); const name = manager.getSelectedPresetName(); return manager.getPresetSettings(name); }, setData: (data) => { const manager = getPresetManager('context'); const name = data.name; return manager.savePreset(name, data); }, isValid: (data) => PresetManager.isPossiblyContextData(data), }, 'sysprompt': { name: 'System Prompt', getData: () => { const manager = getPresetManager('sysprompt'); const name = manager.getSelectedPresetName(); return manager.getPresetSettings(name); }, setData: (data) => { const manager = getPresetManager('sysprompt'); const name = data.name; return manager.savePreset(name, data); }, isValid: (data) => PresetManager.isPossiblySystemPromptData(data), }, 'preset': { name: 'Text Completion Preset', getData: () => { const manager = getPresetManager('textgenerationwebui'); const name = manager.getSelectedPresetName(); const data = manager.getPresetSettings(name); data['name'] = name; return data; }, setData: (data) => { const manager = getPresetManager('textgenerationwebui'); const name = data.name; return manager.savePreset(name, data); }, isValid: (data) => PresetManager.isPossiblyTextCompletionData(data), }, }; static isPossiblyInstructData(data) { const instructProps = ['name', 'input_sequence', 'output_sequence']; return data && instructProps.every(prop => Object.keys(data).includes(prop)); } static isPossiblyContextData(data) { const contextProps = ['name', 'story_string']; return data && contextProps.every(prop => Object.keys(data).includes(prop)); } static isPossiblySystemPromptData(data) { const sysPromptProps = ['name', 'content']; return data && sysPromptProps.every(prop => Object.keys(data).includes(prop)); } static isPossiblyTextCompletionData(data) { const textCompletionProps = ['temp', 'top_k', 'top_p', 'rep_pen']; return data && textCompletionProps.every(prop => Object.keys(data).includes(prop)); } /** * Imports master settings from JSON data. * @param {object} data Data to import * @param {string} fileName File name * @returns {Promise} */ static async performMasterImport(data, fileName) { if (!data || typeof data !== 'object') { toastr.error(t`Invalid data provided for master import`); return; } // Check for legacy file imports // 1. Instruct Template if (this.isPossiblyInstructData(data)) { toastr.info(t`Importing instruct template...`, t`Instruct template detected`); return await getPresetManager('instruct').savePreset(data.name, data); } // 2. Context Template if (this.isPossiblyContextData(data)) { toastr.info(t`Importing as context template...`, t`Context template detected`); return await getPresetManager('context').savePreset(data.name, data); } // 3. System Prompt if (this.isPossiblySystemPromptData(data)) { toastr.info(t`Importing as system prompt...`, t`System prompt detected`); return await getPresetManager('sysprompt').savePreset(data.name, data); } // 4. Text Completion settings if (this.isPossiblyTextCompletionData(data)) { toastr.info(t`Importing as settings preset...`, t`Text Completion settings detected`); return await getPresetManager('textgenerationwebui').savePreset(fileName, data); } const validSections = []; for (const [key, section] of Object.entries(this.masterSections)) { if (key in data && section.isValid(data[key])) { validSections.push(key); } } if (validSections.length === 0) { toastr.error(t`No valid sections found in imported data`); return; } const sectionNames = validSections.reduce((acc, key) => { acc[key] = { key: key, name: this.masterSections[key].name, preset: data[key]?.name || '' }; return acc; }, {}); const html = $(await renderTemplateAsync('masterImport', { sections: sectionNames })); const popup = new Popup(html, POPUP_TYPE.CONFIRM, '', { okButton: t`Import`, cancelButton: t`Cancel`, }); const result = await popup.show(); // Import cancelled if (result !== POPUP_RESULT.AFFIRMATIVE) { return; } const importedSections = []; const confirmedSections = html.find('input:checked').map((_, el) => el instanceof HTMLInputElement && el.value).get(); if (confirmedSections.length === 0) { toastr.info(t`No sections selected for import`); return; } for (const section of confirmedSections) { const sectionData = data[section]; const masterSection = this.masterSections[section]; if (sectionData && masterSection) { await masterSection.setData(sectionData); importedSections.push(masterSection.name); } } toastr.success(t`Imported ${importedSections.length} settings: ${importedSections.join(', ')}`); } /** * Exports master settings to JSON data. * @returns {Promise} JSON data */ static async performMasterExport() { const sectionNames = Object.entries(this.masterSections).reduce((acc, [key, section]) => { acc[key] = { key: key, name: section.name, checked: key !== 'preset' }; return acc; }, {}); const html = $(await renderTemplateAsync('masterExport', { sections: sectionNames })); const popup = new Popup(html, POPUP_TYPE.CONFIRM, '', { okButton: t`Export`, cancelButton: t`Cancel`, }); const result = await popup.show(); // Export cancelled if (result !== POPUP_RESULT.AFFIRMATIVE) { return; } const confirmedSections = html.find('input:checked').map((_, el) => el instanceof HTMLInputElement && el.value).get(); const data = {}; if (confirmedSections.length === 0) { toastr.info(t`No sections selected for export`); return; } for (const section of confirmedSections) { const masterSection = this.masterSections[section]; if (masterSection) { data[section] = masterSection.getData(); } } return JSON.stringify(data, null, 4); } /** * Gets all preset names. * @returns {string[]} List of preset names */ getAllPresets() { return $(this.select).find('option').map((_, el) => el.text).toArray(); } /** * Finds a preset by name. * @param {string} name Preset name * @returns {any} Preset value */ findPreset(name) { return $(this.select).find('option').filter(function () { return $(this).text() === name; }).val(); } /** * Gets the selected preset value. * @returns {any} Selected preset value */ getSelectedPreset() { return $(this.select).find('option:selected').val(); } /** * Gets the selected preset name. * @returns {string} Selected preset name */ getSelectedPresetName() { return $(this.select).find('option:selected').text(); } /** * Selects a preset by option value. * @param {string} value Preset option value */ selectPreset(value) { const option = $(this.select).filter(function () { return $(this).val() === value; }); option.prop('selected', true); $(this.select).val(value).trigger('change'); } async updatePreset() { const selected = $(this.select).find('option:selected'); console.log(selected); if (selected.val() == 'gui') { toastr.info(t`Cannot update GUI preset`); return; } const name = selected.text(); await this.savePreset(name); const successToast = !this.isAdvancedFormatting() ? t`Preset updated` : t`Template updated`; toastr.success(successToast); } async savePresetAs() { const inputValue = this.getSelectedPresetName(); const popupText = !this.isAdvancedFormatting() ? '

' + t`Hint: Use a character/group name to bind preset to a specific chat.` + '

' : ''; const headerText = !this.isAdvancedFormatting() ? t`Preset name:` : t`Template name:`; const name = await Popup.show.input(headerText, popupText, inputValue); if (!name) { console.log('Preset name not provided'); return; } await this.savePreset(name); const successToast = !this.isAdvancedFormatting() ? t`Preset saved` : t`Template saved`; toastr.success(successToast); } async savePreset(name, settings) { if (this.apiId === 'instruct' && settings) { await checkForSystemPromptInInstructTemplate(name, settings); } if (this.apiId === 'novel' && settings) { settings = convertNovelPreset(settings); } const preset = settings ?? this.getPresetSettings(name); const response = await fetch('/api/presets/save', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ preset, name, apiId: this.apiId }), }); if (!response.ok) { toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Preset could not be saved`); console.error('Preset could not be saved', response); throw new Error('Preset could not be saved'); } const data = await response.json(); name = data.name; this.updateList(name, preset); } async renamePreset(newName) { const oldName = this.getSelectedPresetName(); try { await this.savePreset(newName); await this.deletePreset(oldName); } catch (error) { toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Preset could not be renamed`); console.error('Preset could not be renamed', error); throw new Error('Preset could not be renamed'); } } getPresetList() { let presets = []; let preset_names = {}; switch (this.apiId) { case 'koboldhorde': case 'kobold': presets = koboldai_settings; preset_names = koboldai_setting_names; break; case 'novel': presets = novelai_settings; preset_names = novelai_setting_names; break; case 'textgenerationwebui': presets = textgenerationwebui_presets; preset_names = textgenerationwebui_preset_names; break; case 'context': presets = context_presets; preset_names = context_presets.map(x => x.name); break; case 'instruct': presets = instruct_presets; preset_names = instruct_presets.map(x => x.name); break; case 'sysprompt': presets = system_prompts; preset_names = system_prompts.map(x => x.name); break; default: console.warn(`Unknown API ID ${this.apiId}`); } return { presets, preset_names }; } isKeyedApi() { return this.apiId == 'textgenerationwebui' || this.isAdvancedFormatting(); } isAdvancedFormatting() { return this.apiId == 'context' || this.apiId == 'instruct' || this.apiId == 'sysprompt'; } updateList(name, preset) { const { presets, preset_names } = this.getPresetList(); const presetExists = this.isKeyedApi() ? preset_names.includes(name) : Object.keys(preset_names).includes(name); if (presetExists) { if (this.isKeyedApi()) { presets[preset_names.indexOf(name)] = preset; $(this.select).find(`option[value="${name}"]`).prop('selected', true); $(this.select).val(name).trigger('change'); } else { const value = preset_names[name]; presets[value] = preset; $(this.select).find(`option[value="${value}"]`).prop('selected', true); $(this.select).val(value).trigger('change'); } } else { presets.push(preset); const value = presets.length - 1; if (this.isKeyedApi()) { preset_names[value] = name; const option = $('', { value: name, text: name, selected: true }); $(this.select).append(option); $(this.select).val(name).trigger('change'); } else { preset_names[name] = value; const option = $('', { value: value, text: name, selected: true }); $(this.select).append(option); $(this.select).val(value).trigger('change'); } } } getPresetSettings(name) { function getSettingsByApiId(apiId) { switch (apiId) { case 'koboldhorde': case 'kobold': return kai_settings; case 'novel': return nai_settings; case 'textgenerationwebui': return textgen_settings; case 'context': { const context_preset = getContextSettings(); context_preset['name'] = name || power_user.context.preset; return context_preset; } case 'instruct': { const instruct_preset = structuredClone(power_user.instruct); instruct_preset['name'] = name || power_user.instruct.preset; return instruct_preset; } case 'sysprompt': { const sysprompt_preset = structuredClone(power_user.sysprompt); sysprompt_preset['name'] = name || power_user.sysprompt.preset; return sysprompt_preset; } default: console.warn(`Unknown API ID ${apiId}`); return {}; } } const filteredKeys = [ 'preset', 'streaming', 'truncation_length', 'n', 'streaming_url', 'stopping_strings', 'can_use_tokenization', 'can_use_streaming', 'preset_settings_novel', 'streaming_novel', 'nai_preamble', 'model_novel', 'streaming_kobold', 'enabled', 'bind_to_context', 'seed', 'legacy_api', 'mancer_model', 'togetherai_model', 'ollama_model', 'vllm_model', 'aphrodite_model', 'server_urls', 'type', 'custom_model', 'bypass_status_check', 'infermaticai_model', 'dreamgen_model', 'openrouter_model', 'featherless_model', 'max_tokens_second', 'openrouter_providers', 'openrouter_allow_fallbacks', 'tabby_model', ]; const settings = Object.assign({}, getSettingsByApiId(this.apiId)); for (const key of filteredKeys) { if (Object.hasOwn(settings, key)) { delete settings[key]; } } if (!this.isAdvancedFormatting()) { settings['genamt'] = amount_gen; settings['max_length'] = max_context; } return settings; } // pass no arguments to delete current preset async deletePreset(name) { const { preset_names, presets } = this.getPresetList(); const value = name ? (this.isKeyedApi() ? this.findPreset(name) : name) : this.getSelectedPreset(); const nameToDelete = name || this.getSelectedPresetName(); if (value == 'gui') { toastr.info(t`Cannot delete GUI preset`); return; } $(this.select).find(`option[value="${value}"]`).remove(); if (this.isKeyedApi()) { const index = preset_names.indexOf(nameToDelete); preset_names.splice(index, 1); presets.splice(index, 1); } else { delete preset_names[nameToDelete]; } // switch in UI only when deleting currently selected preset const switchPresets = !name || this.getSelectedPresetName() == name; if (Object.keys(preset_names).length && switchPresets) { const nextPresetName = Object.keys(preset_names)[0]; const newValue = preset_names[nextPresetName]; $(this.select).find(`option[value="${newValue}"]`).attr('selected', 'true'); $(this.select).trigger('change'); } const response = await fetch('/api/presets/delete', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ name: nameToDelete, apiId: this.apiId }), }); return response.ok; } async getDefaultPreset(name) { const response = await fetch('/api/presets/restore', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ name, apiId: this.apiId }), }); if (!response.ok) { const errorToast = !this.isAdvancedFormatting() ? t`Failed to restore default preset` : t`Failed to restore default template`; toastr.error(errorToast); return; } return await response.json(); } } /** * Selects a preset by name for current API. * @param {any} _ Named arguments * @param {string} name Unnamed arguments * @returns {Promise} Selected or current preset name */ async function presetCommandCallback(_, name) { const shouldReconnect = online_status !== 'no_connection'; const presetManager = getPresetManager(); const allPresets = presetManager.getAllPresets(); const currentPreset = presetManager.getSelectedPresetName(); if (!presetManager) { console.debug(`Preset Manager not found for API: ${main_api}`); return ''; } if (!name) { console.log('No name provided for /preset command, using current preset'); return currentPreset; } if (!Array.isArray(allPresets) || allPresets.length === 0) { console.log(`No presets found for API: ${main_api}`); return currentPreset; } // Find exact match const exactMatch = allPresets.find(p => p.toLowerCase().trim() === name.toLowerCase().trim()); if (exactMatch) { console.log('Found exact preset match', exactMatch); if (currentPreset !== exactMatch) { const presetValue = presetManager.findPreset(exactMatch); if (presetValue) { presetManager.selectPreset(presetValue); shouldReconnect && await waitForConnection(); } } return exactMatch; } else { // Find fuzzy match const fuse = new Fuse(allPresets); const fuzzyMatch = fuse.search(name); if (!fuzzyMatch.length) { console.warn(`WARN: Preset found with name ${name}`); return currentPreset; } const fuzzyPresetName = fuzzyMatch[0].item; const fuzzyPresetValue = presetManager.findPreset(fuzzyPresetName); if (fuzzyPresetValue) { console.log('Found fuzzy preset match', fuzzyPresetName); if (currentPreset !== fuzzyPresetName) { presetManager.selectPreset(fuzzyPresetValue); shouldReconnect && await waitForConnection(); } } return fuzzyPresetName; } } /** * Waits for API connection to be established. */ async function waitForConnection() { try { await waitUntilCondition(() => online_status !== 'no_connection', 10000, 100); } catch { console.log('Timeout waiting for API to connect'); } } export async function initPresetManager() { eventSource.on(event_types.CHAT_CHANGED, autoSelectPreset); registerPresetManagers(); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'preset', callback: presetCommandCallback, returns: 'current preset', unnamedArgumentList: [ SlashCommandArgument.fromProps({ description: 'name', typeList: [ARGUMENT_TYPE.STRING], enumProvider: () => getPresetManager().getAllPresets().map(preset => new SlashCommandEnumValue(preset, null, enumTypes.enum, enumIcons.preset)), }), ], helpString: `
Sets a preset by name for the current API. Gets the current preset if no name is provided.
Example:
  • /preset myPreset
  • /preset
`, })); $(document).on('click', '[data-preset-manager-update]', async function () { const apiId = $(this).data('preset-manager-update'); const presetManager = getPresetManager(apiId); if (!presetManager) { console.warn(`Preset Manager not found for API: ${apiId}`); return; } await presetManager.updatePreset(); }); $(document).on('click', '[data-preset-manager-new]', async function () { const apiId = $(this).data('preset-manager-new'); const presetManager = getPresetManager(apiId); if (!presetManager) { console.warn(`Preset Manager not found for API: ${apiId}`); return; } await presetManager.savePresetAs(); }); $(document).on('click', '[data-preset-manager-rename]', async function () { const apiId = $(this).data('preset-manager-rename'); const presetManager = getPresetManager(apiId); if (!presetManager) { console.warn(`Preset Manager not found for API: ${apiId}`); return; } const popupHeader = !presetManager.isAdvancedFormatting() ? t`Rename preset` : t`Rename template`; const oldName = presetManager.getSelectedPresetName(); const newName = await Popup.show.input(popupHeader, t`Enter a new name:`, oldName); if (!newName || oldName === newName) { console.debug(!presetManager.isAdvancedFormatting() ? 'Preset rename cancelled' : 'Template rename cancelled'); return; } await presetManager.renamePreset(newName); const successToast = !presetManager.isAdvancedFormatting() ? t`Preset renamed` : t`Template renamed`; toastr.success(successToast); }); $(document).on('click', '[data-preset-manager-export]', async function () { const apiId = $(this).data('preset-manager-export'); const presetManager = getPresetManager(apiId); if (!presetManager) { console.warn(`Preset Manager not found for API: ${apiId}`); return; } const selected = $(presetManager.select).find('option:selected'); const name = selected.text(); const preset = presetManager.getPresetSettings(name); const data = JSON.stringify(preset, null, 4); download(data, `${name}.json`, 'application/json'); }); $(document).on('click', '[data-preset-manager-import]', async function () { const apiId = $(this).data('preset-manager-import'); $(`[data-preset-manager-file="${apiId}"]`).trigger('click'); }); $(document).on('change', '[data-preset-manager-file]', async function (e) { const apiId = $(this).data('preset-manager-file'); const presetManager = getPresetManager(apiId); if (!presetManager) { console.warn(`Preset Manager not found for API: ${apiId}`); return; } const file = e.target.files[0]; if (!file) { return; } const fileName = file.name.replace('.json', '').replace('.settings', ''); const data = await parseJsonFile(file); const name = data?.name ?? fileName; data['name'] = name; await presetManager.savePreset(name, data); const successToast = !presetManager.isAdvancedFormatting() ? t`Preset imported` : t`Template imported`; toastr.success(successToast); e.target.value = null; }); $(document).on('click', '[data-preset-manager-delete]', async function () { const apiId = $(this).data('preset-manager-delete'); const presetManager = getPresetManager(apiId); if (!presetManager) { console.warn(`Preset Manager not found for API: ${apiId}`); return; } const headerText = !presetManager.isAdvancedFormatting() ? t`Delete this preset?` : t`Delete this template?`; const confirm = await Popup.show.confirm(headerText, t`This action is irreversible and your current settings will be overwritten.`); if (!confirm) { return; } const result = await presetManager.deletePreset(); if (result) { const successToast = !presetManager.isAdvancedFormatting() ? t`Preset deleted` : t`Template deleted`; toastr.success(successToast); } else { const warningToast = !presetManager.isAdvancedFormatting() ? t`Preset was not deleted from server` : t`Template was not deleted from server`; toastr.warning(warningToast); } saveSettingsDebounced(); }); $(document).on('click', '[data-preset-manager-restore]', async function () { const apiId = $(this).data('preset-manager-restore'); const presetManager = getPresetManager(apiId); if (!presetManager) { console.warn(`Preset Manager not found for API: ${apiId}`); return; } const name = presetManager.getSelectedPresetName(); const data = await presetManager.getDefaultPreset(name); if (name == 'gui') { toastr.info(t`Cannot restore GUI preset`); return; } if (!data) { return; } if (data.isDefault) { if (Object.keys(data.preset).length === 0) { const errorToast = !presetManager.isAdvancedFormatting() ? t`Default preset cannot be restored` : t`Default template cannot be restored`; toastr.error(errorToast); return; } const confirmText = !presetManager.isAdvancedFormatting() ? t`Resetting a default preset will restore the default settings.` : t`Resetting a default template will restore the default settings.`; const confirm = await Popup.show.confirm(t`Are you sure?`, confirmText); if (!confirm) { return; } await presetManager.deletePreset(); await presetManager.savePreset(name, data.preset); const option = presetManager.findPreset(name); presetManager.selectPreset(option); const successToast = !presetManager.isAdvancedFormatting() ? t`Default preset restored` : t`Default template restored`; toastr.success(successToast); } else { const confirmText = !presetManager.isAdvancedFormatting() ? t`Resetting a custom preset will restore to the last saved state.` : t`Resetting a custom template will restore to the last saved state.`; const confirm = await Popup.show.confirm(t`Are you sure?`, confirmText); if (!confirm) { return; } const option = presetManager.findPreset(name); presetManager.selectPreset(option); const successToast = !presetManager.isAdvancedFormatting() ? t`Preset restored` : t`Template restored`; toastr.success(successToast); } }); $('#af_master_import').on('click', () => { $('#af_master_import_file').trigger('click'); }); $('#af_master_import_file').on('change', async function (e) { if (!(e.target instanceof HTMLInputElement)) { return; } const file = e.target.files[0]; if (!file) { return; } const data = await parseJsonFile(file); const fileName = file.name.replace('.json', ''); await PresetManager.performMasterImport(data, fileName); e.target.value = null; }); $('#af_master_export').on('click', async () => { const data = await PresetManager.performMasterExport(); if (!data) { return; } const shortDate = new Date().toISOString().split('T')[0]; download(data, `ST-formatting-${shortDate}.json`, 'application/json'); }); }