SillyTavern/public/scripts/world-info.js

1633 lines
56 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { saveSettings, callPopup, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types } from "../script.js";
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, deepClone, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option } from "./utils.js";
import { getContext } from "./extensions.js";
import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from "./authors-note.js";
import { registerSlashCommand } from "./slash-commands.js";
import { deviceInfo } from "./RossAscends-mods.js";
import { FILTER_TYPES, FilterHelper } from "./filters.js";
import { getTokenCount } from "./tokenizers.js";
export {
world_info,
world_info_budget,
world_info_depth,
world_info_recursive,
world_info_overflow_alert,
world_info_case_sensitive,
world_info_match_whole_words,
world_info_character_strategy,
world_info_budget_cap,
world_names,
checkWorldInfo,
deleteWorldInfo,
setWorldInfoSettings,
getWorldInfoPrompt,
}
const world_info_insertion_strategy = {
evenly: 0,
character_first: 1,
global_first: 2,
};
let world_info = {};
let selected_world_info = [];
let world_names;
let world_info_depth = 2;
let world_info_budget = 25;
let world_info_recursive = false;
let world_info_overflow_alert = false;
let world_info_case_sensitive = false;
let world_info_match_whole_words = false;
let world_info_character_strategy = world_info_insertion_strategy.character_first;
let world_info_budget_cap = 0;
const saveWorldDebounced = debounce(async (name, data) => await _save(name, data), 1000);
const saveSettingsDebounced = debounce(() => {
Object.assign(world_info, { globalSelect: selected_world_info })
saveSettings()
}, 1000);
const sortFn = (a, b) => b.order - a.order;
let updateEditor = (navigation) => { navigation; };
// Do not optimize. updateEditor is a function that is updated by the displayWorldEntries with new data.
const worldInfoFilter = new FilterHelper(() => updateEditor());
export function getWorldInfoSettings() {
return {
world_info,
world_info_depth,
world_info_budget,
world_info_recursive,
world_info_overflow_alert,
world_info_case_sensitive,
world_info_match_whole_words,
world_info_character_strategy,
world_info_budget_cap,
}
}
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_overflow_alert !== undefined)
world_info_overflow_alert = Boolean(settings.world_info_overflow_alert);
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);
if (settings.world_info_budget_cap !== undefined)
world_info_budget_cap = Number(settings.world_info_budget_cap);
// Migrate old settings
if (world_info_budget > 100) {
world_info_budget = 25;
}
// Reset selected world from old string and delete old keys
// TODO: Remove next release
const existingWorldInfo = settings.world_info;
if (typeof existingWorldInfo === "string") {
delete settings.world_info;
selected_world_info = [existingWorldInfo];
} else if (Array.isArray(existingWorldInfo)) {
delete settings.world_info;
selected_world_info = existingWorldInfo;
}
world_info = settings.world_info ?? {}
$("#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_overflow_alert").prop('checked', world_info_overflow_alert);
$("#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_info_budget_cap").val(world_info_budget_cap);
$("#world_info_budget_cap_counter").text(world_info_budget_cap);
world_names = data.world_names?.length ? data.world_names : [];
// Add to existing selected WI if it exists
selected_world_info = selected_world_info.concat(settings.world_info?.globalSelect?.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}'${selected_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}'${selected_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, navigation = navigation_option.none) {
updateEditor = (navigation) => displayWorldEntries(name, data, navigation);
$("#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();
$('#world_info_pagination').html('');
return;
}
function getDataArray(callback) {
// Convert the data.entries object into an array
let 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);
entriesArray = worldInfoFilter.applyFilters(entriesArray);
callback(entriesArray);
return entriesArray;
}
let startPage = 1;
if (navigation === navigation_option.previous) {
startPage = $("#world_info_pagination").pagination('getCurrentPageNum');
}
const storageKey = 'WI_PerPage';
$("#world_info_pagination").pagination({
dataSource: getDataArray,
pageSize: 25,
//pageSize: Number(localStorage.getItem(storageKey)) || 25,
//sizeChangerOptions: [10, 25, 50, 100],
//showSizeChanger: true,
pageRange: 1,
pageNumber: startPage,
position: 'top',
showPageNumbers: false,
prevText: '<',
nextText: '>',
formatNavigator: PAGINATION_TEMPLATE,
showNavigator: true,
callback: function (page) {
$("#world_popup_entries_list").empty();
const blocks = page.map(entry => getWorldEntry(name, data, entry));
$("#world_popup_entries_list").append(blocks);
},
afterSizeSelectorChange: function (e) {
localStorage.setItem(storageKey, e.target.value);
}
});
if (navigation === navigation_option.last) {
$("#world_info_pagination").pagination('go', $("#world_info_pagination").pagination('getTotalPage'));
}
$("#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;
}
if (world_info.charLore) {
world_info.charLore.forEach((charLore, index) => {
if (charLore.extraBooks?.includes(name)) {
const tempCharLore = charLore.extraBooks.filter((e) => e !== name);
if (tempCharLore.length === 0) {
world_info.charLore.splice(index, 1);
} else {
charLore.extraBooks = tempCharLore;
}
}
});
saveSettingsDebounced();
}
// 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({
delay: getSortableDelay(),
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 getWorldEntry(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 = String($(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);
// logic AND/NOT
const selectiveLogicDropdown = template.find('select[name="entryLogicType"]');
selectiveLogicDropdown.data("uid", entry.uid);
selectiveLogicDropdown.on("input", function () {
const uid = $(this).data("uid");
const value = Number($(this).val());
console.debug(`logic for ${entry.uid} set to ${value}`)
data.entries[uid].selectiveLogic = !isNaN(value) ? value : 0;
setOriginalDataValue(data, uid, "selectiveLogic", data.entries[uid].selectiveLogic);
saveWorldInfo(name, data);
});
template
.find(`select[name="entryLogicType"] option[value=${entry.selectiveLogic}]`)
.prop("selected", true)
.trigger("input");
// keysecondary
const keySecondaryInput = template.find('textarea[name="keysecondary"]');
keySecondaryInput.data("uid", entry.uid);
keySecondaryInput.on("input", function () {
const uid = $(this).data("uid");
const value = String($(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", true /* entry.addMemo */).trigger("input");
commentToggle.parent().hide()
// content
const counter = template.find(".world_entry_form_token_counter");
const countTokensDebounced = debounce(function (counter, value) {
const numberOfTokens = getTokenCount(value);
$(counter).text(numberOfTokens);
}, 1000);
const contentInput = template.find('textarea[name="content"]');
contentInput.data("uid", entry.uid);
contentInput.on("input", function (_, { skipCount } = {}) {
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);
if (skipCount) {
return;
}
// count tokens
countTokensDebounced(counter, value);
});
contentInput.val(entry.content).trigger("input", { skipCount: true });
//initScrollHeight(contentInput);
template.find('.inline-drawer-toggle').on('click', function () {
if (counter.data('first-run')) {
counter.data('first-run', false);
countTokensDebounced(counter, contentInput.val());
}
});
// 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();
});
//forced on, ignored if empty
selectiveInput.prop("checked", true /* entry.selective */).trigger("input");
selectiveInput.parent().hide();
// 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");
// probability
if (entry.probability === undefined) {
entry.probability = null;
}
const probabilityInput = template.find('input[name="probability"]');
probabilityInput.data("uid", entry.uid);
probabilityInput.on("input", function () {
const uid = $(this).data("uid");
const value = parseInt($(this).val());
data.entries[uid].probability = !isNaN(value) ? value : null;
// Clamp probability to 0-100
if (data.entries[uid].probability !== null) {
data.entries[uid].probability = Math.min(100, Math.max(0, data.entries[uid].probability));
if (data.entries[uid].probability !== value) {
$(this).val(data.entries[uid].probability);
}
}
setOriginalDataValue(data, uid, "extensions.probability", data.entries[uid].probability);
saveWorldInfo(name, data);
});
probabilityInput.val(entry.probability).trigger("input");
// probability toggle
if (entry.useProbability === undefined) {
entry.useProbability = false;
}
const probabilityToggle = template.find('input[name="useProbability"]');
probabilityToggle.data("uid", entry.uid);
probabilityToggle.on("input", function () {
const uid = $(this).data("uid");
const value = $(this).prop("checked");
data.entries[uid].useProbability = value;
const probabilityContainer = $(this)
.closest(".world_entry")
.find(".probabilityContainer");
saveWorldInfo(name, data);
value ? probabilityContainer.show() : probabilityContainer.hide();
if (value && data.entries[uid].probability === null) {
data.entries[uid].probability = 100;
}
if (!value) {
data.entries[uid].probability = null;
}
probabilityInput.val(data.entries[uid].probability).trigger("input");
});
//forced on, 100% by default
probabilityToggle.prop("checked", true /* entry.useProbability */).trigger("input");
probabilityToggle.parent().hide();
// position
if (entry.position === undefined) {
entry.position = 0;
}
const positionInput = template.find('select[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(`select[name="position"] option[value=${entry.position}]`)
.prop("selected", 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);
saveWorldInfo(name, data);
updateEditor(navigation_option.previous);
});
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: true,
selectiveLogic: 0,
addMemo: false,
order: 100,
position: 0,
disable: false,
excludeRecursion: false,
probability: 100,
useProbability: true,
};
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;
updateEditor(navigation_option.last);
}
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 = selected_world_info.findIndex((e) => e === oldName);
await saveWorldInfo(newName, data, true);
await deleteWorldInfo(oldName);
const existingCharLores = world_info.charLore?.filter((e) => e.extraBooks.includes(oldName));
if (existingCharLores && existingCharLores.length > 0) {
existingCharLores.forEach((charLore) => {
const tempCharLore = charLore.extraBooks.filter((e) => e !== oldName);
tempCharLore.push(newName);
charLore.extraBooks = tempCharLore;
});
saveSettingsDebounced();
}
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 = selected_world_info.findIndex((e) => e === worldInfoName);
if (existingWorldIndex !== -1) {
selected_world_info.splice(existingWorldIndex, 1);
saveSettingsDebounced();
}
await updateWorldInfoList();
$('#world_editor_select').trigger('change');
if ($('#character_world').val() === worldInfoName) {
$('#character_world').val('').trigger('change');
setWorldInfoButtonClass(undefined, false);
if (menu_type != 'create') {
saveCharacterDebounced();
}
}
}
}
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 character = characters[this_chid];
const name = character?.name;
let worldsToSearch = new Set();
const baseWorldName = character?.data?.extensions?.world;
if (baseWorldName) {
worldsToSearch.add(baseWorldName);
} else {
console.debug(`Character ${name}'s base world could not be found or is empty! Skipping...`)
return [];
}
// TODO: Maybe make the utility function not use the window context?
const fileName = getCharaFilename(this_chid);
const extraCharLore = world_info.charLore?.find((e) => e.name === fileName);
if (extraCharLore) {
worldsToSearch = new Set([...worldsToSearch, ...extraCharLore.extraBooks]);
}
let entries = [];
for (const worldName of worldsToSearch) {
if (selected_world_info.includes(worldName)) {
console.debug(`Character ${name}'s world ${worldName} is already activated in global world info! Skipping...`);
continue;
}
const data = await loadWorldInfoData(worldName);
const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]) : [];
entries = entries.concat(newEntries);
}
console.debug(`Character ${characters[this_chid]?.name} lore (${baseWorldName}) has ${entries.length} world info entries`);
return entries;
}
async function getGlobalLore() {
if (!selected_world_info) {
return [];
}
let entries = [];
for (const worldName of selected_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}`);
// Need to deep clone the entries to avoid modifying the cached data
return deepClone(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();
let failedProbabilityChecks = new Set();
let allActivatedText = '';
let budget = Math.round(world_info_budget * maxContext / 100) || 1;
if (world_info_budget_cap > 0 && budget > world_info_budget_cap) {
console.debug(`Budget ${budget} exceeds cap ${world_info_budget_cap}, using cap`);
budget = world_info_budget_cap;
}
console.debug(`Context size: ${maxContext}; WI budget: ${budget} (max% = ${world_info_budget}%, cap = ${world_info_budget_cap})`);
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 (failedProbabilityChecks.has(entry)) {
continue;
}
if (allActivatedEntries.has(entry) || entry.disable == true || (count > 1 && world_info_recursive && entry.excludeRecursion)) {
continue;
}
if (entry.constant) {
entry.content = substituteParams(entry.content)
activatedNow.add(entry);
continue;
}
if (Array.isArray(entry.key) && entry.key.length) { //check for keywords existing
primary: for (let key of entry.key) {
const substituted = substituteParams(key);
console.debug(`${entry.uid}: ${substituted}`)
if (substituted && matchKeys(textToScan, substituted.trim())) {
console.debug(`${entry.uid}: got primary match`)
//selective logic begins
if (
entry.selective && //all entries are selective now
Array.isArray(entry.keysecondary) && //always true
entry.keysecondary.length //ignore empties
) {
console.debug(`uid:${entry.uid}: checking logic: ${entry.selectiveLogic}`)
secondary: for (let keysecondary of entry.keysecondary) {
const secondarySubstituted = substituteParams(keysecondary);
console.debug(`uid:${entry.uid}: filtering ${secondarySubstituted}`);
// If selectiveLogic isn't found, assume it's AND
const selectiveLogic = entry.selectiveLogic ?? 0;
//AND operator
if (selectiveLogic === 0) {
console.debug('saw AND logic, checking..')
if (secondarySubstituted && matchKeys(textToScan, secondarySubstituted.trim())) {
console.debug(`activating entry ${entry.uid} with AND found`)
activatedNow.add(entry);
break secondary;
}
}
//NOT operator
if (selectiveLogic === 1) {
console.debug(`uid ${entry.uid}: checking NOT logic for ${secondarySubstituted}`)
if (secondarySubstituted && matchKeys(textToScan, secondarySubstituted.trim())) {
console.debug(`uid ${entry.uid}: canceled; filtered out by ${secondarySubstituted}`)
break primary;
} else {
console.debug(`${entry.uid}: activated; passed NOT filter`)
activatedNow.add(entry);
break secondary;
}
}
}
//handle cases where secondary is empty
} else {
console.debug(`uid ${entry.uid}: activated without filter logic`)
activatedNow.add(entry);
break primary;
}
} else { console.debug('no active entries for logic checks yet') }
}
}
}
needsToScan = world_info_recursive && activatedNow.size > 0;
const newEntries = [...activatedNow]
.sort((a, b) => sortedEntries.indexOf(a) - sortedEntries.indexOf(b));
let newContent = "";
const textToScanTokens = getTokenCount(allActivatedText);
const probabilityChecksBefore = failedProbabilityChecks.size;
console.debug(`-- PROBABILITY CHECKS BEGIN --`)
for (const entry of newEntries) {
const rollValue = Math.random() * 100;
if (entry.useProbability && rollValue > entry.probability) {
console.debug(`WI entry ${entry.uid} ${entry.key} failed probability check, skipping`);
failedProbabilityChecks.add(entry);
continue;
} else { console.debug(`uid:${entry.uid} passed probability check, inserting to prompt`) }
newContent += `${substituteParams(entry.content)}\n`;
if (textToScanTokens + getTokenCount(newContent) >= budget) {
console.debug(`WI budget reached, stopping`);
if (world_info_overflow_alert) {
console.log("Alerting");
toastr.warning(`World info budget reached after ${count} entries.`, 'World Info');
}
needsToScan = false;
break;
}
allActivatedEntries.add(entry);
console.debug('WI entry activated:', entry);
}
const probabilityChecksAfter = failedProbabilityChecks.size;
if ((probabilityChecksAfter - probabilityChecksBefore) === activatedNow.size) {
console.debug(`WI probability checks failed for all activated entries, stopping`);
needsToScan = false;
}
if (needsToScan) {
const text = newEntries
.filter(x => !failedProbabilityChecks.has(x))
.map(x => x.content).join('\n');
const currentlyActivatedText = transformString(text);
textToScan = (currentlyActivatedText + '\n' + textToScan);
allActivatedText = (currentlyActivatedText + '\n' + allActivatedText);
}
}
// Forward-sorted list of entries for joining
const WIBeforeEntries = [];
const WIAfterEntries = [];
const ANTopEntries = [];
const ANBottomEntries = [];
// Appends from insertion order 999 to 1. Use unshift for this purpose
[...allActivatedEntries].sort(sortFn).forEach((entry) => {
switch (entry.position) {
case world_info_position.before:
WIBeforeEntries.unshift(substituteParams(entry.content));
break;
case world_info_position.after:
WIAfterEntries.unshift(substituteParams(entry.content));
break;
case world_info_position.ANTop:
ANTopEntries.unshift(entry.content);
break;
case world_info_position.ANBottom:
ANBottomEntries.unshift(entry.content);
break;
default:
break;
}
});
const worldInfoBefore = WIBeforeEntries.length ? WIBeforeEntries.join("\n") : '';
const worldInfoAfter = WIAfterEntries.length ? WIAfterEntries.join("\n") : '';
if (shouldWIAddPrompt) {
const originalAN = context.extensionPrompts[NOTE_MODULE_NAME].value;
const ANWithWI = `${ANTopEntries.join("\n")}\n${originalAN}\n${ANBottomEntries.join("\n")}`
context.setExtensionPrompt(NOTE_MODULE_NAME, 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${escapeRegex(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,
probability: null,
useProbability: false,
};
});
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,
probability: entry.activationPercent ?? null,
useProbability: entry.activationPercent ?? false,
};
});
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,
probability: null,
useProbability: false,
};
});
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[entry.id] = {
uid: entry.id,
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,
probability: entry.extensions?.probability ?? null,
useProbability: entry.extensions?.useProbability ?? false,
};
});
return result;
}
export function setWorldInfoButtonClass(chid, forceValue = undefined) {
if (forceValue !== undefined) {
$('#set_character_world, #world_button').toggleClass('world_set', forceValue);
return;
}
if (!chid) {
return;
}
const world = characters[chid]?.data?.extensions?.world;
const worldSet = Boolean(world && world_names.includes(world));
$('#set_character_world, #world_button').toggleClass('world_set', worldSet);
}
export function checkEmbeddedWorld(chid) {
$('#import_character_info').hide();
if (chid === undefined) {
return false;
}
if (characters[chid]?.data?.character_book) {
$('#import_character_info').data('chid', chid).show();
// Only show the alert once per character
const checkKey = `AlertWI_${characters[chid].avatar}`;
const worldName = characters[chid]?.data?.extensions?.world;
if (!localStorage.getItem(checkKey) && (!worldName || !world_names.includes(worldName))) {
toastr.info(
'To import and use it, select "Import Card Lore" in the "More..." dropdown menu on the character panel.',
`${characters[chid].name} has an embedded World/Lorebook`,
{ timeOut: 10000, extendedTimeOut: 20000, positionClass: 'toast-top-center' },
);
localStorage.setItem(checkKey, 1);
}
return true;
}
return false;
}
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');
}
setWorldInfoButtonClass(chid, true);
}
function onWorldInfoChange(_, text) {
if (_ !== '__notSlashCommand__') { // if it's a slash command
if (text !== undefined) { // and args are provided
const slashInputSplitText = text.trim().toLowerCase().split(",");
slashInputSplitText.forEach((worldName) => {
const wiElement = getWIElement(worldName);
if (wiElement.length > 0) {
selected_world_info.push(wiElement.text());
wiElement.prop("selected", true);
toastr.success(`Activated world: ${wiElement.text()}`);
} else {
toastr.error(`No world found named: ${worldName}`);
}
});
$("#world_info").trigger("change");
} else { // if no args, unset all worlds
toastr.success('Deactivated all worlds');
selected_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.`);
}
});
}
selected_world_info = tempWorldInfo;
}
saveSettingsDebounced();
eventSource.emit(event_types.WORLDINFO_SETTINGS_UPDATED);
}
export async function importWorldInfo(file) {
if (!file) {
return;
}
const formData = new FormData();
formData.append('avatar', file);
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) => { },
});
}
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 there's no world names, don't do anything
if (world_names.length === 0) {
e.preventDefault();
return;
}
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];
await importWorldInfo(file);
// Will allow to select the same file twice in a row
e.target.value = '';
});
$("#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 () => {
$("#world_info_search").val('');
worldInfoFilter.setFilterData(FILTER_TYPES.WORLD_INFO_SEARCH, '', true);
const selectedIndex = $("#world_editor_select").find(":selected").val();
if (selectedIndex === "") {
hideWorldEditor();
} else {
const worldName = world_names[selectedIndex];
showWorldEditor(worldName);
}
});
const saveSettings = () => {
saveSettingsDebounced()
eventSource.emit(event_types.WORLDINFO_SETTINGS_UPDATED);
}
$(document).on("input", "#world_info_depth", function () {
world_info_depth = Number($(this).val());
$("#world_info_depth_counter").text($(this).val());
saveSettings();
});
$(document).on("input", "#world_info_budget", function () {
world_info_budget = Number($(this).val());
$("#world_info_budget_counter").text($(this).val());
saveSettings();
});
$(document).on("input", "#world_info_recursive", function () {
world_info_recursive = !!$(this).prop('checked');
saveSettings();
})
$('#world_info_case_sensitive').on('input', function () {
world_info_case_sensitive = !!$(this).prop('checked');
saveSettings();
})
$('#world_info_match_whole_words').on('input', function () {
world_info_match_whole_words = !!$(this).prop('checked');
saveSettings();
});
$('#world_info_character_strategy').on('change', function () {
world_info_character_strategy = $(this).val();
saveSettings();
});
$('#world_info_overflow_alert').on('change', function () {
world_info_overflow_alert = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#world_info_budget_cap').on('input', function () {
world_info_budget_cap = Number($(this).val());
$("#world_info_budget_cap_counter").text(world_info_budget_cap);
saveSettings();
});
$('#world_button').on('click', async function () {
const chid = $('#set_character_world').data('chid');
if (chid) {
const worldName = characters[chid]?.data?.extensions?.world;
const hasEmbed = checkEmbeddedWorld(chid);
if (worldName && world_names.includes(worldName)) {
if (!$('#WorldInfo').is(':visible')) {
$('#WIDrawerIcon').trigger('click');
}
const index = world_names.indexOf(worldName);
$("#world_editor_select").val(index).trigger('change');
} else if (hasEmbed) {
await importEmbeddedWorldInfo();
saveCharacterDebounced();
}
else {
$('#char-management-dropdown').val($('#set_character_world').val()).trigger('change');
}
}
});
$('#world_info_search').on('input', function () {
const term = $(this).val();
worldInfoFilter.setFilterData(FILTER_TYPES.WORLD_INFO_SEARCH, term);
});
// Not needed on mobile
if (deviceInfo.device.type === 'desktop') {
$('#world_info').select2({
width: '100%',
placeholder: 'No Worlds active. Click here to select.',
allowClear: true,
closeOnSelect: false,
});
}
})