mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-01-31 11:35:37 +01:00
#746 Add preset manager for ooba/kobold
This commit is contained in:
parent
06899581db
commit
130559d499
@ -95,6 +95,7 @@
|
||||
<script type="module" src="scripts/context-template.js"></script>
|
||||
<script type="module" src="scripts/extensions.js"></script>
|
||||
<script type="module" src="scripts/authors-note.js"></script>
|
||||
<script type="module" src="scripts/preset-manager.js"></script>
|
||||
<script type="text/javascript" src="scripts/toolcool-color-picker.js"></script>
|
||||
|
||||
<title>SillyTavern</title>
|
||||
@ -131,15 +132,24 @@
|
||||
<div class="scrollableInner">
|
||||
<div class="flex-container" id="ai_response_configuration">
|
||||
<div id="respective-presets-block" class="width100p">
|
||||
<input type="file" hidden data-preset-manager-file="" accept=".json, .settings">
|
||||
<div id="kobold_api-presets">
|
||||
<h3><span data-i18n="kobldpresets">Kobold Presets</span>
|
||||
<a href="https://docs.sillytavern.app/usage/api-connections/koboldai/" class="notes-link" target="_blank">
|
||||
<span class="note-link-span">?</span>
|
||||
</a>
|
||||
</h3>
|
||||
<select id="settings_perset" data-preset-manager-for="kobold,koboldhorde">
|
||||
<option value="gui" data-i18n="guikoboldaisettings">GUI KoboldAI Settings</option>
|
||||
</select>
|
||||
|
||||
<div class="preset_buttons">
|
||||
<select id="settings_perset" data-preset-manager-for="kobold">
|
||||
<option value="gui" data-i18n="guikoboldaisettings">GUI KoboldAI Settings</option>
|
||||
</select>
|
||||
<i data-preset-manager-update="kobold" class="menu_button fa-solid fa-save" title="Update current preset" data-i18n="[title]Update current preset"></i>
|
||||
<i data-preset-manager-new="kobold" class="menu_button fa-solid fa-plus" title="Create new preset" data-i18n="[title]Create new preset"></i>
|
||||
<i data-preset-manager-import="kobold" class="menu_button fa-solid fa-upload" title="Import preset" data-i18n="[title]Import preset"></i>
|
||||
<i data-preset-manager-export="kobold" class="menu_button fa-solid fa-download"title="Export preset" data-i18n="[title]Export preset"></i>
|
||||
<i data-preset-manager-delete="kobold" class="menu_button fa-solid fa-trash-can" title="Delete the preset" data-i18n="[title]Delete the preset"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div id="novel_api-presets">
|
||||
<h3>
|
||||
@ -171,8 +181,15 @@
|
||||
<div id="textgenerationwebui_api-presets">
|
||||
<h3><span data-i18n="text gen webio(ooba)preset">Text Gen WebUI (ooba) presets</span>
|
||||
</h3>
|
||||
<select id="settings_preset_textgenerationwebui" data-preset-manager-for="textgenerationwebui">
|
||||
</select>
|
||||
<div class="preset_buttons">
|
||||
<select id="settings_preset_textgenerationwebui" data-preset-manager-for="textgenerationwebui">
|
||||
</select>
|
||||
<i data-preset-manager-update="textgenerationwebui" class="menu_button fa-solid fa-save" title="Update current preset" data-i18n="[title]Update current preset"></i>
|
||||
<i data-preset-manager-new="textgenerationwebui" class="menu_button fa-solid fa-plus" title="Create new preset" data-i18n="[title]Create new preset"></i>
|
||||
<i data-preset-manager-import="textgenerationwebui" class="menu_button fa-solid fa-upload" title="Import preset" data-i18n="[title]Import preset"></i>
|
||||
<i data-preset-manager-export="textgenerationwebui" class="menu_button fa-solid fa-download"title="Export preset" data-i18n="[title]Export preset"></i>
|
||||
<i data-preset-manager-delete="textgenerationwebui" class="menu_button fa-solid fa-trash-can" title="Delete the preset" data-i18n="[title]Delete the preset"></i>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
</div>
|
||||
|
@ -76,6 +76,7 @@ import {
|
||||
persona_description_positions,
|
||||
loadMovingUIState,
|
||||
getCustomStoppingStrings,
|
||||
MAX_CONTEXT_DEFAULT,
|
||||
} from "./scripts/power-user.js";
|
||||
|
||||
import {
|
||||
@ -701,11 +702,11 @@ var is_use_scroll_holder = false;
|
||||
|
||||
//settings
|
||||
var settings;
|
||||
var koboldai_settings;
|
||||
var koboldai_setting_names;
|
||||
export let koboldai_settings;
|
||||
export let koboldai_setting_names;
|
||||
var preset_settings = "gui";
|
||||
var user_avatar = "you.png";
|
||||
var amount_gen = 80; //default max length of AI generated responses
|
||||
export var amount_gen = 80; //default max length of AI generated responses
|
||||
var max_context = 2048;
|
||||
|
||||
var is_pygmalion = false;
|
||||
@ -719,8 +720,8 @@ let extension_prompts = {};
|
||||
var main_api;// = "kobold";
|
||||
//novel settings
|
||||
let novel_tier;
|
||||
let novelai_settings;
|
||||
let novelai_setting_names;
|
||||
export let novelai_settings;
|
||||
export let novelai_setting_names;
|
||||
let abortController;
|
||||
|
||||
//css
|
||||
@ -5087,7 +5088,23 @@ async function saveSettings(type) {
|
||||
});
|
||||
}
|
||||
|
||||
export function setGenerationParamsFromPreset(preset) {
|
||||
if (preset.genamt !== undefined) {
|
||||
amount_gen = preset.genamt;
|
||||
$("#amount_gen").val(amount_gen);
|
||||
$("#amount_gen_counter").text(`${amount_gen}`);
|
||||
}
|
||||
|
||||
if (preset.max_length !== undefined) {
|
||||
max_context = preset.max_length;
|
||||
|
||||
const needsUnlock = max_context > MAX_CONTEXT_DEFAULT;
|
||||
$('#max_context_unlocked').prop('checked', needsUnlock).trigger('change');
|
||||
|
||||
$("#max_context").val(max_context);
|
||||
$("#max_context_counter").text(`${max_context}`);
|
||||
}
|
||||
}
|
||||
|
||||
function setCharacterBlockHeight() {
|
||||
const $children = $("#rm_print_characters_block").children();
|
||||
@ -7724,13 +7741,7 @@ $(document).ready(function () {
|
||||
const preset = koboldai_settings[koboldai_setting_names[preset_settings]];
|
||||
loadKoboldSettings(preset);
|
||||
|
||||
amount_gen = preset.genamt;
|
||||
$("#amount_gen").val(amount_gen);
|
||||
$("#amount_gen_counter").text(`${amount_gen}`);
|
||||
|
||||
max_context = preset.max_length;
|
||||
$("#max_context").val(max_context);
|
||||
$("#max_context_counter").text(`${max_context}`);
|
||||
setGenerationParamsFromPreset(preset);
|
||||
|
||||
$("#range_block").find('input').prop("disabled", false);
|
||||
$("#kobold-advanced-config").find('input').prop("disabled", false);
|
||||
|
@ -43,7 +43,7 @@ export {
|
||||
send_on_enter_options,
|
||||
};
|
||||
|
||||
const MAX_CONTEXT_DEFAULT = 4096;
|
||||
export const MAX_CONTEXT_DEFAULT = 4096;
|
||||
const MAX_CONTEXT_UNLOCKED = 65536;
|
||||
|
||||
const avatar_styles = {
|
||||
|
351
public/scripts/preset-manager.js
Normal file
351
public/scripts/preset-manager.js
Normal file
@ -0,0 +1,351 @@
|
||||
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,
|
||||
saveSettingsDebounced,
|
||||
this_chid,
|
||||
} from "../script.js";
|
||||
import { groups, selected_group } from "./group-chats.js";
|
||||
import { kai_settings } from "./kai-settings.js";
|
||||
import {
|
||||
textgenerationwebui_preset_names,
|
||||
textgenerationwebui_presets,
|
||||
textgenerationwebui_settings,
|
||||
} from "./textgen-settings.js";
|
||||
import { download, parseJsonFile, waitUntilCondition } from "./utils.js";
|
||||
|
||||
const presetManagers = {};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
function getPresetManager() {
|
||||
const apiId = main_api == 'koboldhorde' ? 'kobold' : main_api;
|
||||
|
||||
if (!Object.keys(presetManagers).includes(apiId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return presetManagers[apiId];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
findPreset(name) {
|
||||
return $(this.select).find(`option:contains(${name})`).val();
|
||||
}
|
||||
|
||||
getSelectedPreset() {
|
||||
return $(this.select).find("option:selected").val();
|
||||
}
|
||||
|
||||
getSelectedPresetName() {
|
||||
return $(this.select).find("option:selected").text();
|
||||
}
|
||||
|
||||
selectPreset(preset) {
|
||||
$(this.select).find(`option[value=${preset}]`).prop('selected', true);
|
||||
$(this.select).val(preset).trigger("change");
|
||||
}
|
||||
|
||||
async updatePreset() {
|
||||
const selected = $(this.select).find("option: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 popupText = `
|
||||
<h3>Preset name:</h3>
|
||||
<h4>Hint: Use a character/group name to bind preset to a specific chat.</h4>`;
|
||||
const name = await callPopup(popupText, "input");
|
||||
await this.savePreset(name);
|
||||
toastr.success('Preset saved');
|
||||
}
|
||||
|
||||
async savePreset(name, settings) {
|
||||
const preset = settings ?? this.getPresetSettings();
|
||||
const res = await fetch(`/save_preset`, {
|
||||
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;
|
||||
default:
|
||||
console.warn(`Unknown API ID ${this.apiId}`);
|
||||
}
|
||||
|
||||
return { presets, preset_names };
|
||||
}
|
||||
|
||||
updateList(name, preset) {
|
||||
const { presets, preset_names } = this.getPresetList();
|
||||
const presetExists = this.apiId == "textgenerationwebui" ? preset_names.includes(name) : Object.keys(preset_names).includes(name);
|
||||
|
||||
if (presetExists) {
|
||||
if (this.apiId == "textgenerationwebui") {
|
||||
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;
|
||||
// ooba is reversed
|
||||
if (this.apiId == "textgenerationwebui") {
|
||||
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() {
|
||||
function getSettingsByApiId(apiId) {
|
||||
switch (apiId) {
|
||||
case "koboldhorde":
|
||||
case "kobold":
|
||||
return kai_settings;
|
||||
case "novel":
|
||||
return nai_settings;
|
||||
case "textgenerationwebui":
|
||||
return textgenerationwebui_settings;
|
||||
default:
|
||||
console.warn(`Unknown API ID ${apiId}`);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
const filteredKeys = ['preset', 'streaming_url', 'stopping_strings', 'use_stop_sequence'];
|
||||
const settings = Object.assign({}, getSettingsByApiId(this.apiId));
|
||||
|
||||
for (const key of filteredKeys) {
|
||||
if (settings.hasOwnProperty(key)) {
|
||||
delete settings[key];
|
||||
}
|
||||
}
|
||||
|
||||
settings['genamt'] = amount_gen;
|
||||
settings['max_length'] = max_context;
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
async deleteCurrentPreset() {
|
||||
const { presets, preset_names } = 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.apiId == "textgenerationwebui") {
|
||||
preset_names.splice(preset_names.indexOf(value), 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('/delete_preset', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({ name: nameToDelete, apiId: this.apiId }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
toastr.warning('Preset was not deleted from server');
|
||||
} else {
|
||||
toastr.success('Preset deleted');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jQuery(async () => {
|
||||
await waitUntilCondition(() => eventSource !== undefined);
|
||||
|
||||
eventSource.on(event_types.CHAT_CHANGED, autoSelectPreset);
|
||||
registerPresetManagers();
|
||||
$(document).on("click", "[data-preset-manager-update]", async function () {
|
||||
const presetManager = getPresetManager();
|
||||
|
||||
if (!presetManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
await presetManager.updatePreset();
|
||||
});
|
||||
|
||||
$(document).on("click", "[data-preset-manager-new]", async function () {
|
||||
const presetManager = getPresetManager();
|
||||
|
||||
if (!presetManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
await presetManager.savePresetAs();
|
||||
});
|
||||
|
||||
$(document).on("click", "[data-preset-manager-export]", async function () {
|
||||
const presetManager = getPresetManager();
|
||||
|
||||
if (!presetManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selected = $(presetManager.select).find("option:selected");
|
||||
const name = selected.text();
|
||||
const preset = presetManager.getPresetSettings();
|
||||
const data = JSON.stringify(preset, null, 4);
|
||||
download(data, `${name}.json`, "application/json");
|
||||
});
|
||||
|
||||
$(document).on("click", "[data-preset-manager-import]", async function () {
|
||||
$('[data-preset-manager-file]').trigger('click');
|
||||
});
|
||||
|
||||
$(document).on("change", "[data-preset-manager-file]", async function (e) {
|
||||
const presetManager = getPresetManager();
|
||||
|
||||
if (!presetManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = e.target.files[0];
|
||||
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = file.name.replace('.json', '').replace('.settings', '');
|
||||
const data = await parseJsonFile(file);
|
||||
|
||||
await presetManager.savePreset(name, data);
|
||||
toastr.success('Preset imported');
|
||||
e.target.value = null;
|
||||
});
|
||||
|
||||
$(document).on("click", "[data-preset-manager-delete]", async function () {
|
||||
const presetManager = getPresetManager();
|
||||
|
||||
if (!presetManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirm = await callPopup('Delete the preset? This action is irreversible and your current settings will be overwritten.', 'confirm');
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
await presetManager.deleteCurrentPreset();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
})
|
@ -3,6 +3,7 @@ import {
|
||||
getStoppingStrings,
|
||||
max_context,
|
||||
saveSettingsDebounced,
|
||||
setGenerationParamsFromPreset,
|
||||
} from "../script.js";
|
||||
|
||||
export {
|
||||
@ -44,8 +45,8 @@ const textgenerationwebui_settings = {
|
||||
mirostat_eta: 0.1,
|
||||
};
|
||||
|
||||
let textgenerationwebui_presets = [];
|
||||
let textgenerationwebui_preset_names = [];
|
||||
export let textgenerationwebui_presets = [];
|
||||
export let textgenerationwebui_preset_names = [];
|
||||
|
||||
const setting_names = [
|
||||
"temp",
|
||||
@ -89,6 +90,7 @@ function selectPreset(name) {
|
||||
const value = preset[name];
|
||||
setSettingByName(name, value, true);
|
||||
}
|
||||
setGenerationParamsFromPreset(preset);
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
|
@ -4500,14 +4500,14 @@ toolcool-color-picker {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.openai_preset_buttons {
|
||||
.openai_preset_buttons, .preset_buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.openai_preset_buttons select {
|
||||
.openai_preset_buttons select, .preset_buttons select {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@ -5422,4 +5422,4 @@ body.waifuMode .zoomed_avatar {
|
||||
background-color: var(--SmartThemeBlurTintColor);
|
||||
text-align: center;
|
||||
line-height: 14px;
|
||||
}
|
||||
}
|
||||
|
55
server.js
55
server.js
@ -3341,6 +3341,47 @@ app.post("/tokenize_openai", jsonParser, function (request, response_tokenize_op
|
||||
response_tokenize_openai.send({ "token_count": num_tokens });
|
||||
});
|
||||
|
||||
app.post("/save_preset", jsonParser, function (request, response) {
|
||||
const name = sanitize(request.body.name);
|
||||
if (!request.body.preset || !name) {
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
|
||||
const filename = `${name}.settings`;
|
||||
const directory = getPresetFolderByApiId(request.body.apiId);
|
||||
|
||||
if (!directory) {
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
|
||||
const fullpath = path.join(directory, filename);
|
||||
fs.writeFileSync(fullpath, JSON.stringify(request.body.preset, null, 4), 'utf-8');
|
||||
return response.send({ name });
|
||||
});
|
||||
|
||||
app.post("/delete_preset", jsonParser, function (request, response) {
|
||||
const name = sanitize(request.body.name);
|
||||
if (!name) {
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
|
||||
const filename = `${name}.settings`;
|
||||
const directory = getPresetFolderByApiId(request.body.apiId);
|
||||
|
||||
if (!directory) {
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
|
||||
const fullpath = path.join(directory, filename);
|
||||
|
||||
if (fs.existsSync) {
|
||||
fs.unlinkSync(fullpath);
|
||||
return response.sendStatus(200);
|
||||
} else {
|
||||
return response.sendStatus(404);
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/savepreset_openai", jsonParser, function (request, response) {
|
||||
const name = sanitize(request.query.name);
|
||||
if (!request.body || !name) {
|
||||
@ -3353,6 +3394,20 @@ app.post("/savepreset_openai", jsonParser, function (request, response) {
|
||||
return response.send({ name });
|
||||
});
|
||||
|
||||
function getPresetFolderByApiId(apiId) {
|
||||
switch (apiId) {
|
||||
case 'kobold':
|
||||
case 'koboldhorde':
|
||||
return directories.koboldAI_Settings;
|
||||
case 'novel':
|
||||
return directories.novelAI_Settings;
|
||||
case 'textgenerationwebui':
|
||||
return directories.textGen_Settings;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function createTokenizationHandler(getTokenizerFn) {
|
||||
return async function (request, response) {
|
||||
if (!request.body) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user