Spaces:
Running
Running
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 { Popup } 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 { | |
textgenerationwebui_preset_names, | |
textgenerationwebui_presets, | |
textgenerationwebui_settings as textgen_settings, | |
} from './textgen-settings.js'; | |
import { download, parseJsonFile, waitUntilCondition } from './utils.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 | |
*/ | |
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; | |
} | |
/** | |
* 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('Cannot update GUI preset'); | |
return; | |
} | |
const name = selected.text(); | |
await this.savePreset(name); | |
toastr.success('Preset updated'); | |
} | |
async savePresetAs() { | |
const inputValue = this.getSelectedPresetName(); | |
const popupText = !this.isNonGenericApi() ? '<h4>Hint: Use a character/group name to bind preset to a specific chat.</h4>' : ''; | |
const name = await Popup.show.input('Preset name:', popupText, inputValue); | |
if (!name) { | |
console.log('Preset name not provided'); | |
return; | |
} | |
await this.savePreset(name); | |
toastr.success('Preset saved'); | |
} | |
async savePreset(name, 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('Check the server connection and reload the page to prevent data loss.', '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); | |
} | |
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; | |
default: | |
console.warn(`Unknown API ID ${this.apiId}`); | |
} | |
return { presets, preset_names }; | |
} | |
isKeyedApi() { | |
return this.apiId == 'textgenerationwebui' || this.apiId == 'context' || this.apiId == 'instruct'; | |
} | |
isNonGenericApi() { | |
return this.apiId == 'context' || this.apiId == 'instruct'; | |
} | |
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 = $('<option></option>', { value: name, text: name, selected: true }); | |
$(this.select).append(option); | |
$(this.select).val(name).trigger('change'); | |
} else { | |
preset_names[name] = value; | |
const option = $('<option></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; | |
} | |
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', | |
]; | |
const settings = Object.assign({}, getSettingsByApiId(this.apiId)); | |
for (const key of filteredKeys) { | |
if (Object.hasOwn(settings, key)) { | |
delete settings[key]; | |
} | |
} | |
if (!this.isNonGenericApi()) { | |
settings['genamt'] = amount_gen; | |
settings['max_length'] = max_context; | |
} | |
return settings; | |
} | |
async deleteCurrentPreset() { | |
const { preset_names, presets } = this.getPresetList(); | |
const value = this.getSelectedPreset(); | |
const nameToDelete = this.getSelectedPresetName(); | |
if (value == 'gui') { | |
toastr.info('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]; | |
} | |
if (Object.keys(preset_names).length) { | |
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) { | |
toastr.error('Failed to restore default preset'); | |
return; | |
} | |
return await response.json(); | |
} | |
} | |
/** | |
* Selects a preset by name for current API. | |
* @param {any} _ Named arguments | |
* @param {string} name Unnamed arguments | |
* @returns {Promise<string>} 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: ` | |
<div> | |
Sets a preset by name for the current API. Gets the current preset if no name is provided. | |
</div> | |
<div> | |
<strong>Example:</strong> | |
<ul> | |
<li> | |
<pre><code>/preset myPreset</code></pre> | |
</li> | |
<li> | |
<pre><code>/preset</code></pre> | |
</li> | |
</ul> | |
</div> | |
`, | |
})); | |
$(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-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); | |
toastr.success('Preset imported'); | |
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; | |
} | |
// default context preset cannot be deleted | |
if (apiId == 'context' && power_user.default_context === power_user.context.preset) { | |
return; | |
} | |
const confirm = await Popup.show.confirm('Delete the preset?', 'This action is irreversible and your current settings will be overwritten.'); | |
if (!confirm) { | |
return; | |
} | |
const result = await presetManager.deleteCurrentPreset(); | |
if (result) { | |
toastr.success('Preset deleted'); | |
} else { | |
toastr.warning('Preset was not deleted from server'); | |
} | |
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('Cannot restore GUI preset'); | |
return; | |
} | |
if (!data) { | |
return; | |
} | |
if (data.isDefault) { | |
if (Object.keys(data.preset).length === 0) { | |
toastr.error('Default preset cannot be restored'); | |
return; | |
} | |
const confirm = await Popup.show.confirm('Are you sure?', 'Resetting a <b>default preset</b> will restore the default settings.'); | |
if (!confirm) { | |
return; | |
} | |
await presetManager.deleteCurrentPreset(); | |
await presetManager.savePreset(name, data.preset); | |
const option = presetManager.findPreset(name); | |
presetManager.selectPreset(option); | |
toastr.success('Default preset restored'); | |
} else { | |
const confirm = await Popup.show.confirm('Are you sure?', 'Resetting a <b>custom preset</b> will restore to the last saved state.'); | |
if (!confirm) { | |
return; | |
} | |
const option = presetManager.findPreset(name); | |
presetManager.selectPreset(option); | |
toastr.success('Preset restored'); | |
} | |
}); | |
} | |