SillyTavern/public/scripts/preset-manager.js
2024-05-02 22:40:40 +00:00

628 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
amount_gen,
callPopup,
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 { context_presets, getContextSettings, power_user } from './power-user.js';
import { registerSlashCommand } from './slash-commands.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:contains(${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) {
$(this.select).find(`option[value=${value}]`).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 = `
<h3>Preset name:</h3>
${!this.isNonGenericApi() ? '<h4>Hint: Use a character/group name to bind preset to a specific chat.</h4>' : ''}`;
const name = await callPopup(popupText, 'input', 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 res = await fetch('/api/presets/save', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ preset, name, apiId: this.apiId }),
});
if (!res.ok) {
toastr.error('Failed to save preset');
}
const data = await res.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_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',
'max_tokens_second',
];
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();
registerSlashCommand('preset', presetCommandCallback, [], '<span class="monospace">(name)</span> sets a preset by name for the current API. Gets the current preset if no name is provided', true, true);
$(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 callPopup('Delete the preset? This action is irreversible and your current settings will be overwritten.', 'confirm');
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 callPopup('<h3>Are you sure?</h3>Resetting a <b>default preset</b> will restore the default settings.', 'confirm');
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 callPopup('<h3>Are you sure?</h3>Resetting a <b>custom preset</b> will restore to the last saved state.', 'confirm');
if (!confirm) {
return;
}
const option = presetManager.findPreset(name);
presetManager.selectPreset(option);
toastr.success('Preset restored');
}
});
}