mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules
|
30
public/KoboldAI Worlds/Sample.json
Normal file
30
public/KoboldAI Worlds/Sample.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"folders": {
|
||||
"Sample Folder": [
|
||||
0,
|
||||
1
|
||||
]
|
||||
},
|
||||
"entries": {
|
||||
"0": {
|
||||
"uid": 0,
|
||||
"title": "AAA",
|
||||
"key": [ "AAA" ],
|
||||
"keysecondary": [ ],
|
||||
"constant": false,
|
||||
"content": "AAA is a city where BBB lives.",
|
||||
"comment": "AAA definition",
|
||||
"selective": true
|
||||
},
|
||||
"1": {
|
||||
"uid": 1,
|
||||
"title": "BBB",
|
||||
"key": [ "BBB" ],
|
||||
"keysecondary": [ ],
|
||||
"constant": false,
|
||||
"content": "BBB is a 21-year old female student of CCC academy.",
|
||||
"comment": "BBB definition",
|
||||
"selective": true
|
||||
}
|
||||
}
|
||||
}
|
@@ -54,6 +54,7 @@
|
||||
var is_mes_reload_avatar = false;
|
||||
|
||||
var is_advanced_char_open = false;
|
||||
var is_world_edit_open = false;
|
||||
|
||||
var menu_type = '';//what is selected in the menu
|
||||
var selected_button = '';//which button pressed
|
||||
@@ -67,6 +68,8 @@
|
||||
var create_save_mes_example = '';
|
||||
|
||||
var timerSaveEdit;
|
||||
var timerKoboldSync;
|
||||
var timerWorldSave;
|
||||
var durationSaveEdit = 200;
|
||||
//animation right menu
|
||||
var animation_rm_duration = 200;
|
||||
@@ -102,6 +105,13 @@
|
||||
var user_avatar = 'you.png';
|
||||
var temp = 0.5;
|
||||
var amount_gen = 80;
|
||||
var kobold_world = null;
|
||||
var koboldai_world_names;
|
||||
var kobold_world_synced = false;
|
||||
var kobold_sync_failed = false;
|
||||
var kobold_is_united = false;
|
||||
var kobold_world_data = null;
|
||||
var imported_world_name = '';
|
||||
var max_context = 2048;//2048;
|
||||
var rep_pen = 1;
|
||||
var rep_pen_size = 100;
|
||||
@@ -191,7 +201,6 @@
|
||||
$("#online_status_indicator3").css("background-color", "green");
|
||||
$("#online_status_text3").html(online_status);
|
||||
}
|
||||
|
||||
}
|
||||
async function getLastVersion(){
|
||||
|
||||
@@ -251,6 +260,9 @@
|
||||
online_status = data.result;
|
||||
if(online_status == undefined){
|
||||
online_status = 'no_connection';
|
||||
|
||||
kobold_world_synced = false;
|
||||
updateWorldStatus();
|
||||
}
|
||||
if(online_status.toLowerCase().indexOf('pygmalion') != -1){
|
||||
is_pygmalion = true;
|
||||
@@ -263,6 +275,7 @@
|
||||
resultCheckStatus();
|
||||
if(online_status !== 'no_connection'){
|
||||
var checkStatusNow = setTimeout(getStatus, 3000);//getStatus();
|
||||
syncKoboldWorldInfo(false);
|
||||
}
|
||||
},
|
||||
error: function (jqXHR, exception) {
|
||||
@@ -270,12 +283,18 @@
|
||||
console.log(jqXHR);
|
||||
online_status = 'no_connection';
|
||||
|
||||
// invalidate world info when losing connection to kobold
|
||||
kobold_world_synced = false;
|
||||
updateWorldStatus();
|
||||
|
||||
resultCheckStatus();
|
||||
}
|
||||
});
|
||||
}else{
|
||||
if(is_get_status_novel != true){
|
||||
online_status = 'no_connection';
|
||||
kobold_world_synced = false;
|
||||
updateWorldStatus();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -896,7 +915,8 @@
|
||||
|
||||
var generate_data;
|
||||
if(main_api == 'kobold'){
|
||||
var generate_data = {prompt: finalPromt, gui_settings: true,max_length: amount_gen,temperature: temp, max_context_length: max_context};
|
||||
const use_world_info = Boolean(kobold_world && kobold_world_synced);
|
||||
var generate_data = {prompt: finalPromt, gui_settings: true,max_length: amount_gen,temperature: temp, max_context_length: max_context, use_world_info};
|
||||
if(preset_settings != 'gui'){
|
||||
|
||||
var this_settings = koboldai_settings[koboldai_setting_names[preset_settings]];
|
||||
@@ -937,7 +957,8 @@
|
||||
s4:this_settings.sampler_order[3],
|
||||
s5:this_settings.sampler_order[4],
|
||||
s6:this_settings.sampler_order[5],
|
||||
s7:this_settings.sampler_order[6]
|
||||
s7:this_settings.sampler_order[6],
|
||||
use_world_info: use_world_info,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1199,6 +1220,9 @@
|
||||
$( "#rm_button_characters" ).children("h2").css(deselected_button_style);
|
||||
$( "#rm_button_settings" ).children("h2").css(seleced_button_style);
|
||||
$( "#rm_button_selected_ch" ).children("h2").css(deselected_button_style);
|
||||
|
||||
// Dumb call, but won't need an interval
|
||||
updateWorldStatus();
|
||||
});
|
||||
$( "#rm_button_characters" ).click(function() {
|
||||
selected_button = 'characters';
|
||||
@@ -1504,6 +1528,17 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
if (popup_type === 'world_imported' && imported_world_name) {
|
||||
koboldai_world_names.forEach((item, i) => {
|
||||
if (item === imported_world_name) {
|
||||
$('#world_info').val(i).change();
|
||||
}
|
||||
})
|
||||
imported_world_name = '';
|
||||
}
|
||||
if (popup_type === 'del_world' && kobold_world) {
|
||||
deleteWorldInfo(kobold_world);
|
||||
}
|
||||
if(popup_type == 'new_chat' && this_chid != undefined && menu_type != "create"){//Fix it; New chat doesn't create while open create character menu
|
||||
clearChat();
|
||||
chat.length = 0;
|
||||
@@ -1529,11 +1564,13 @@
|
||||
$("#dialogue_popup_cancel").css("display", "none");
|
||||
break;
|
||||
|
||||
case 'world_imported':
|
||||
case 'new_chat':
|
||||
|
||||
$("#dialogue_popup_ok").css("background-color", "#191b31CC");
|
||||
$("#dialogue_popup_ok").text("Yes");
|
||||
break;
|
||||
case 'del_world':
|
||||
default:
|
||||
$("#dialogue_popup_ok").css("background-color", "#791b31");
|
||||
$("#dialogue_popup_ok").text("Delete");
|
||||
@@ -1829,6 +1866,7 @@
|
||||
is_get_status = true;
|
||||
is_api_button_press = true;
|
||||
getStatus();
|
||||
detectUnitedKobold();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1927,6 +1965,23 @@
|
||||
|
||||
});
|
||||
|
||||
$("#world_info").change(function() {
|
||||
const selectedWorld = $('#world_info').find(":selected").val();
|
||||
kobold_world_synced = false;
|
||||
kobold_sync_failed = false;
|
||||
kobold_world = null;
|
||||
|
||||
if (selectedWorld !== 'None') {
|
||||
const worldIndex = Number(selectedWorld);
|
||||
kobold_world = !isNaN(worldIndex) ? koboldai_world_names[worldIndex] : null;
|
||||
}
|
||||
|
||||
hideWorldEditor();
|
||||
syncKoboldWorldInfo(true);
|
||||
saveSettings();
|
||||
updateWorldStatus();
|
||||
});
|
||||
|
||||
$( "#settings_perset" ).change(function() {
|
||||
|
||||
if($('#settings_perset').find(":selected").val() != 'gui'){
|
||||
@@ -2005,6 +2060,7 @@
|
||||
main_api = 'kobold';
|
||||
$('#max_context_block').css('display', 'block');
|
||||
$('#amount_gen_block').css('display', 'block');
|
||||
$('#world_info_block').css('display', 'flex');
|
||||
}
|
||||
if($('#main_api').find(":selected").val() == 'novel'){
|
||||
$('#kobold_api').css("display", "none");
|
||||
@@ -2012,7 +2068,10 @@
|
||||
main_api = 'novel';
|
||||
$('#max_context_block').css('display', 'none');
|
||||
$('#amount_gen_block').css('display', 'none');
|
||||
$('#world_info_block').css('display', 'none');
|
||||
}
|
||||
|
||||
updateWorldStatus();
|
||||
}
|
||||
async function getUserAvatars(){
|
||||
const response = await fetch("/getuseravatars", {
|
||||
@@ -2037,7 +2096,100 @@
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function updateWorldStatus() {
|
||||
if($('#world_info_block').is(':visible') && kobold_world) {
|
||||
$('#world_info_edit_button').show();
|
||||
$('#world_status').show();
|
||||
|
||||
if (kobold_world_synced) {
|
||||
$("#world_status_indicator").css("background-color", "green");
|
||||
$("#world_status_text").html("Synchronized with KoboldAI")
|
||||
}
|
||||
else {
|
||||
let statusText = online_status === 'no_connection'
|
||||
? "Waiting for connection"
|
||||
: "Synchronizing...";
|
||||
|
||||
if (kobold_sync_failed) {
|
||||
statusText = "Synchronization failed (see console)";
|
||||
}
|
||||
|
||||
$("#world_status_text").html(statusText);
|
||||
$("#world_status_indicator").css("background-color", "red");
|
||||
}
|
||||
|
||||
if (kobold_is_united) {
|
||||
$("#world_status_text").html('<span style="font-size:90%">KoboldAI United detected. WI may not work as intended.<br>If experiencing issues, please select "None".</span>');
|
||||
}
|
||||
} else {
|
||||
$('#world_status').hide();
|
||||
$('#world_info_edit_button').hide();
|
||||
}
|
||||
}
|
||||
|
||||
async function detectUnitedKobold() {
|
||||
if (!api_server || main_api !== 'kobold') {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we can reach Kobold's new ui, then it should be United branch
|
||||
kobold_is_united = false;
|
||||
try {
|
||||
const kobold_united_ui2 = api_server.replace('/api', '/new_ui');
|
||||
const response = await fetch(kobold_united_ui2, { method: 'HEAD'});
|
||||
|
||||
if (response.ok && response.status == 200) {
|
||||
kobold_is_united = true;
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// empty catch
|
||||
}
|
||||
}
|
||||
|
||||
async function syncKoboldWorldInfo(force) {
|
||||
// Don't sync if no world selected or if synced and not forcing
|
||||
if (online_status === 'no_connection' || (!kobold_world && !force) || (kobold_world_synced && !force)) {
|
||||
updateWorldStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch("/synckoboldworld", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ "name": kobold_world })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const syncData = await response.json();
|
||||
|
||||
if (syncData.ok) {
|
||||
kobold_world_synced = true;
|
||||
kobold_sync_failed = false;
|
||||
}
|
||||
|
||||
if (syncData.busy) {
|
||||
// console.log('Sync API is busy. Retrying in 3sec');
|
||||
clearTimeout(timerKoboldSync);
|
||||
timerKoboldSync = setTimeout(() => syncKoboldWorldInfo(force), 3000);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
kobold_sync_failed = true;
|
||||
let responseLog = response.statusText;
|
||||
|
||||
try {
|
||||
var responseBody = await response.text();
|
||||
responseLog += ('\n' + responseBody);
|
||||
} catch {
|
||||
// empty catch
|
||||
}
|
||||
console.error(`Sync API response: ${responseLog}`);
|
||||
}
|
||||
|
||||
updateWorldStatus();
|
||||
}
|
||||
|
||||
$(document).on('input', '#temp', function() {
|
||||
temp = $(this).val();
|
||||
@@ -2181,6 +2333,26 @@
|
||||
koboldai_setting_names = {};
|
||||
koboldai_setting_names = arr_holder;
|
||||
|
||||
// world info settings
|
||||
koboldai_world_names = data.koboldai_world_names?.length ? data.koboldai_world_names : [];
|
||||
|
||||
if(settings.kobold_world != undefined) {
|
||||
if (koboldai_world_names.includes(settings.kobold_world)) {
|
||||
kobold_world = settings.kobold_world;
|
||||
kobold_world_synced = false;
|
||||
kobold_sync_failed = false;
|
||||
}
|
||||
}
|
||||
|
||||
koboldai_world_names.forEach((item, i) => {
|
||||
$('#world_info').append(`<option value='${i}'>${item}</option>`);
|
||||
// preselect world if saved
|
||||
if (item == kobold_world){
|
||||
$('#world_info').val(i).change();
|
||||
}
|
||||
});
|
||||
// end world info settings
|
||||
|
||||
preset_settings = settings.preset_settings;
|
||||
|
||||
temp = settings.temp;
|
||||
@@ -2256,7 +2428,6 @@
|
||||
preset_settings = 'gui';
|
||||
$("#settings_perset option[value=gui]").attr('selected', 'true');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//User
|
||||
@@ -2306,7 +2477,8 @@
|
||||
model_novel: model_novel,
|
||||
temp_novel: temp_novel,
|
||||
rep_pen_novel: rep_pen_novel,
|
||||
rep_pen_size_novel: rep_pen_size_novel
|
||||
rep_pen_size_novel: rep_pen_size_novel,
|
||||
kobold_world: kobold_world,
|
||||
}),
|
||||
beforeSend: function(){
|
||||
|
||||
@@ -2321,7 +2493,8 @@
|
||||
if(type === 'change_name'){
|
||||
location.reload();
|
||||
}
|
||||
|
||||
|
||||
syncKoboldWorldInfo(false);
|
||||
},
|
||||
error: function (jqXHR, exception) {
|
||||
console.log(exception);
|
||||
@@ -2739,6 +2912,376 @@
|
||||
$('#load_select_chat_div').css('display', 'block');
|
||||
|
||||
});
|
||||
|
||||
//**************************WORLD INFO IMPORT EXPORT*************************//
|
||||
$("#world_import_button" ).click(function() {
|
||||
$("#world_import_file").click();
|
||||
});
|
||||
|
||||
$("#world_import_file").on("change", function(e) {
|
||||
var file = e.target.files[0];
|
||||
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ext = file.name.match(/\.(\w+)$/);
|
||||
if (!ext || (ext[1].toLowerCase() !== "json")){
|
||||
return;
|
||||
}
|
||||
|
||||
var formData = new FormData($("#form_world_import").get(0));
|
||||
|
||||
jQuery.ajax({
|
||||
type: 'POST',
|
||||
url: '/importworldinfo',
|
||||
data: formData,
|
||||
beforeSend: () => {},
|
||||
cache: false,
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: function(data){
|
||||
if (data.name) {
|
||||
imported_world_name = data.name;
|
||||
updateWorldInfoList(imported_world_name);
|
||||
}
|
||||
},
|
||||
error: (jqXHR, exception) => {},
|
||||
});
|
||||
|
||||
// Will allow to select the same file twice in a row
|
||||
$('#form_world_import').trigger("reset");
|
||||
});
|
||||
|
||||
async function updateWorldInfoList(importedWorldName) {
|
||||
var result = await fetch('/getsettings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
var data = await result.json();
|
||||
koboldai_world_names = data.koboldai_world_names?.length ? data.koboldai_world_names : [];
|
||||
$('#world_info').find('option[value!="None"]').remove();
|
||||
|
||||
koboldai_world_names.forEach((item, i) => {
|
||||
$('#world_info').append(`<option value='${i}'>${item}</option>`);
|
||||
});
|
||||
|
||||
if (importedWorldName) {
|
||||
const indexOf = koboldai_world_names.indexOf(kobold_world);
|
||||
$('#world_info').val(indexOf);
|
||||
|
||||
popup_type = 'world_imported';
|
||||
callPopup('<h3>World imported successfully! Select it now?</h3>');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function download(content, fileName, contentType) {
|
||||
var a = document.createElement("a");
|
||||
var file = new Blob([content], {type: contentType});
|
||||
a.href = URL.createObjectURL(file);
|
||||
a.download = fileName;
|
||||
a.click();
|
||||
}
|
||||
|
||||
// World Info Editor
|
||||
async function showWorldEditor() {
|
||||
is_world_edit_open = true;
|
||||
$('#world_popup_name').val(kobold_world);
|
||||
$('#world_popup').css('display', 'flex');
|
||||
|
||||
if (kobold_world) {
|
||||
const response = await fetch("/getworldinfo", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: kobold_world })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
kobold_world_data = await response.json();
|
||||
displayWorldEntries(kobold_world_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hideWorldEditor() {
|
||||
is_world_edit_open = false;
|
||||
$('#world_popup').css('display', 'none');
|
||||
syncKoboldWorldInfo(true);
|
||||
}
|
||||
|
||||
function displayWorldEntries(data) {
|
||||
$('#world_popup_entries_list').empty();
|
||||
|
||||
if (!data || !('entries' in data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const entryUid in data.entries) {
|
||||
const entry = data.entries[entryUid];
|
||||
appendWorldEntry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
function appendWorldEntry(entry) {
|
||||
const template = $('#entry_edit_template .world_entry').clone();
|
||||
template.data('uid', entry.uid);
|
||||
|
||||
// key
|
||||
const keyInput = template.find('input[name="key"]');
|
||||
keyInput.data('uid', entry.uid);
|
||||
keyInput.on('input', function () {
|
||||
const uid = $(this).data('uid');
|
||||
const value = $(this).val();
|
||||
kobold_world_data.entries[uid].key = value.split(',').map(x => x.trim()).filter(x => x);
|
||||
saveWorldInfo();
|
||||
});
|
||||
keyInput.val(entry.key.join(',')).trigger('input');
|
||||
|
||||
// keysecondary
|
||||
const keySecondaryInput = template.find('input[name="keysecondary"]');
|
||||
keySecondaryInput.data('uid', entry.uid);
|
||||
keySecondaryInput.on('input', function() {
|
||||
const uid = $(this).data('uid');
|
||||
const value = $(this).val();
|
||||
kobold_world_data.entries[uid].keysecondary = value.split(',').map(x => x.trim()).filter(x => x);
|
||||
saveWorldInfo();
|
||||
});
|
||||
keySecondaryInput.val(entry.keysecondary.join(',')).trigger('input');
|
||||
|
||||
// comment
|
||||
const commentInput = template.find('input[name="comment"]');
|
||||
commentInput.data('uid', entry.uid);
|
||||
commentInput.on('input', function() {
|
||||
const uid = $(this).data('uid');
|
||||
const value = $(this).val();
|
||||
kobold_world_data.entries[uid].comment = value;
|
||||
saveWorldInfo();
|
||||
});
|
||||
commentInput.val(entry.comment).trigger('input');
|
||||
|
||||
// content
|
||||
const contentInput = template.find('textarea[name="content"]');
|
||||
contentInput.data('uid', entry.uid);
|
||||
contentInput.on('input', function() {
|
||||
const uid = $(this).data('uid');
|
||||
const value = $(this).val();
|
||||
kobold_world_data.entries[uid].content = value;
|
||||
saveWorldInfo();
|
||||
|
||||
// count tokens
|
||||
const numberOfTokens = encode(value).length;
|
||||
$(this).closest('.world_entry').find('.world_entry_form_token_counter').html(numberOfTokens);
|
||||
});
|
||||
contentInput.val(entry.content).trigger('input');
|
||||
|
||||
// selective
|
||||
const selectiveInput = template.find('input[name="selective"]')
|
||||
selectiveInput.data('uid', entry.uid);
|
||||
selectiveInput.on('input', function() {
|
||||
const uid = $(this).data('uid');
|
||||
const value = $(this).prop('checked');
|
||||
kobold_world_data.entries[uid].selective = value;
|
||||
saveWorldInfo();
|
||||
});
|
||||
selectiveInput.prop('checked', entry.selective).trigger('input');
|
||||
selectiveInput.siblings('.checkbox_fancy').click(function() {
|
||||
$(this).siblings('input').click();
|
||||
});
|
||||
|
||||
|
||||
// constant
|
||||
const constantInput = template.find('input[name="constant"]')
|
||||
constantInput.data('uid', entry.uid);
|
||||
constantInput.on('input', function() {
|
||||
const uid = $(this).data('uid');
|
||||
const value = $(this).prop('checked');
|
||||
kobold_world_data.entries[uid].constant = value;
|
||||
saveWorldInfo();
|
||||
});
|
||||
constantInput.prop('checked', entry.constant).trigger('input');
|
||||
constantInput.siblings('.checkbox_fancy').click(function() {
|
||||
$(this).siblings('input').click();
|
||||
});
|
||||
|
||||
// display uid
|
||||
template.find('.world_entry_form_uid_value').html(entry.uid);
|
||||
|
||||
// delete button
|
||||
const deleteButton = template.find('input.delete_entry_button');
|
||||
deleteButton.data('uid', entry.uid);
|
||||
deleteButton.on('click', function() {
|
||||
const uid = $(this).data('uid');
|
||||
deleteWorldInfoEntry(uid);
|
||||
$(this).closest('.world_entry').remove();
|
||||
saveWorldInfo();
|
||||
});
|
||||
|
||||
template.appendTo('#world_popup_entries_list');
|
||||
return template;
|
||||
}
|
||||
|
||||
async function deleteWorldInfoEntry(uid) {
|
||||
if (!kobold_world_data || !('entries' in kobold_world_data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
delete kobold_world_data.entries[uid];
|
||||
|
||||
if ('folders' in kobold_world_data) {
|
||||
for (const folderName in kobold_world_data.folders) {
|
||||
const folder = kobold_world_data.folders[folderName]
|
||||
const index = folder.indexOf(Number(uid));
|
||||
|
||||
if (index !== -1) {
|
||||
folder.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createWorldInfoEntry() {
|
||||
const newEntryTemplate = {
|
||||
key: [],
|
||||
keysecondary: [],
|
||||
comment: '',
|
||||
content: '',
|
||||
constant: false,
|
||||
selective: false,
|
||||
};
|
||||
const newUid = getFreeWorldEntryUid();
|
||||
|
||||
if (!Number.isInteger(newUid)) {
|
||||
console.error("Couldn't assign UID to a new entry");
|
||||
return;
|
||||
}
|
||||
|
||||
const newEntry = { uid: newUid, ...newEntryTemplate };
|
||||
kobold_world_data.entries[newUid] = newEntry;
|
||||
|
||||
if ('folders' in kobold_world_data) {
|
||||
if (kobold_world in kobold_world_data.folders && Array.isArray(kobold_world_data.folders)) {
|
||||
kobold_world_data.folders[kobold_world].push(newUid);
|
||||
} else {
|
||||
kobold_world_data.folders[kobold_world] = [newUid];
|
||||
}
|
||||
}
|
||||
|
||||
const entryTemplate = appendWorldEntry(newEntry);
|
||||
entryTemplate.get(0).scrollIntoView({behavior: 'smooth'});
|
||||
}
|
||||
|
||||
async function saveWorldInfo(immediately) {
|
||||
if (!kobold_world || !kobold_world_data) {
|
||||
return;
|
||||
}
|
||||
|
||||
async function _save() {
|
||||
const response = await fetch("/editworldinfo", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: kobold_world, data: kobold_world_data })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
kobold_world_synced = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (immediately) {
|
||||
return await _save();
|
||||
}
|
||||
|
||||
clearTimeout(timerWorldSave);
|
||||
timerWorldSave = setTimeout(async () => await _save(), durationSaveEdit);
|
||||
}
|
||||
|
||||
async function renameWorldInfo() {
|
||||
const oldName = kobold_world;
|
||||
const newName = $('#world_popup_name').val();
|
||||
|
||||
if (oldName === newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
kobold_world = newName;
|
||||
await saveWorldInfo(true);
|
||||
await deleteWorldInfo(oldName, newName);
|
||||
}
|
||||
|
||||
async function deleteWorldInfo(worldInfoName, selectWorldName) {
|
||||
if (!koboldai_world_names.includes(worldInfoName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch("/deleteworldinfo", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: worldInfoName })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
await updateWorldInfoList();
|
||||
|
||||
const selectedIndex = koboldai_world_names.indexOf(selectWorldName);
|
||||
if (selectedIndex !== -1) {
|
||||
$('#world_info').val(selectedIndex).change();
|
||||
}
|
||||
else {
|
||||
$('#world_info').val('None').change();
|
||||
}
|
||||
|
||||
hideWorldEditor();
|
||||
}
|
||||
}
|
||||
|
||||
function getFreeWorldEntryUid() {
|
||||
if (!kobold_world_data || !('entries' in kobold_world_data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const MAX_UID = 1_000_000; // <- should be safe enough :)
|
||||
for (let uid = 0; uid < MAX_UID; uid++) {
|
||||
if (uid in kobold_world_data.entries) {
|
||||
continue;
|
||||
}
|
||||
return uid;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$('#world_info_edit_button').click(() => {
|
||||
is_world_edit_open ? hideWorldEditor() : showWorldEditor();
|
||||
});
|
||||
|
||||
$('#world_popup_export').click(() => {
|
||||
if (kobold_world && kobold_world_data) {
|
||||
const jsonValue = JSON.stringify(kobold_world_data);
|
||||
const fileName = `${kobold_world}.json`;
|
||||
download(jsonValue, fileName, 'application/json');
|
||||
}
|
||||
});
|
||||
|
||||
$('#world_popup_delete').click(() => {
|
||||
popup_type = 'del_world';
|
||||
callPopup('<h3>Delete the World Info?</h3>');
|
||||
});
|
||||
|
||||
$('#world_popup_new').click(() => {
|
||||
createWorldInfoEntry();
|
||||
});
|
||||
|
||||
$('#world_cross').click(() => {
|
||||
hideWorldEditor();
|
||||
});
|
||||
|
||||
$('#world_popup_name_button').click(() => {
|
||||
renameWorldInfo();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<title>Tavern.AI</title>
|
||||
@@ -2796,6 +3339,95 @@
|
||||
<div></div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="world_popup">
|
||||
<div id="world_popup_text">
|
||||
<img id="world_cross" src="img/cross.png">
|
||||
<div id="world_popup_header">
|
||||
<!-- Consider changing logo to something else -->
|
||||
<img src="img/book2.png" id="world_logo">
|
||||
<h3>
|
||||
World Info Editor
|
||||
<span>(<a href="/notes/13" target="_blank">?</a>)</span>
|
||||
</h3>
|
||||
<div class="world_popup_expander"> </div>
|
||||
<form id="form_rename_world" action="javascript:void(null);" method="post" enctype="multipart/form-data">
|
||||
<input id="world_popup_name" name="world_popup_name" class="text_pole" maxlength="99" size="32" value="" autocomplete="off">
|
||||
<input id="world_popup_name_button" type="submit" value="Rename">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="world_popup_entries_list">
|
||||
</div>
|
||||
|
||||
<div id="world_popup_bottom_holder">
|
||||
<div id="world_popup_new" class="menu_button">New Entry</div>
|
||||
<div class="world_popup_expander"> </div>
|
||||
<div id="world_popup_export" class="menu_button">Export</div>
|
||||
<div id="world_popup_delete" class="menu_button">Delete World</div>
|
||||
</div>
|
||||
|
||||
<div id="entry_edit_template">
|
||||
<div class="world_entry">
|
||||
<form class="world_entry_form">
|
||||
<div class="world_entry_form_control">
|
||||
<label for="key">
|
||||
<h4>Key</h4>
|
||||
<h5>Comma-separated list of keywords (e.g: foo,bar).</h5>
|
||||
</label>
|
||||
<input class="text_pole" type="text" name="key" placeholder=""/>
|
||||
</div>
|
||||
<div class="world_entry_form_control">
|
||||
<label for="keysecondary">
|
||||
<h4>Secondary Key</h4>
|
||||
<h5>Comma-separated list of additional keywords (e.g: foo,bar).</h5>
|
||||
</label>
|
||||
<input class="text_pole" type="text" name="keysecondary" placeholder=""/>
|
||||
</div>
|
||||
<div class="world_entry_form_control">
|
||||
<label for="comment">
|
||||
<h4>Comment</h4>
|
||||
<h5>Optional comment (doesn't affect the AI).</h5>
|
||||
</label>
|
||||
<input class="text_pole" type="text" name="comment" placeholder=""/>
|
||||
</div>
|
||||
<div class="world_entry_form_control">
|
||||
<label for="content">
|
||||
<h4>Content</h4>
|
||||
<h5>Text that will be inserted to the prompt upon activation.</h5>
|
||||
</label>
|
||||
<textarea class="text_pole" name="content" rows="4" placeholder=""></textarea>
|
||||
</div>
|
||||
<div class="world_entry_form_control world_entry_form_horizontal">
|
||||
<label class="checkbox" for="constant">
|
||||
<input type="checkbox" name="constant" />
|
||||
<span class="checkbox_fancy"></span>
|
||||
<h4>Constant</h4>
|
||||
</label>
|
||||
<label class="checkbox" for="selective">
|
||||
<input type="checkbox" name="selective" />
|
||||
<span class="checkbox_fancy"></span>
|
||||
<h4>Selective</h4>
|
||||
</label>
|
||||
<span class="world_popup_expander"> </span>
|
||||
<h5 class="world_entry_form_uid">
|
||||
UID:
|
||||
|
||||
<span class="world_entry_form_uid_value"></span>
|
||||
</h5>
|
||||
<h5 class="world_entry_form_tokens">
|
||||
Tokens used:
|
||||
|
||||
<span class="world_entry_form_token_counter">0</span>
|
||||
</h5>
|
||||
<input class="delete_entry_button" type="button" value="Delete Entry" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="shadow_select_chat_popup">
|
||||
<div id="select_chat_popup">
|
||||
<div id="select_chat_popup_text">
|
||||
@@ -2946,6 +3578,25 @@
|
||||
<h4>Repetition Penalty Range</h4><h5 id="rep_pen_size_counter">select</h5>
|
||||
<input type="range" id="rep_pen_size" name="volume" min="0" max="2048" step="1">
|
||||
</div>
|
||||
<div>
|
||||
<h4 id="world_info_block">
|
||||
<span>World Info</span>
|
||||
<div id="world_import_button" class="right_menu_button"><h2>+Import</h2></div>
|
||||
</h4>
|
||||
<h5>How to use (<a href="/notes/13" target="_blank">?</a>)</h5>
|
||||
<div id="rm_world_import" class="right_menu" style="display: none;">
|
||||
<form id="form_world_import" action="javascript:void(null);" method="post" enctype="multipart/form-data">
|
||||
<input type="file" id="world_import_file" accept=".json" name="avatar">
|
||||
</form>
|
||||
</div>
|
||||
<select id="world_info" class="option_select_right_menu">
|
||||
<option value="None">None</option>
|
||||
</select>
|
||||
<input id="world_info_edit_button" type="button" value="Details">
|
||||
<div id="world_status">
|
||||
<div id="world_status_indicator"></div><div id="world_status_text"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="novel_api" style="display: none;position: relative;">
|
||||
<div style="position: absolute; right:152px; top:-25px; opacity:0.25;"><a href="https://novelai.net/" target="_blank"><img src="img/novelai.png" style="width:auto;height:22px;"></a></div>
|
||||
|
43
public/notes/13.html
Normal file
43
public/notes/13.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>World Info</title>
|
||||
<link rel="stylesheet" href="/css/notes.css">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body>
|
||||
<div id="main">
|
||||
<div id="content">
|
||||
<h2>World Info</h2>
|
||||
<h4>World Info enhances AI's understanding of the details in your world.</h4>
|
||||
<p>It functions like a dynamic dictionary that only inserts relevant information from World Info entries when keywords associated with the entries are present in the message text.</p>
|
||||
<p>The KoboldAI engine activates and seamlessly integrates the appropriate lore into the prompt, providing background information to the AI.</p>
|
||||
<p><i>It is important to note that while World Info helps guide the AI towards your desired lore, it does not guarantee its appearance in the generated output messages.</i></p>
|
||||
|
||||
<h3>Pro Tips</h3>
|
||||
<ul>
|
||||
<li>The AI does not insert keywords into context, so each World Info entry should be a comprehensive, standalone description.</li>
|
||||
<li>To create a rich and detailed world lore, entries can be interlinked and reference one another.</li>
|
||||
<li>To conserve tokens, it is advisable to keep entry contents concise, with a general recommended limit of 50 tokens per entry.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Entry Fields Explained</h3>
|
||||
<dl>
|
||||
<dt>Key</dt>
|
||||
<dd>A list of keywords that trigger the activation of a World Info entry.</dd>
|
||||
<dt>Secondary Key</dt>
|
||||
<dd>A list of supplementary keywords that are used in conjunction with the main keywords (see <a href="#Selective">Selective</a>).</dd>
|
||||
<dt>Content</dt>
|
||||
<dd>The text that is inserted into the prompt upon entry activation.</dd>
|
||||
<dt>Comment</dt>
|
||||
<dd>A supplemental text comment for the your convenience, which is not utilized by the AI.</dd>
|
||||
<dt>Constant</dt>
|
||||
<dd>If enabled, the entry would always be present in the prompt. <em>Currently, this is unsupported!</em></dd>
|
||||
<dt id="Selective">Selective</dt>
|
||||
<dd>If enabled, the entry would only be inserted when both a Key <b>AND</b> a Secondary Key have been activated. <em>Currently, this is unsupported!</em></dd>
|
||||
</dl>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
228
public/style.css
228
public/style.css
@@ -953,6 +953,234 @@ input[type=button] {
|
||||
margin-left: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#world_info {
|
||||
margin-bottom: 12px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
#world_info_block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#world_status{
|
||||
opacity: 0.5;
|
||||
margin-top: 2px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
#world_status_indicator{
|
||||
border-radius: 7px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background-color: red;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#world_status_text {
|
||||
margin-left: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#world_import_button {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#world_import_button h2 {
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
margin-left: 1rem;
|
||||
font-size: 16px;
|
||||
color: rgb(188, 193, 200, 0.5);
|
||||
}
|
||||
|
||||
#world_info_edit_button {
|
||||
cursor: pointer;
|
||||
color: #ffffffaa;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
#world_popup {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
max-width: 800px;
|
||||
height: 83vh;
|
||||
position: absolute;
|
||||
z-index: 2060;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-top: 0px;
|
||||
box-shadow: 0 0 2px rgba(200, 200, 200, 0.1);
|
||||
padding: 4px 36px;
|
||||
background: #191b31F5;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
#world_popup_bottom_holder {
|
||||
padding: 1rem 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
#world_popup_bottom_holder div {
|
||||
margin-left: 1rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
#entry_edit_template {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.world_entry:not(:last-child)::after {
|
||||
margin-top: 1rem;
|
||||
height: 1px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
content: '';
|
||||
background-image: linear-gradient(270deg, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0));
|
||||
}
|
||||
|
||||
#world_popup_header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
#form_rename_world {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 20px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
#form_rename_world input[type="submit"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#form_rename_world input:not(:last-child) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#world_popup_header h5 {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.world_popup_expander {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#world_popup_entries_list {
|
||||
flex-grow: 1;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
#world_popup_entries_list:empty {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#world_popup_entries_list:empty::before {
|
||||
content: 'No entries exist. Try creating one!';
|
||||
font-size: 1.5rem;
|
||||
font-weight: bolder;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.world_entry_form_control {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.world_entry_form_control label {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
|
||||
.world_entry_form_control label h4 {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.world_entry_form_control label h5 {
|
||||
margin-top: 3px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.world_entry_form_control textarea {
|
||||
height: auto;
|
||||
width: auto;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.world_entry_form_control.world_entry_form_horizontal {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.world_entry_form_control input[type=button] {
|
||||
opacity: 0.7;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.world_entry_form_horizontal h5 {
|
||||
margin: 0 1rem;
|
||||
}
|
||||
|
||||
.world_entry_form_control .checkbox h4 {
|
||||
margin-left: 0.5rem;
|
||||
margin-top: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.world_entry_form_control .checkbox:not(:first-child) {
|
||||
margin-left: 2rem;
|
||||
}
|
||||
|
||||
#world_cross {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 15px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
#world_logo {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
#world_popup h4 a, #world_popup h5 a, #world_popup h3 a {
|
||||
color: #936f4a;
|
||||
}
|
||||
|
||||
#world_popup h5 a:hover, #world_popup h4 a:hover, #world_popup h4 a:hover a {
|
||||
color: #998e6b;
|
||||
}
|
||||
|
||||
#world_popup h5 {
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
.del_checkbox{
|
||||
display: none;
|
||||
opacity: 0.5;
|
||||
|
397
server.js
397
server.js
@@ -48,7 +48,8 @@ var is_colab = false;
|
||||
|
||||
const jsonParser = express.json({limit: '100mb'});
|
||||
const urlencodedParser = express.urlencoded({extended: true, limit: '100mb'});
|
||||
|
||||
const baseRequestArgs = { headers: { "Content-Type": "application/json" } };
|
||||
const directories = { worlds: 'public/KoboldAI Worlds/' };
|
||||
|
||||
app.use(function (req, res, next) { //Security
|
||||
const clientIp = req.connection.remoteAddress.split(':').pop();
|
||||
@@ -128,7 +129,7 @@ app.post("/generate", jsonParser, function(request, response_generate = response
|
||||
//console.log(request.body.prompt);
|
||||
//const dataJson = JSON.parse(request.body);
|
||||
request_promt = request.body.prompt;
|
||||
|
||||
|
||||
//console.log(request.body);
|
||||
var this_settings = { prompt: request_promt,
|
||||
use_story:false,
|
||||
@@ -146,7 +147,7 @@ app.post("/generate", jsonParser, function(request, response_generate = response
|
||||
use_story:false,
|
||||
use_memory:false,
|
||||
use_authors_note:false,
|
||||
use_world_info:false,
|
||||
use_world_info:!!request.body.use_world_info,
|
||||
max_context_length: request.body.max_context_length,
|
||||
max_length: request.body.max_length,
|
||||
rep_pen: request.body.rep_pen,
|
||||
@@ -629,6 +630,12 @@ app.post('/getsettings', jsonParser, (request, response) => { //Wintermute's cod
|
||||
new Date(fs.statSync(`public/KoboldAI Settings/${a}`).mtime)
|
||||
);
|
||||
|
||||
const worldFiles = fs
|
||||
.readdirSync(directories.worlds)
|
||||
.filter(file => path.extname(file).toLowerCase() === '.json')
|
||||
.sort((a, b) => a < b);
|
||||
const koboldai_world_names = worldFiles.map(item => path.parse(item).name);
|
||||
|
||||
files.forEach(item => {
|
||||
const file = fs.readFileSync(
|
||||
`public/KoboldAI Settings/${item}`,
|
||||
@@ -671,11 +678,64 @@ app.post('/getsettings', jsonParser, (request, response) => { //Wintermute's cod
|
||||
settings,
|
||||
koboldai_settings,
|
||||
koboldai_setting_names,
|
||||
koboldai_world_names,
|
||||
novelai_settings,
|
||||
novelai_setting_names
|
||||
});
|
||||
});
|
||||
|
||||
// Work around to disable parallel requests to endpoint
|
||||
let kobold_world_sync_busy = false;
|
||||
|
||||
app.post('/synckoboldworld', jsonParser, async (request, response) => {
|
||||
if(!request.body) return response.sendStatus(400);
|
||||
|
||||
if (!api_server || kobold_world_sync_busy) {
|
||||
response.send({ busy: true });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
kobold_world_sync_busy = true;
|
||||
const worldName = request.body.name;
|
||||
await synchronizeKoboldWorldInfo(worldName);
|
||||
response.send({ ok: true });
|
||||
} catch (err) {
|
||||
var message = JSON.stringify(err);
|
||||
console.error(`Error during world synchronization: ${message}`);
|
||||
response.status(500).send(message);
|
||||
} finally {
|
||||
kobold_world_sync_busy = false;
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/getworldinfo', jsonParser, async (request, response) => {
|
||||
if (!request.body?.name) {
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
|
||||
const file = readWorldInfoFile(request.body.name);
|
||||
|
||||
return response.send(file.tavernWorldInfo);
|
||||
});
|
||||
|
||||
app.post('/deleteworldinfo', jsonParser, async (request, response) => {
|
||||
if (!request.body?.name) {
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
|
||||
const worldInfoName = request.body.name;
|
||||
const filename = `${worldInfoName}.json`;
|
||||
const pathToWorldInfo = path.join(directories.worlds, filename);
|
||||
|
||||
if (!fs.existsSync(pathToWorldInfo)) {
|
||||
throw new Error(`World info file ${filename} doesn't exist.`);
|
||||
}
|
||||
|
||||
fs.rmSync(pathToWorldInfo);
|
||||
|
||||
return response.sendStatus(200);
|
||||
});
|
||||
|
||||
function getCharaterFile(directories,response,i){ //old need del
|
||||
if(directories.length > i){
|
||||
@@ -1038,10 +1098,341 @@ app.post("/importchat", urlencodedParser, function(request, response){
|
||||
|
||||
});
|
||||
|
||||
app.post('/importworldinfo', urlencodedParser, (request, response) => {
|
||||
if(!request.file) return response.sendStatus(400);
|
||||
|
||||
const filename = request.file.originalname;
|
||||
|
||||
if (path.parse(filename).ext.toLowerCase() !== '.json') {
|
||||
return response.status(400).send('Only JSON files are supported.')
|
||||
}
|
||||
|
||||
const pathToUpload = path.join('./uploads/' + request.file.filename);
|
||||
const fileContents = fs.readFileSync(pathToUpload, 'utf8');
|
||||
|
||||
try {
|
||||
const worldContent = JSON.parse(fileContents);
|
||||
if (!('entries' in worldContent)) {
|
||||
throw new Error('File must contain a world info entries list');
|
||||
}
|
||||
} catch (err) {
|
||||
return response.status(400).send('Is not a valid world info file');
|
||||
}
|
||||
|
||||
const pathToNewFile = path.join(directories.worlds, filename);
|
||||
const worldName = path.parse(pathToNewFile).name;
|
||||
|
||||
if (!worldName) {
|
||||
return response.status(400).send('World file must have a name');
|
||||
}
|
||||
|
||||
fs.writeFileSync(pathToNewFile, fileContents);
|
||||
return response.send({ name: worldName });
|
||||
});
|
||||
|
||||
app.post('/editworldinfo', jsonParser, (request, response) => {
|
||||
if (!request.body) {
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
|
||||
if (!request.body.name) {
|
||||
return response.status(400).send('World file must have a name');
|
||||
}
|
||||
|
||||
try {
|
||||
if (!('entries' in request.body.data)) {
|
||||
throw new Error('World info must contain an entries list');
|
||||
}
|
||||
} catch (err) {
|
||||
return response.status(400).send('Is not a valid world info file');
|
||||
}
|
||||
|
||||
const filename = `${request.body.name}.json`;
|
||||
const pathToFile = path.join(directories.worlds, filename);
|
||||
|
||||
fs.writeFileSync(pathToFile, JSON.stringify(request.body.data));
|
||||
|
||||
return response.send({ ok: true });
|
||||
});
|
||||
|
||||
function findTavernWorldEntry(info, key, content) {
|
||||
for (const entryId in info.entries) {
|
||||
const entry = info.entries[entryId];
|
||||
const keyString = entry.key.join(',');
|
||||
|
||||
if (keyString === key && entry.content === content) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function synchronizeKoboldWorldInfo(worldInfoName) {
|
||||
const { koboldFolderName, tavernWorldInfo } = readWorldInfoFile(worldInfoName);
|
||||
|
||||
// Get existing world info
|
||||
const koboldWorldInfo = await getAsync(`${api_server}/v1/world_info`, baseRequestArgs);
|
||||
|
||||
// Validate kobold world info
|
||||
let {
|
||||
shouldCreateWorld,
|
||||
koboldWorldUid,
|
||||
tavernEntriesToCreate,
|
||||
koboldEntriesToDelete,
|
||||
koboldFoldersToDelete,
|
||||
} = await validateKoboldWorldInfo(koboldFolderName, koboldWorldInfo, tavernWorldInfo);
|
||||
|
||||
// Create folder if not already exists
|
||||
if (koboldFolderName && shouldCreateWorld) {
|
||||
koboldWorldUid = await createKoboldFolder(koboldFolderName, tavernEntriesToCreate, tavernWorldInfo);
|
||||
}
|
||||
|
||||
await deleteKoboldFolders(koboldFoldersToDelete);
|
||||
await deleteKoboldEntries(koboldEntriesToDelete);
|
||||
await createTavernEntries(tavernEntriesToCreate, koboldWorldUid, tavernWorldInfo);
|
||||
}
|
||||
|
||||
function readWorldInfoFile(worldInfoName) {
|
||||
if (!worldInfoName) {
|
||||
return { koboldFolderName: null, tavernWorldInfo: { entries: {}, folders: {} }};
|
||||
}
|
||||
|
||||
const koboldFolderName = getKoboldWorldInfoName(worldInfoName);
|
||||
const filename = `${worldInfoName}.json`;
|
||||
const pathToWorldInfo = path.join(directories.worlds, filename);
|
||||
|
||||
if (!fs.existsSync(pathToWorldInfo)) {
|
||||
throw new Error(`World info file ${filename} doesn't exist.`);
|
||||
}
|
||||
|
||||
const tavernWorldInfoText = fs.readFileSync(pathToWorldInfo, 'utf8');
|
||||
const tavernWorldInfo = JSON.parse(tavernWorldInfoText);
|
||||
return { koboldFolderName, tavernWorldInfo };
|
||||
}
|
||||
|
||||
async function createKoboldFolder(koboldFolderName, tavernEntriesToCreate, tavernWorldInfo) {
|
||||
const createdFolder = await postAsync(`${api_server}/v1/world_info/folders`, { data: {}, ...baseRequestArgs });
|
||||
const koboldWorldUid = createdFolder.uid;
|
||||
|
||||
// Set a name so we could find the folder later
|
||||
const setNameArgs = { data: { value: koboldFolderName }, ...baseRequestArgs };
|
||||
await putAsync(`${api_server}/v1/world_info/folders/${koboldWorldUid}/name`, setNameArgs);
|
||||
|
||||
// Create all world info entries
|
||||
tavernEntriesToCreate.push(...Object.keys(tavernWorldInfo.entries));
|
||||
return koboldWorldUid;
|
||||
}
|
||||
|
||||
async function createTavernEntries(tavernEntriesToCreate, koboldWorldUid, tavernWorldInfo) {
|
||||
if (tavernEntriesToCreate.length && koboldWorldUid) {
|
||||
for (const tavernUid of tavernEntriesToCreate) {
|
||||
try {
|
||||
const tavernEntry = tavernWorldInfo.entries[tavernUid];
|
||||
const koboldEntry = await postAsync(`${api_server}/v1/world_info/folders/${koboldWorldUid}`, { data: {}, ...baseRequestArgs });
|
||||
await setKoboldEntryData(tavernEntry, koboldEntry);
|
||||
} catch (err) {
|
||||
console.error(`Couldn't create Kobold world info entry, tavernUid=${tavernUid}. Skipping...`);
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteKoboldEntries(koboldEntriesToDelete) {
|
||||
if (koboldEntriesToDelete.length) {
|
||||
for (const uid of koboldEntriesToDelete) {
|
||||
try {
|
||||
await deleteAsync(`${api_server}/v1/world_info/${uid}`);
|
||||
} catch (err) {
|
||||
console.error(`Couldn't delete Kobold world info entry, uid=${uid}. Skipping...`);
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteKoboldFolders(koboldFoldersToDelete) {
|
||||
if (koboldFoldersToDelete.length) {
|
||||
for (const uid of koboldFoldersToDelete) {
|
||||
try {
|
||||
await deleteAsync(api_server + `/v1/world_info/folders/${uid}`, baseRequestArgs);
|
||||
} catch (err) {
|
||||
console.error(`Couldn't delete Kobold world info folder, uid=${uid}. Skipping...`);
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function setKoboldEntryData(tavernEntry, koboldEntry) {
|
||||
// 1. Set primary key
|
||||
if (tavernEntry.key?.length) {
|
||||
const keyArgs = { data: { value: tavernEntry.key.join(',') }, ...baseRequestArgs };
|
||||
await putAsync(`${api_server}/v1/world_info/${koboldEntry.uid}/key`, keyArgs);
|
||||
}
|
||||
|
||||
// 2. Set secondary key
|
||||
if (tavernEntry.keysecondary?.length) {
|
||||
const keySecondaryArgs = { data: { value: tavernEntry.keysecondary.join(',') }, ...baseRequestArgs };
|
||||
await putAsync(`${api_server}/v1/world_info/${koboldEntry.uid}/keysecondary`, keySecondaryArgs);
|
||||
}
|
||||
|
||||
// 3. Set content
|
||||
if (tavernEntry.content) {
|
||||
const contentArgs = { data: { value: tavernEntry.content }, ...baseRequestArgs };
|
||||
await putAsync(`${api_server}/v1/world_info/${koboldEntry.uid}/content`, contentArgs);
|
||||
}
|
||||
|
||||
// 4. Set comment
|
||||
if (tavernEntry.comment) {
|
||||
const commentArgs = { data: { value: tavernEntry.comment }, ...baseRequestArgs };
|
||||
await putAsync(`${api_server}/v1/world_info/${koboldEntry.uid}/comment`, commentArgs);
|
||||
};
|
||||
|
||||
/* Can't set these via API due to bug in Kobold)
|
||||
// 5. Set constant flag
|
||||
if (tavernEntry.constant) {
|
||||
const constantArgs = { data: { value: tavernEntry.constant.toString() }, ...baseRequestArgs };
|
||||
await putToPromise(`${api_server}/v1/world_info/${koboldEntry.uid}/constant`, constantArgs);
|
||||
|
||||
}
|
||||
// 6. Set selective flag
|
||||
if (tavernEntry.selective) {
|
||||
const selectiveArgs = { data: { value: tavernEntry.selective.toString() }, ...baseRequestArgs };
|
||||
await putToPromise(`${api_server}/v1/world_info/${koboldEntry.uid}/selective`, selectiveArgs);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
async function validateKoboldWorldInfo(koboldFolderName, koboldWorldInfo, tavernWorldInfo) {
|
||||
let shouldCreateWorld = true;
|
||||
let koboldWorldUid = null;
|
||||
|
||||
const koboldEntriesToDelete = []; // KoboldUIDs
|
||||
const koboldFoldersToDelete = []; // KoboldUIDs
|
||||
const tavernEntriesToCreate = []; // TavernUIDs
|
||||
|
||||
if (koboldWorldInfo?.folders?.length) {
|
||||
let existingFolderAlreadyFound = false;
|
||||
|
||||
for (const folder of koboldWorldInfo.folders) {
|
||||
// Don't care about non-Tavern folders
|
||||
if (!isTavernKoboldWorldInfo(folder.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Other Tavern folders should be deleted (including dupes). If folder name selected is null, then delete anyway to clean-up
|
||||
if (!koboldFolderName || folder.name !== koboldFolderName || existingFolderAlreadyFound) {
|
||||
koboldFoldersToDelete.push(folder.uid);
|
||||
// Should also delete all entries in folder otherwise they will be detached
|
||||
if (Array.isArray(folder.entries)) {
|
||||
koboldEntriesToDelete.push(...folder.entries.map(entry => entry.uid));
|
||||
}
|
||||
}
|
||||
|
||||
// Validate existing entries in Kobold world
|
||||
if (folder.name === koboldFolderName) {
|
||||
existingFolderAlreadyFound = true;
|
||||
shouldCreateWorld = false;
|
||||
koboldWorldUid = folder.uid;
|
||||
if (folder.entries?.length) {
|
||||
const foundTavernEntries = [];
|
||||
for (const koboldEntry of folder.entries) {
|
||||
const tavernEntry = findTavernWorldEntry(tavernWorldInfo, koboldEntry.key, koboldEntry.content);
|
||||
|
||||
if (tavernEntry) {
|
||||
foundTavernEntries.push(tavernEntry.uid);
|
||||
if (isEntryOutOfSync(tavernEntry, koboldEntry)) {
|
||||
// Entry is out of sync. Should be recreated
|
||||
koboldEntriesToDelete.push(koboldEntry.uid);
|
||||
tavernEntriesToCreate.push(tavernEntry.uid);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// We don't have that entry in our world. It should be deleted
|
||||
koboldEntriesToDelete.push(koboldEntry.uid);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if every tavern entry was found in kobold world
|
||||
// BTW. Entries is an object, not an array!
|
||||
for (const tavernEntryUid in tavernWorldInfo.entries) {
|
||||
const tavernEntry = tavernWorldInfo.entries[tavernEntryUid];
|
||||
if (!foundTavernEntries.includes(tavernEntry.uid)) {
|
||||
tavernEntriesToCreate.push(tavernEntry.uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { shouldCreateWorld, koboldWorldUid, tavernEntriesToCreate, koboldEntriesToDelete, koboldFoldersToDelete };
|
||||
}
|
||||
|
||||
function isEntryOutOfSync(tavernEntry, koboldEntry) {
|
||||
return tavernEntry.content !== koboldEntry.content ||
|
||||
tavernEntry.comment !== koboldEntry.comment ||
|
||||
tavernEntry.selective !== koboldEntry.selective ||
|
||||
tavernEntry.constant !== koboldEntry.constant ||
|
||||
tavernEntry.key.join(',') !== koboldEntry.key ||
|
||||
tavernEntry.keysecondary.join(',') !== koboldEntry.keysecondary;
|
||||
}
|
||||
|
||||
// ** REST CLIENT ASYNC WRAPPERS **
|
||||
function deleteAsync(url, args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
client.delete(url, args, (data, response) => {
|
||||
if (response.statusCode >= 400) {
|
||||
reject(data);
|
||||
}
|
||||
resolve(data);
|
||||
}).on('error', e => reject(e));
|
||||
})
|
||||
}
|
||||
|
||||
function putAsync(url, args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
client.put(url, args, (data, response) => {
|
||||
if (response.statusCode >= 400) {
|
||||
reject(data);
|
||||
}
|
||||
resolve(data);
|
||||
}).on('error', e => reject(e));
|
||||
})
|
||||
}
|
||||
|
||||
function postAsync(url, args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
client.post(url, args, (data, response) => {
|
||||
if (response.statusCode >= 400) {
|
||||
reject(data);
|
||||
}
|
||||
resolve(data);
|
||||
}).on('error', e => reject(e));
|
||||
})
|
||||
}
|
||||
|
||||
function getAsync(url, args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
client.get(url, args, (data, response) => {
|
||||
if (response.statusCode >= 400) {
|
||||
reject(data);
|
||||
}
|
||||
resolve(data);
|
||||
}).on('error', e => reject(e));
|
||||
})
|
||||
}
|
||||
// ** END **
|
||||
|
||||
function getKoboldWorldInfoName(worldInfoName) {
|
||||
return worldInfoName ? `TavernAI_${worldInfoName}_WI` : null;
|
||||
}
|
||||
|
||||
function isTavernKoboldWorldInfo(folderName) {
|
||||
return /^TavernAI_(.*)_WI$/.test(folderName);
|
||||
}
|
||||
|
||||
|
||||
app.listen(server_port, function() {
|
||||
|
Reference in New Issue
Block a user