diff --git a/public/index.html b/public/index.html index 89457f2be..e5d7c6c46 100644 --- a/public/index.html +++ b/public/index.html @@ -69,6 +69,7 @@ var timerSaveEdit; var timerKoboldSync; + var timerWorldSave; var durationSaveEdit = 200; //animation right menu var animation_rm_duration = 200; @@ -109,6 +110,7 @@ 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; @@ -2133,11 +2135,16 @@ // If we can reach Kobold's new ui, then it should be United branch kobold_is_united = false; - const kobold_united_ui2 = api_server.replace('/api', '/new_ui'); - const response = await fetch(kobold_united_ui2, { method: 'HEAD'}); + 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; + if (response.ok && response.status == 200) { + kobold_is_united = true; + } + } + catch { + // empty catch } } @@ -2927,7 +2934,7 @@ jQuery.ajax({ type: 'POST', - url: '/importworld', + url: '/importworldinfo', data: formData, beforeSend: () => {}, cache: false, @@ -2964,7 +2971,7 @@ if (importedWorldName) { const indexOf = koboldai_world_names.indexOf(kobold_world); - $(`#world_info`).val(indexOf); + $('#world_info').val(indexOf); popup_type = 'world_imported'; callPopup('

World imported successfully! Select it now?

'); @@ -2983,7 +2990,7 @@ // World Info Editor async function showWorldEditor() { is_world_edit_open = true; - $('#world_text_content').val(''); + $('#world_popup_name').val(kobold_world); $('#world_popup').css('display', 'flex'); if (kobold_world) { @@ -2994,8 +3001,8 @@ }); if (response.ok) { - const worldInfoData = await response.text(); - $('#world_text_content').val(worldInfoData); + kobold_world_data = await response.json(); + displayWorldEntries(kobold_world_data); } } } @@ -3003,10 +3010,209 @@ function hideWorldEditor() { is_world_edit_open = false; $('#world_popup').css('display', 'none'); - $('#world_text_content').val(''); + syncKoboldWorldInfo(true); } - async function deleteWorldInfo(worldInfoName) { + 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; } @@ -3019,18 +3225,42 @@ if (response.ok) { await updateWorldInfoList(); - $('#world_info').val('None').change(); + + 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(() => { - const jsonValue = $('#world_text_content').val(); - if (kobold_world && jsonValue) { + if (kobold_world && kobold_world_data) { + const jsonValue = JSON.stringify(kobold_world_data); const fileName = `${kobold_world}.json`; download(jsonValue, fileName, 'application/json'); } @@ -3041,9 +3271,17 @@ callPopup('

Delete the World Info?

'); }); + $('#world_popup_new').click(() => { + createWorldInfoEntry(); + }); + $('#world_cross').click(() => { hideWorldEditor(); }); + + $('#world_popup_name_button').click(() => { + renameWorldInfo(); + }); });      Tavern.AI @@ -3105,20 +3343,88 @@
-
- - -

World Info creation

+
+ + +

+ World Info Editor + (?) +

+
 
+
+ + +
- - -

File content (read-only)

- - + +
+
+
+ +
 
- + +
+ +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +   +
+ UID: +   + +
+
+ Tokens used: +   + 0 +
+ +
+
+
diff --git a/public/style.css b/public/style.css index e9ed5752f..0d16b4257 100644 --- a/public/style.css +++ b/public/style.css @@ -1012,7 +1012,6 @@ input[type=button] { height: 83vh; position: absolute; z-index: 2060; - background-color: blue; margin-left: auto; margin-right: auto; left: 0; @@ -1024,11 +1023,6 @@ input[type=button] { border-radius: 1px; } -#world_text_content { - margin: 0; - flex-grow: 1; -} - #world_popup_bottom_holder { padding: 1rem 0; display: flex; @@ -1040,6 +1034,123 @@ input[type=button] { 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 { @@ -1052,19 +1163,17 @@ input[type=button] { opacity: 0.6; } -#world_popup h5 a { +#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 { - color: #998e6b; -} - -#world_popup h4 a { - color: #936f4a; -} - -#world_popup h4 a:hover { +#world_popup h5 a:hover, #world_popup h4 a:hover, #world_popup h4 a:hover a { color: #998e6b; } diff --git a/server.js b/server.js index 2286739a9..0776bbb76 100644 --- a/server.js +++ b/server.js @@ -1098,7 +1098,7 @@ app.post("/importchat", urlencodedParser, function(request, response){ }); -app.post('/importworld', urlencodedParser, (request, response) => { +app.post('/importworldinfo', urlencodedParser, (request, response) => { if(!request.file) return response.sendStatus(400); const filename = request.file.originalname; @@ -1130,12 +1130,37 @@ app.post('/importworld', urlencodedParser, (request, response) => { return response.send({ name: worldName }); }); -function findTavernWorldEntry(info, key) { +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) { + if (keyString === key && entry.content === content) { return entry; } } @@ -1300,6 +1325,10 @@ async function validateKoboldWorldInfo(koboldFolderName, koboldWorldInfo, tavern // 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 @@ -1310,7 +1339,7 @@ async function validateKoboldWorldInfo(koboldFolderName, koboldWorldInfo, tavern if (folder.entries?.length) { const foundTavernEntries = []; for (const koboldEntry of folder.entries) { - const tavernEntry = findTavernWorldEntry(tavernWorldInfo, koboldEntry.key); + const tavernEntry = findTavernWorldEntry(tavernWorldInfo, koboldEntry.key, koboldEntry.content); if (tavernEntry) { foundTavernEntries.push(tavernEntry.uid); @@ -1348,7 +1377,7 @@ function isEntryOutOfSync(tavernEntry, koboldEntry) { tavernEntry.selective !== koboldEntry.selective || tavernEntry.constant !== koboldEntry.constant || tavernEntry.key.join(',') !== koboldEntry.key || - tavernEntry.keysecondary(',') !== koboldEntry.keysecondary; + tavernEntry.keysecondary.join(',') !== koboldEntry.keysecondary; } // ** REST CLIENT ASYNC WRAPPERS **