mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2024-12-14 10:24:47 +01:00
614 lines
19 KiB
JavaScript
614 lines
19 KiB
JavaScript
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',
|
||
'seed',
|
||
'mancer_model',
|
||
];
|
||
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', 5000, 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', 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');
|
||
}
|
||
});
|
||
}
|