mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Global world info always involved some kind of merging leading to extremely large lorebook files that took a long time to import. This commit adds the ability to select more than one world info file and they will be merged together along with character world info. In short, multiple worlds can be meshed together to further contribute to context. You can also use this for world "DLCs" of sorts. Let's say someone else has more information to add regarding a world, but doesn't want to use a large world file. The JSONs can now be merged. Signed-off-by: kingbri <bdashore3@proton.me>
1222 lines
40 KiB
JavaScript
1222 lines
40 KiB
JavaScript
import { saveSettings, callPopup, substituteParams, getTokenCount, getRequestHeaders, chat_metadata, this_chid, characters } from "../script.js";
|
||
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, delay } from "./utils.js";
|
||
import { getContext } from "./extensions.js";
|
||
import { metadata_keys, shouldWIAddPrompt } from "./extensions/floating-prompt/index.js";
|
||
import { registerSlashCommand } from "./slash-commands.js";
|
||
import { deviceInfo } from "./RossAscends-mods.js";
|
||
|
||
export {
|
||
world_info,
|
||
world_info_budget,
|
||
world_info_depth,
|
||
world_info_recursive,
|
||
world_info_case_sensitive,
|
||
world_info_match_whole_words,
|
||
world_info_character_strategy,
|
||
world_names,
|
||
checkWorldInfo,
|
||
deleteWorldInfo,
|
||
setWorldInfoSettings,
|
||
getWorldInfoPrompt,
|
||
}
|
||
|
||
const world_info_insertion_strategy = {
|
||
evenly: 0,
|
||
character_first: 1,
|
||
global_first: 2,
|
||
};
|
||
|
||
let world_info = [];
|
||
let world_names;
|
||
let world_info_depth = 2;
|
||
let world_info_budget = 25;
|
||
let world_info_recursive = false;
|
||
let world_info_case_sensitive = false;
|
||
let world_info_match_whole_words = false;
|
||
let world_info_character_strategy = world_info_insertion_strategy.evenly;
|
||
const saveWorldDebounced = debounce(async (name, data) => await _save(name, data), 1000);
|
||
const saveSettingsDebounced = debounce(() => saveSettings(), 1000);
|
||
const sortFn = (a, b) => b.order - a.order;
|
||
|
||
const world_info_position = {
|
||
before: 0,
|
||
after: 1,
|
||
ANTop: 2,
|
||
ANBottom: 3,
|
||
|
||
};
|
||
|
||
const worldInfoCache = {};
|
||
|
||
async function getWorldInfoPrompt(chat2, maxContext) {
|
||
let worldInfoString = "", worldInfoBefore = "", worldInfoAfter = "";
|
||
|
||
const activatedWorldInfo = await checkWorldInfo(chat2, maxContext);
|
||
worldInfoBefore = activatedWorldInfo.worldInfoBefore;
|
||
worldInfoAfter = activatedWorldInfo.worldInfoAfter;
|
||
worldInfoString = worldInfoBefore + worldInfoAfter;
|
||
|
||
return { worldInfoString, worldInfoBefore, worldInfoAfter };
|
||
}
|
||
|
||
function setWorldInfoSettings(settings, data) {
|
||
if (settings.world_info_depth !== undefined)
|
||
world_info_depth = Number(settings.world_info_depth);
|
||
if (settings.world_info_budget !== undefined)
|
||
world_info_budget = Number(settings.world_info_budget);
|
||
if (settings.world_info_recursive !== undefined)
|
||
world_info_recursive = Boolean(settings.world_info_recursive);
|
||
if (settings.world_info_case_sensitive !== undefined)
|
||
world_info_case_sensitive = Boolean(settings.world_info_case_sensitive);
|
||
if (settings.world_info_match_whole_words !== undefined)
|
||
world_info_match_whole_words = Boolean(settings.world_info_match_whole_words);
|
||
if (settings.world_info_character_strategy !== undefined)
|
||
world_info_character_strategy = Number(settings.world_info_character_strategy);
|
||
|
||
// Migrate old settings
|
||
if (world_info_budget > 100) {
|
||
world_info_budget = 25;
|
||
}
|
||
|
||
// Reset selected world from old string
|
||
if (typeof settings.world_info === "string") {
|
||
const existingWorldInfo = settings.world_info;
|
||
settings.world_info = [existingWorldInfo];
|
||
}
|
||
|
||
$("#world_info_depth_counter").text(world_info_depth);
|
||
$("#world_info_depth").val(world_info_depth);
|
||
|
||
$("#world_info_budget_counter").text(world_info_budget);
|
||
$("#world_info_budget").val(world_info_budget);
|
||
|
||
$("#world_info_recursive").prop('checked', world_info_recursive);
|
||
$("#world_info_case_sensitive").prop('checked', world_info_case_sensitive);
|
||
$("#world_info_match_whole_words").prop('checked', world_info_match_whole_words);
|
||
|
||
$(`#world_info_character_strategy option[value='${world_info_character_strategy}']`).prop('selected', true);
|
||
$("#world_info_character_strategy").val(world_info_character_strategy);
|
||
|
||
world_names = data.world_names?.length ? data.world_names : [];
|
||
world_info = settings.world_info?.filter((e) => world_names.includes(e)) ?? [];
|
||
|
||
if (world_names.length > 0) {
|
||
$("#world_info").empty();
|
||
}
|
||
|
||
world_names.forEach((item, i) => {
|
||
$("#world_info").append(`<option value='${i}'${world_info.includes(item) ? ' selected' : ''}>${item}</option>`);
|
||
$("#world_editor_select").append(`<option value='${i}'>${item}</option>`);
|
||
});
|
||
|
||
$("#world_editor_select").trigger("change");
|
||
}
|
||
|
||
// World Info Editor
|
||
async function showWorldEditor(name) {
|
||
if (!name) {
|
||
hideWorldEditor();
|
||
return;
|
||
}
|
||
|
||
const wiData = await loadWorldInfoData(name);
|
||
displayWorldEntries(name, wiData);
|
||
}
|
||
|
||
async function loadWorldInfoData(name) {
|
||
if (!name) {
|
||
return;
|
||
}
|
||
|
||
if (worldInfoCache[name]) {
|
||
return worldInfoCache[name];
|
||
}
|
||
|
||
const response = await fetch("/getworldinfo", {
|
||
method: "POST",
|
||
headers: getRequestHeaders(),
|
||
body: JSON.stringify({ name: name }),
|
||
cache: 'no-cache',
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
worldInfoCache[name] = data;
|
||
return data;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
async function updateWorldInfoList() {
|
||
var result = await fetch("/getsettings", {
|
||
method: "POST",
|
||
headers: getRequestHeaders(),
|
||
body: JSON.stringify({}),
|
||
});
|
||
|
||
if (result.ok) {
|
||
var data = await result.json();
|
||
world_names = data.world_names?.length ? data.world_names : [];
|
||
$("#world_info").find('option[value!=""]').remove();
|
||
$("#world_editor_select").find('option[value!=""]').remove();
|
||
|
||
world_names.forEach((item, i) => {
|
||
$("#world_info").append(`<option value='${i}'${world_info.includes(item) ? ' selected' : ''}>${item}</option>`);
|
||
$("#world_editor_select").append(`<option value='${i}'>${item}</option>`);
|
||
});
|
||
}
|
||
}
|
||
|
||
function hideWorldEditor() {
|
||
displayWorldEntries(null, null);
|
||
}
|
||
|
||
function getWIElement(name) {
|
||
const wiElement = $("#world_info").children().filter(function() {
|
||
return $(this).text().toLowerCase() === name.toLowerCase()
|
||
});
|
||
|
||
return wiElement;
|
||
}
|
||
|
||
function nullWorldInfo() {
|
||
toastr.info("Create or import a new World Info file first.", "World Info is not set", { timeOut: 10000, preventDuplicates: true });
|
||
}
|
||
|
||
function displayWorldEntries(name, data) {
|
||
$("#world_popup_entries_list").empty().show();
|
||
|
||
if (!data || !("entries" in data)) {
|
||
$("#world_popup_new").off('click').on('click', nullWorldInfo);
|
||
$("#world_popup_name_button").off('click').on('click', nullWorldInfo);
|
||
$("#world_popup_export").off('click').on('click', nullWorldInfo);
|
||
$("#world_popup_delete").off('click').on('click', nullWorldInfo);
|
||
$("#world_popup_entries_list").hide();
|
||
return;
|
||
}
|
||
|
||
// Convert the data.entries object into an array
|
||
const entriesArray = Object.keys(data.entries).map(uid => {
|
||
const entry = data.entries[uid];
|
||
entry.displayIndex = entry.displayIndex ?? entry.uid;
|
||
return entry;
|
||
});
|
||
|
||
// Sort the entries array by displayIndex and uid
|
||
entriesArray.sort((a, b) => a.displayIndex - b.displayIndex || a.uid - b.uid);
|
||
|
||
// Loop through the sorted array and call appendWorldEntry
|
||
for (const entry of entriesArray) {
|
||
appendWorldEntry(name, data, entry);
|
||
}
|
||
|
||
$("#world_popup_new").off('click').on('click', () => {
|
||
createWorldInfoEntry(name, data);
|
||
});
|
||
|
||
$("#world_popup_name_button").off('click').on('click', async () => {
|
||
await renameWorldInfo(name, data);
|
||
});
|
||
|
||
$("#world_popup_export").off('click').on('click', () => {
|
||
if (name && data) {
|
||
const jsonValue = JSON.stringify(data);
|
||
const fileName = `${name}.json`;
|
||
download(jsonValue, fileName, "application/json");
|
||
}
|
||
});
|
||
|
||
$("#world_popup_delete").off('click').on('click', async () => {
|
||
const confirmation = await callPopup(`<h3>Delete the World/Lorebook: "${name}"?</h3>This action is irreversible!`, "confirm");
|
||
|
||
if (!confirmation) {
|
||
return;
|
||
}
|
||
|
||
// Selected world_info automatically refreshes
|
||
await deleteWorldInfo(name);
|
||
});
|
||
|
||
// Check if a sortable instance exists
|
||
if ($('#world_popup_entries_list').sortable('instance') !== undefined) {
|
||
// Destroy the instance
|
||
$('#world_popup_entries_list').sortable('destroy');
|
||
}
|
||
|
||
$("#world_popup_entries_list").sortable({
|
||
handle: ".drag-handle",
|
||
stop: async function (event, ui) {
|
||
$('#world_popup_entries_list .world_entry').each(function (index) {
|
||
const uid = $(this).data('uid');
|
||
|
||
// Update the display index in the data array
|
||
const item = data.entries[uid];
|
||
|
||
if (!item) {
|
||
console.debug(`Could not find entry with uid ${uid}`);
|
||
return;
|
||
}
|
||
|
||
item.displayIndex = index;
|
||
setOriginalDataValue(data, uid, 'extensions.display_index', index);
|
||
});
|
||
|
||
console.table(Object.keys(data.entries).map(uid => data.entries[uid]).map(x => ({ uid: x.uid, key: x.key.join(','), displayIndex: x.displayIndex })));
|
||
|
||
await saveWorldInfo(name, data, true);
|
||
}
|
||
});
|
||
$("#world_popup_entries_list").disableSelection();
|
||
}
|
||
|
||
function setOriginalDataValue(data, uid, key, value) {
|
||
if (data.originalData && Array.isArray(data.originalData.entries)) {
|
||
let originalEntry = data.originalData.entries.find(x => x.uid === uid);
|
||
|
||
if (!originalEntry) {
|
||
return;
|
||
}
|
||
|
||
const keyParts = key.split('.');
|
||
let currentObject = originalEntry;
|
||
|
||
for (let i = 0; i < keyParts.length - 1; i++) {
|
||
const part = keyParts[i];
|
||
|
||
if (!currentObject.hasOwnProperty(part)) {
|
||
currentObject[part] = {};
|
||
}
|
||
|
||
currentObject = currentObject[part];
|
||
}
|
||
|
||
currentObject[keyParts[keyParts.length - 1]] = value;
|
||
}
|
||
}
|
||
|
||
function deleteOriginalDataValue(data, uid) {
|
||
if (data.originalData && Array.isArray(data.originalData.entries)) {
|
||
const originalIndex = data.originalData.entries.findIndex(x => x.uid === uid);
|
||
|
||
if (originalIndex >= 0) {
|
||
data.originalData.entries.splice(originalIndex, 1);
|
||
}
|
||
}
|
||
}
|
||
|
||
function appendWorldEntry(name, data, entry) {
|
||
const template = $("#entry_edit_template .world_entry").clone();
|
||
template.data("uid", entry.uid);
|
||
|
||
// key
|
||
const keyInput = template.find('textarea[name="key"]');
|
||
keyInput.data("uid", entry.uid);
|
||
keyInput.on("click", function (event) {
|
||
// Prevent closing the drawer on clicking the input
|
||
event.stopPropagation();
|
||
});
|
||
|
||
keyInput.on("input", function () {
|
||
const uid = $(this).data("uid");
|
||
const value = $(this).val();
|
||
resetScrollHeight(this);
|
||
data.entries[uid].key = value
|
||
.split(",")
|
||
.map((x) => x.trim())
|
||
.filter((x) => x);
|
||
|
||
setOriginalDataValue(data, uid, "keys", data.entries[uid].key);
|
||
saveWorldInfo(name, data);
|
||
});
|
||
keyInput.val(entry.key.join(",")).trigger("input");
|
||
initScrollHeight(keyInput);
|
||
|
||
// keysecondary
|
||
const keySecondaryInput = template.find('textarea[name="keysecondary"]');
|
||
keySecondaryInput.data("uid", entry.uid);
|
||
keySecondaryInput.on("input", function () {
|
||
const uid = $(this).data("uid");
|
||
const value = $(this).val();
|
||
resetScrollHeight(this);
|
||
data.entries[uid].keysecondary = value
|
||
.split(",")
|
||
.map((x) => x.trim())
|
||
.filter((x) => x);
|
||
|
||
setOriginalDataValue(data, uid, "secondary_keys", data.entries[uid].keysecondary);
|
||
saveWorldInfo(name, data);
|
||
});
|
||
|
||
keySecondaryInput.val(entry.keysecondary.join(",")).trigger("input");
|
||
initScrollHeight(keySecondaryInput);
|
||
|
||
// comment
|
||
const commentInput = template.find('textarea[name="comment"]');
|
||
const commentToggle = template.find('input[name="addMemo"]');
|
||
commentInput.data("uid", entry.uid);
|
||
commentInput.on("input", function () {
|
||
const uid = $(this).data("uid");
|
||
const value = $(this).val();
|
||
data.entries[uid].comment = value;
|
||
|
||
setOriginalDataValue(data, uid, "comment", data.entries[uid].comment);
|
||
saveWorldInfo(name, data);
|
||
});
|
||
commentToggle.data("uid", entry.uid);
|
||
commentToggle.on("input", function () {
|
||
const uid = $(this).data("uid");
|
||
const value = $(this).prop("checked");
|
||
//console.log(value)
|
||
const commentContainer = $(this)
|
||
.closest(".world_entry")
|
||
.find(".commentContainer");
|
||
data.entries[uid].addMemo = value;
|
||
saveWorldInfo(name, data);
|
||
value ? commentContainer.show() : commentContainer.hide();
|
||
});
|
||
|
||
commentInput.val(entry.comment).trigger("input");
|
||
commentToggle.prop("checked", entry.addMemo).trigger("input");
|
||
|
||
// content
|
||
const countTokensDebounced = debounce(function (that, value) {
|
||
const numberOfTokens = getTokenCount(value);
|
||
$(that)
|
||
.closest(".world_entry")
|
||
.find(".world_entry_form_token_counter")
|
||
.text(numberOfTokens);
|
||
}, 1000);
|
||
|
||
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();
|
||
data.entries[uid].content = value;
|
||
|
||
setOriginalDataValue(data, uid, "content", data.entries[uid].content);
|
||
saveWorldInfo(name, data);
|
||
|
||
// count tokens
|
||
countTokensDebounced(this, value);
|
||
});
|
||
contentInput.val(entry.content).trigger("input");
|
||
//initScrollHeight(contentInput);
|
||
|
||
// 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");
|
||
data.entries[uid].selective = value;
|
||
|
||
setOriginalDataValue(data, uid, "selective", data.entries[uid].selective);
|
||
saveWorldInfo(name, data);
|
||
|
||
const keysecondary = $(this)
|
||
.closest(".world_entry")
|
||
.find(".keysecondary");
|
||
|
||
const keysecondarytextpole = $(this)
|
||
.closest(".world_entry")
|
||
.find(".keysecondarytextpole");
|
||
|
||
const keyprimarytextpole = $(this)
|
||
.closest(".world_entry")
|
||
.find(".keyprimarytextpole");
|
||
|
||
const keyprimaryHeight = keyprimarytextpole.outerHeight();
|
||
keysecondarytextpole.css('height', keyprimaryHeight + 'px');
|
||
|
||
value ? keysecondary.show() : keysecondary.hide();
|
||
|
||
});
|
||
selectiveInput.prop("checked", entry.selective).trigger("input");
|
||
|
||
|
||
// 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");
|
||
data.entries[uid].constant = value;
|
||
setOriginalDataValue(data, uid, "constant", data.entries[uid].constant);
|
||
saveWorldInfo(name, data);
|
||
});
|
||
constantInput.prop("checked", entry.constant).trigger("input");
|
||
|
||
// order
|
||
const orderInput = template.find('input[name="order"]');
|
||
orderInput.data("uid", entry.uid);
|
||
orderInput.on("input", function () {
|
||
const uid = $(this).data("uid");
|
||
const value = Number($(this).val());
|
||
|
||
data.entries[uid].order = !isNaN(value) ? value : 0;
|
||
setOriginalDataValue(data, uid, "insertion_order", data.entries[uid].order);
|
||
saveWorldInfo(name, data);
|
||
});
|
||
orderInput.val(entry.order).trigger("input");
|
||
|
||
// position
|
||
if (entry.position === undefined) {
|
||
entry.position = 0;
|
||
}
|
||
|
||
const positionInput = template.find('input[name="position"]');
|
||
positionInput.data("uid", entry.uid);
|
||
positionInput.on("input", function () {
|
||
const uid = $(this).data("uid");
|
||
const value = Number($(this).val());
|
||
data.entries[uid].position = !isNaN(value) ? value : 0;
|
||
// Spec v2 only supports before_char and after_char
|
||
setOriginalDataValue(data, uid, "position", data.entries[uid].position == 0 ? 'before_char' : 'after_char');
|
||
// Write the original value as extensions field
|
||
setOriginalDataValue(data, uid, "extensions.position", data.entries[uid].position);
|
||
saveWorldInfo(name, data);
|
||
});
|
||
template
|
||
.find(`input[name="position"][value=${entry.position}]`)
|
||
.prop("checked", true)
|
||
.trigger("input");
|
||
|
||
// display uid
|
||
template.find(".world_entry_form_uid_value").text(entry.uid);
|
||
|
||
// disable
|
||
const disableInput = template.find('input[name="disable"]');
|
||
disableInput.data("uid", entry.uid);
|
||
disableInput.on("input", function () {
|
||
const uid = $(this).data("uid");
|
||
const value = $(this).prop("checked");
|
||
data.entries[uid].disable = value;
|
||
setOriginalDataValue(data, uid, "enabled", !data.entries[uid].disable);
|
||
saveWorldInfo(name, data);
|
||
});
|
||
disableInput.prop("checked", entry.disable).trigger("input");
|
||
|
||
const excludeRecursionInput = template.find('input[name="exclude_recursion"]');
|
||
excludeRecursionInput.data("uid", entry.uid);
|
||
excludeRecursionInput.on("input", function () {
|
||
const uid = $(this).data("uid");
|
||
const value = $(this).prop("checked");
|
||
data.entries[uid].excludeRecursion = value;
|
||
setOriginalDataValue(data, uid, "extensions.exclude_recursion", data.entries[uid].excludeRecursion);
|
||
saveWorldInfo(name, data);
|
||
});
|
||
excludeRecursionInput.prop("checked", entry.excludeRecursion).trigger("input");
|
||
|
||
// 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(data, uid);
|
||
deleteOriginalDataValue(data, uid);
|
||
$(this).closest(".world_entry").remove();
|
||
saveWorldInfo(name, data);
|
||
});
|
||
|
||
template.appendTo("#world_popup_entries_list");
|
||
template.find('.inline-drawer-content').css('display', 'none'); //entries start collapsed
|
||
|
||
return template;
|
||
}
|
||
|
||
async function deleteWorldInfoEntry(data, uid) {
|
||
if (!data || !("entries" in data)) {
|
||
return;
|
||
}
|
||
|
||
delete data.entries[uid];
|
||
}
|
||
|
||
function createWorldInfoEntry(name, data) {
|
||
const newEntryTemplate = {
|
||
key: [],
|
||
keysecondary: [],
|
||
comment: "",
|
||
content: "",
|
||
constant: false,
|
||
selective: false,
|
||
addMemo: false,
|
||
order: 100,
|
||
position: 0,
|
||
disable: false,
|
||
excludeRecursion: false
|
||
};
|
||
const newUid = getFreeWorldEntryUid(data);
|
||
|
||
if (!Number.isInteger(newUid)) {
|
||
console.error("Couldn't assign UID to a new entry");
|
||
return;
|
||
}
|
||
|
||
const newEntry = { uid: newUid, ...newEntryTemplate };
|
||
data.entries[newUid] = newEntry;
|
||
|
||
const entryTemplate = appendWorldEntry(name, data, newEntry);
|
||
entryTemplate.get(0).scrollIntoView({ behavior: "smooth" });
|
||
}
|
||
|
||
async function _save(name, data) {
|
||
const response = await fetch("/editworldinfo", {
|
||
method: "POST",
|
||
headers: getRequestHeaders(),
|
||
body: JSON.stringify({ name: name, data: data }),
|
||
});
|
||
}
|
||
|
||
async function saveWorldInfo(name, data, immediately) {
|
||
if (!name || !data) {
|
||
return;
|
||
}
|
||
|
||
delete worldInfoCache[name];
|
||
|
||
if (immediately) {
|
||
return await _save(name, data);
|
||
}
|
||
|
||
saveWorldDebounced(name, data);
|
||
}
|
||
|
||
async function renameWorldInfo(name, data) {
|
||
const oldName = name;
|
||
const newName = await callPopup("<h3>Rename World Info</h3>Enter a new name:", 'input', oldName);
|
||
|
||
if (oldName === newName || !newName) {
|
||
console.debug("World info rename cancelled");
|
||
return;
|
||
}
|
||
|
||
const entryPreviouslySelected = world_info.findIndex((e) => e === oldName);
|
||
|
||
await saveWorldInfo(newName, data, true);
|
||
await deleteWorldInfo(oldName);
|
||
|
||
if (entryPreviouslySelected !== -1) {
|
||
const wiElement = getWIElement(newName);
|
||
wiElement.prop("selected", true);
|
||
$("#world_info").trigger('change');
|
||
}
|
||
|
||
const selectedIndex = world_names.indexOf(newName);
|
||
if (selectedIndex !== -1) {
|
||
$('#world_editor_select').val(selectedIndex).trigger('change');
|
||
}
|
||
}
|
||
|
||
async function deleteWorldInfo(worldInfoName) {
|
||
if (!world_names.includes(worldInfoName)) {
|
||
return;
|
||
}
|
||
|
||
const response = await fetch("/deleteworldinfo", {
|
||
method: "POST",
|
||
headers: getRequestHeaders(),
|
||
body: JSON.stringify({ name: worldInfoName }),
|
||
});
|
||
|
||
if (response.ok) {
|
||
const existingWorldIndex = world_info.findIndex((e) => e === worldInfoName);
|
||
if (existingWorldIndex !== -1) {
|
||
world_info.splice(existingWorldIndex, 1);
|
||
saveSettingsDebounced();
|
||
}
|
||
|
||
await updateWorldInfoList();
|
||
$('#world_editor_select').trigger('change');
|
||
}
|
||
}
|
||
|
||
function getFreeWorldEntryUid(data) {
|
||
if (!data || !("entries" in data)) {
|
||
return null;
|
||
}
|
||
|
||
const MAX_UID = 1_000_000; // <- should be safe enough :)
|
||
for (let uid = 0; uid < MAX_UID; uid++) {
|
||
if (uid in data.entries) {
|
||
continue;
|
||
}
|
||
return uid;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function getFreeWorldName() {
|
||
const MAX_FREE_NAME = 100_000;
|
||
for (let index = 1; index < MAX_FREE_NAME; index++) {
|
||
const newName = `New World (${index})`;
|
||
if (world_names.includes(newName)) {
|
||
continue;
|
||
}
|
||
return newName;
|
||
}
|
||
|
||
return undefined;
|
||
}
|
||
|
||
async function createNewWorldInfo(worldInfoName) {
|
||
const worldInfoTemplate = { entries: {} };
|
||
|
||
if (!worldInfoName) {
|
||
return;
|
||
}
|
||
|
||
await saveWorldInfo(worldInfoName, worldInfoTemplate, true);
|
||
await updateWorldInfoList();
|
||
|
||
const selectedIndex = world_names.indexOf(worldInfoName);
|
||
if (selectedIndex !== -1) {
|
||
$('#world_editor_select').val(selectedIndex).trigger('change');
|
||
} else {
|
||
hideWorldEditor();
|
||
}
|
||
}
|
||
|
||
// Gets a string that respects the case sensitivity setting
|
||
function transformString(str) {
|
||
return world_info_case_sensitive ? str : str.toLowerCase();
|
||
}
|
||
|
||
async function getCharacterLore() {
|
||
const name = characters[this_chid]?.data?.extensions?.world;
|
||
|
||
if (!name) {
|
||
return [];
|
||
}
|
||
|
||
if (world_info.includes(name)) {
|
||
console.debug(`Character ${characters[this_chid]?.name} world info is the same as global: ${name}. Skipping...`);
|
||
return [];
|
||
}
|
||
|
||
if (!world_names.includes(name)) {
|
||
console.log(`Character ${characters[this_chid]?.name} world info does not exist: ${name}`);
|
||
return [];
|
||
}
|
||
|
||
const data = await loadWorldInfoData(name);
|
||
const entries = data ? Object.keys(data.entries).map((x) => data.entries[x]) : [];
|
||
console.debug(`Character ${characters[this_chid]?.name} lore (${name}) has ${entries.length} world info entries`);
|
||
return entries;
|
||
}
|
||
|
||
async function getGlobalLore() {
|
||
if (!world_info) {
|
||
return [];
|
||
}
|
||
|
||
let entries = [];
|
||
for (const worldName of world_info) {
|
||
const data = await loadWorldInfoData(worldName);
|
||
const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]) : [];
|
||
entries = entries.concat(newEntries);
|
||
}
|
||
|
||
console.debug(`Global world info has ${entries.length} entries`);
|
||
|
||
return entries;
|
||
}
|
||
|
||
async function getSortedEntries() {
|
||
try {
|
||
const globalLore = await getGlobalLore();
|
||
const characterLore = await getCharacterLore();
|
||
|
||
let entries;
|
||
|
||
switch (Number(world_info_character_strategy)) {
|
||
case world_info_insertion_strategy.evenly:
|
||
console.debug('WI using evenly')
|
||
entries = [...globalLore, ...characterLore].sort(sortFn);
|
||
break;
|
||
case world_info_insertion_strategy.character_first:
|
||
console.debug('WI using char first')
|
||
entries = [...characterLore.sort(sortFn), ...globalLore.sort(sortFn)];
|
||
break;
|
||
case world_info_insertion_strategy.global_first:
|
||
console.debug('WI using global first')
|
||
entries = [...globalLore.sort(sortFn), ...characterLore.sort(sortFn)];
|
||
break;
|
||
default:
|
||
console.error("Unknown WI insertion strategy: ", world_info_character_strategy, "defaulting to evenly");
|
||
entries = [...globalLore, ...characterLore].sort(sortFn);
|
||
break;
|
||
}
|
||
|
||
console.debug(`Sorted ${entries.length} world lore entries using strategy ${world_info_character_strategy}`);
|
||
|
||
return entries;
|
||
}
|
||
catch (e) {
|
||
console.error(e);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
async function checkWorldInfo(chat, maxContext) {
|
||
const context = getContext();
|
||
const messagesToLookBack = world_info_depth * 2 || 1;
|
||
let textToScan = transformString(chat.slice(0, messagesToLookBack).join(""));
|
||
let needsToScan = true;
|
||
let count = 0;
|
||
let allActivatedEntries = new Set();
|
||
|
||
const budget = Math.round(world_info_budget * maxContext / 100) || 1;
|
||
console.debug(`Context size: ${maxContext}; WI budget: ${budget} (${world_info_budget}%)`);
|
||
const sortedEntries = await getSortedEntries();
|
||
|
||
if (sortedEntries.length === 0) {
|
||
return { worldInfoBefore: '', worldInfoAfter: '' };
|
||
}
|
||
|
||
while (needsToScan) {
|
||
// Track how many times the loop has run
|
||
count++;
|
||
|
||
let activatedNow = new Set();
|
||
|
||
for (let entry of sortedEntries) {
|
||
if (allActivatedEntries.has(entry) || entry.disable == true || (count > 1 && world_info_recursive && entry.excludeRecursion)) {
|
||
continue;
|
||
}
|
||
|
||
if (entry.constant) {
|
||
activatedNow.add(entry);
|
||
continue;
|
||
}
|
||
|
||
if (Array.isArray(entry.key) && entry.key.length) {
|
||
primary: for (let key of entry.key) {
|
||
const substituted = substituteParams(key);
|
||
if (substituted && matchKeys(textToScan, substituted.trim())) {
|
||
if (
|
||
entry.selective &&
|
||
Array.isArray(entry.keysecondary) &&
|
||
entry.keysecondary.length
|
||
) {
|
||
secondary: for (let keysecondary of entry.keysecondary) {
|
||
const secondarySubstituted = substituteParams(keysecondary);
|
||
if (secondarySubstituted && matchKeys(textToScan, secondarySubstituted.trim())) {
|
||
activatedNow.add(entry);
|
||
break secondary;
|
||
}
|
||
}
|
||
} else {
|
||
activatedNow.add(entry);
|
||
break primary;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
needsToScan = world_info_recursive && activatedNow.size > 0;
|
||
const newEntries = [...activatedNow]
|
||
.sort((a, b) => sortedEntries.indexOf(a) - sortedEntries.indexOf(b));
|
||
let newContent = "";
|
||
|
||
for (const entry of newEntries) {
|
||
newContent += `${substituteParams(entry.content)}\n`;
|
||
|
||
if (getTokenCount(textToScan + newContent) >= budget) {
|
||
console.debug(`WI budget reached, stopping`);
|
||
needsToScan = false;
|
||
break;
|
||
}
|
||
|
||
allActivatedEntries.add(entry);
|
||
}
|
||
|
||
if (needsToScan) {
|
||
textToScan = (transformString(newEntries.map(x => x.content).join('\n')) + textToScan);
|
||
}
|
||
}
|
||
|
||
let worldInfoBefore = "";
|
||
let worldInfoAfter = "";
|
||
|
||
const ANTopInjection = [];
|
||
const ANBottomInjection = [];
|
||
|
||
[...allActivatedEntries].sort(sortFn).forEach((entry) => {
|
||
if (entry.position === world_info_position.before) {
|
||
worldInfoBefore = `${substituteParams(entry.content)}\n${worldInfoBefore}`;
|
||
} else if (entry.position === world_info_position.after) {
|
||
worldInfoAfter = `${substituteParams(entry.content)}\n${worldInfoAfter}`;
|
||
} else if (entry.position === world_info_position.ANTop) {
|
||
ANTopInjection.push(entry.content);
|
||
} else if (entry.position === world_info_position.ANBottom) {
|
||
ANBottomInjection.push(entry.content);
|
||
}
|
||
});
|
||
|
||
if (shouldWIAddPrompt) {
|
||
const originalAN = context.extensionPrompts['2_floating_prompt'].value;
|
||
const ANWithWI = `\n${ANTopInjection.join("\n")} \n${originalAN} \n${ANBottomInjection.reverse().join("\n")}`
|
||
context.setExtensionPrompt('2_floating_prompt', ANWithWI, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth]);
|
||
}
|
||
|
||
return { worldInfoBefore, worldInfoAfter };
|
||
}
|
||
|
||
function matchKeys(haystack, needle) {
|
||
const transformedString = transformString(needle);
|
||
|
||
if (world_info_match_whole_words) {
|
||
const keyWords = transformedString.split(/\s+/);
|
||
|
||
if (keyWords.length > 1) {
|
||
return haystack.includes(transformedString);
|
||
}
|
||
else {
|
||
const regex = new RegExp(`\\b${transformedString}\\b`);
|
||
if (regex.test(haystack)) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
} else {
|
||
return haystack.includes(transformedString);
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
function convertAgnaiMemoryBook(inputObj) {
|
||
const outputObj = { entries: {} };
|
||
|
||
inputObj.entries.forEach((entry, index) => {
|
||
outputObj.entries[index] = {
|
||
uid: index,
|
||
key: entry.keywords,
|
||
keysecondary: [],
|
||
comment: entry.name,
|
||
content: entry.entry,
|
||
constant: false,
|
||
selective: false,
|
||
order: entry.weight,
|
||
position: 0,
|
||
disable: !entry.enabled,
|
||
addMemo: !!entry.name,
|
||
excludeRecursion: false,
|
||
displayIndex: index,
|
||
};
|
||
});
|
||
|
||
return outputObj;
|
||
}
|
||
|
||
function convertRisuLorebook(inputObj) {
|
||
const outputObj = { entries: {} };
|
||
|
||
inputObj.data.forEach((entry, index) => {
|
||
outputObj.entries[index] = {
|
||
uid: index,
|
||
key: entry.key.split(',').map(x => x.trim()),
|
||
keysecondary: entry.secondkey ? entry.secondkey.split(',').map(x => x.trim()) : [],
|
||
comment: entry.comment,
|
||
content: entry.content,
|
||
constant: entry.alwaysActive,
|
||
selective: entry.selective,
|
||
order: entry.insertorder,
|
||
position: world_info_position.before,
|
||
disable: false,
|
||
addMemo: true,
|
||
excludeRecursion: false,
|
||
displayIndex: index,
|
||
};
|
||
});
|
||
|
||
return outputObj;
|
||
}
|
||
|
||
function convertNovelLorebook(inputObj) {
|
||
const outputObj = {
|
||
entries: {}
|
||
};
|
||
|
||
inputObj.entries.forEach((entry, index) => {
|
||
const displayName = entry.displayName;
|
||
const addMemo = displayName !== undefined && displayName.trim() !== '';
|
||
|
||
outputObj.entries[index] = {
|
||
uid: index,
|
||
key: entry.keys,
|
||
keysecondary: [],
|
||
comment: displayName || '',
|
||
content: entry.text,
|
||
constant: false,
|
||
selective: false,
|
||
order: entry.contextConfig?.budgetPriority ?? 0,
|
||
position: 0,
|
||
disable: !entry.enabled,
|
||
addMemo: addMemo,
|
||
excludeRecursion: false,
|
||
displayIndex: index,
|
||
};
|
||
});
|
||
|
||
return outputObj;
|
||
}
|
||
|
||
function convertCharacterBook(characterBook) {
|
||
const result = { entries: {}, originalData: characterBook };
|
||
|
||
characterBook.entries.forEach((entry, index) => {
|
||
// Not in the spec, but this is needed to find the entry in the original data
|
||
if (entry.id === undefined) {
|
||
entry.id = index;
|
||
}
|
||
|
||
result.entries[index] = {
|
||
uid: entry.id || index,
|
||
key: entry.keys,
|
||
keysecondary: entry.secondary_keys || [],
|
||
comment: entry.comment || "",
|
||
content: entry.content,
|
||
constant: entry.constant || false,
|
||
selective: entry.selective || false,
|
||
order: entry.insertion_order,
|
||
position: entry.extensions?.position ?? (entry.position === "before_char" ? world_info_position.before : world_info_position.after),
|
||
excludeRecursion: entry.extensions?.exclude_recursion ?? false,
|
||
disable: !entry.enabled,
|
||
addMemo: entry.comment ? true : false,
|
||
displayIndex: entry.extensions?.display_index ?? index,
|
||
};
|
||
});
|
||
|
||
return result;
|
||
}
|
||
|
||
export async function importEmbeddedWorldInfo() {
|
||
const chid = $('#import_character_info').data('chid');
|
||
|
||
if (chid === undefined) {
|
||
return;
|
||
}
|
||
|
||
const bookName = characters[chid]?.data?.character_book?.name || `${characters[chid]?.name}'s Lorebook`;
|
||
const confirmationText = (`<h3>Are you sure you want to import "${bookName}"?</h3>`) + (world_names.includes(bookName) ? 'It will overwrite the World/Lorebook with the same name.' : '');
|
||
|
||
const confirmation = await callPopup(confirmationText, 'confirm');
|
||
|
||
if (!confirmation) {
|
||
return;
|
||
}
|
||
|
||
const convertedBook = convertCharacterBook(characters[chid].data.character_book);
|
||
|
||
await saveWorldInfo(bookName, convertedBook, true);
|
||
await updateWorldInfoList();
|
||
$('#character_world').val(bookName).trigger('change');
|
||
|
||
toastr.success(`The world "${bookName}" has been imported and linked to the character successfully.`, 'World/Lorebook imported');
|
||
|
||
const newIndex = world_names.indexOf(bookName);
|
||
if (newIndex >= 0) {
|
||
$("#world_editor_select").val(newIndex).trigger('change');
|
||
}
|
||
}
|
||
|
||
function onWorldInfoChange(_, text) {
|
||
let selectedWorlds;
|
||
if (_ !== '__notSlashCommand__') { // if it's a slash command
|
||
if (text !== undefined) { // and args are provided
|
||
const slashInputSplitText = text.trim().toLowerCase().split(",");
|
||
console.log(slashInputSplitText);
|
||
|
||
slashInputSplitText.forEach((worldName) => {
|
||
const wiElement = getWIElement(worldName);
|
||
if (wiElement.length > 0) {
|
||
wiElement.prop("selected", true);
|
||
toastr.success(`Activated world: ${wiElement.text()}`);
|
||
} else {
|
||
toastr.error(`No world found named: ${worldName}`);
|
||
}
|
||
})
|
||
} else { // if no args, unset all worlds
|
||
toastr.success('Deactivated all worlds');
|
||
world_info = [];
|
||
$("#world_info").val("");
|
||
}
|
||
} else { //if it's a pointer selection
|
||
let tempWorldInfo = [];
|
||
let selectedWorlds = $("#world_info").val().map((e) => Number(e)).filter((e) => !isNaN(e));
|
||
if (selectedWorlds.length > 0) {
|
||
selectedWorlds.forEach((worldIndex) => {
|
||
const existingWorldName = world_names[worldIndex];
|
||
if (existingWorldName) {
|
||
tempWorldInfo.push(existingWorldName);
|
||
} else {
|
||
const wiElement = getWIElement(existingWorldName);
|
||
wiElement.prop("selected", false);
|
||
toastr.error(`The world with ${existingWorldName} is invalid or corrupted.`);
|
||
}
|
||
});
|
||
}
|
||
world_info = tempWorldInfo;
|
||
}
|
||
|
||
saveSettingsDebounced();
|
||
}
|
||
|
||
jQuery(() => {
|
||
|
||
$(document).ready(function () {
|
||
registerSlashCommand('world', onWorldInfoChange, [], "– sets active World, or unsets if no args provided", true, true);
|
||
})
|
||
$("#world_info").on('mousedown change', async function (e) {
|
||
if (deviceInfo.device.type === 'desktop') {
|
||
e.preventDefault();
|
||
const option = $(e.target);
|
||
option.prop('selected', !option.prop('selected'));
|
||
await delay(1);
|
||
}
|
||
|
||
onWorldInfoChange('__notSlashCommand__');
|
||
});
|
||
|
||
//**************************WORLD INFO IMPORT EXPORT*************************//
|
||
$("#world_import_button").on('click', function () {
|
||
$("#world_import_file").trigger('click');
|
||
});
|
||
|
||
$("#world_import_file").on("change", async function (e) {
|
||
const file = e.target.files[0];
|
||
|
||
if (!file) {
|
||
return;
|
||
}
|
||
|
||
const formData = new FormData($("#form_world_import").get(0));
|
||
|
||
try {
|
||
let jsonData;
|
||
|
||
if (file.name.endsWith('.png')) {
|
||
const buffer = new Uint8Array(await getFileBuffer(file));
|
||
jsonData = extractDataFromPng(buffer, 'naidata');
|
||
} else {
|
||
// File should be a JSON file
|
||
jsonData = await parseJsonFile(file);
|
||
}
|
||
|
||
if (jsonData === undefined || jsonData === null) {
|
||
toastr.error(`File is not valid: ${file.name}`);
|
||
return;
|
||
}
|
||
|
||
// Convert Novel Lorebook
|
||
if (jsonData.lorebookVersion !== undefined) {
|
||
console.log('Converting Novel Lorebook');
|
||
formData.append('convertedData', JSON.stringify(convertNovelLorebook(jsonData)));
|
||
}
|
||
|
||
// Convert Agnai Memory Book
|
||
if (jsonData.kind === 'memory') {
|
||
console.log('Converting Agnai Memory Book');
|
||
formData.append('convertedData', JSON.stringify(convertAgnaiMemoryBook(jsonData)));
|
||
}
|
||
|
||
// Convert Risu Lorebook
|
||
if (jsonData.type === 'risu') {
|
||
console.log('Converting Risu Lorebook');
|
||
formData.append('convertedData', JSON.stringify(convertRisuLorebook(jsonData)));
|
||
}
|
||
} catch (error) {
|
||
toastr.error(`Error parsing file: ${error}`);
|
||
return;
|
||
}
|
||
|
||
jQuery.ajax({
|
||
type: "POST",
|
||
url: "/importworldinfo",
|
||
data: formData,
|
||
beforeSend: () => { },
|
||
cache: false,
|
||
contentType: false,
|
||
processData: false,
|
||
success: async function (data) {
|
||
if (data.name) {
|
||
await updateWorldInfoList();
|
||
|
||
const newIndex = world_names.indexOf(data.name);
|
||
if (newIndex >= 0) {
|
||
$("#world_editor_select").val(newIndex).trigger('change');
|
||
}
|
||
|
||
toastr.info(`World Info "${data.name}" imported successfully!`);
|
||
}
|
||
},
|
||
error: (jqXHR, exception) => { },
|
||
});
|
||
|
||
// Will allow to select the same file twice in a row
|
||
$("#form_world_import").trigger("reset");
|
||
});
|
||
|
||
$("#world_cross").click(() => {
|
||
hideWorldEditor();
|
||
});
|
||
|
||
$("#world_create_button").on('click', async () => {
|
||
const tempName = getFreeWorldName();
|
||
const finalName = await callPopup("<h3>Create a new World Info?</h3>Enter a name for the new file:", "input", tempName);
|
||
|
||
if (finalName) {
|
||
await createNewWorldInfo(finalName);
|
||
}
|
||
});
|
||
|
||
$("#world_editor_select").on('change', async () => {
|
||
const selectedIndex = $("#world_editor_select").find(":selected").val();
|
||
|
||
if (selectedIndex === "") {
|
||
hideWorldEditor();
|
||
} else {
|
||
const worldName = world_names[selectedIndex];
|
||
showWorldEditor(worldName);
|
||
}
|
||
});
|
||
|
||
$(document).on("input", "#world_info_depth", function () {
|
||
world_info_depth = Number($(this).val());
|
||
$("#world_info_depth_counter").text($(this).val());
|
||
saveSettingsDebounced();
|
||
});
|
||
|
||
$(document).on("input", "#world_info_budget", function () {
|
||
world_info_budget = Number($(this).val());
|
||
$("#world_info_budget_counter").text($(this).val());
|
||
saveSettingsDebounced();
|
||
});
|
||
|
||
$(document).on("input", "#world_info_recursive", function () {
|
||
world_info_recursive = !!$(this).prop('checked');
|
||
saveSettingsDebounced();
|
||
})
|
||
|
||
$('#world_info_case_sensitive').on('input', function () {
|
||
world_info_case_sensitive = !!$(this).prop('checked');
|
||
saveSettingsDebounced();
|
||
})
|
||
|
||
$('#world_info_match_whole_words').on('input', function () {
|
||
world_info_match_whole_words = !!$(this).prop('checked');
|
||
saveSettingsDebounced();
|
||
});
|
||
|
||
$('#world_info_character_strategy').on('change', function () {
|
||
world_info_character_strategy = $(this).val();
|
||
saveSettingsDebounced();
|
||
})
|
||
});
|