mirror of
				https://github.com/SillyTavern/SillyTavern.git
				synced 2025-06-05 21:59:27 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			1194 lines
		
	
	
		
			39 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1194 lines
		
	
	
		
			39 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
import { saveSettings, callPopup, substituteParams, getTokenCount, getRequestHeaders, chat_metadata, this_chid, characters } from "../script.js";
 | 
						||
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer } from "./utils.js";
 | 
						||
import { getContext } from "./extensions.js";
 | 
						||
import { metadata_keys, shouldWIAddPrompt } from "./extensions/floating-prompt/index.js";
 | 
						||
import { registerSlashCommand } from "./slash-commands.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 = null;
 | 
						||
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;
 | 
						||
    }
 | 
						||
 | 
						||
    $("#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 : [];
 | 
						||
 | 
						||
    if (settings.world_info != undefined) {
 | 
						||
        if (world_names.includes(settings.world_info)) {
 | 
						||
            world_info = settings.world_info;
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    world_names.forEach((item, i) => {
 | 
						||
        $("#world_info").append(`<option value='${i}'>${item}</option>`);
 | 
						||
        $("#world_editor_select").append(`<option value='${i}'>${item}</option>`);
 | 
						||
        // preselect world if saved
 | 
						||
        if (item == world_info) {
 | 
						||
            $("#world_info").val(i).trigger('change');
 | 
						||
        }
 | 
						||
    });
 | 
						||
 | 
						||
    $("#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}'>${item}</option>`);
 | 
						||
            $("#world_editor_select").append(`<option value='${i}'>${item}</option>`);
 | 
						||
        });
 | 
						||
    }
 | 
						||
}
 | 
						||
 | 
						||
function hideWorldEditor() {
 | 
						||
    displayWorldEntries(null, null);
 | 
						||
}
 | 
						||
 | 
						||
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;
 | 
						||
        }
 | 
						||
 | 
						||
        await deleteWorldInfo(name, world_info);
 | 
						||
    });
 | 
						||
 | 
						||
    // 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;
 | 
						||
    }
 | 
						||
 | 
						||
    let selectNewName = null;
 | 
						||
    if (oldName === world_info) {
 | 
						||
        console.debug("Renaming current world info");
 | 
						||
        world_info = newName;
 | 
						||
        selectNewName = newName;
 | 
						||
    }
 | 
						||
    else {
 | 
						||
        console.debug("Renaming non-current world info");
 | 
						||
        selectNewName = world_info;
 | 
						||
    }
 | 
						||
 | 
						||
    await saveWorldInfo(newName, data, true);
 | 
						||
    await deleteWorldInfo(oldName, selectNewName);
 | 
						||
 | 
						||
    const selectedIndex = world_names.indexOf(newName);
 | 
						||
    if (selectedIndex !== -1) {
 | 
						||
        $('#world_editor_select').val(selectedIndex).trigger('change');
 | 
						||
    }
 | 
						||
}
 | 
						||
 | 
						||
async function deleteWorldInfo(worldInfoName, selectWorldName) {
 | 
						||
    if (!world_names.includes(worldInfoName)) {
 | 
						||
        return;
 | 
						||
    }
 | 
						||
 | 
						||
    const response = await fetch("/deleteworldinfo", {
 | 
						||
        method: "POST",
 | 
						||
        headers: getRequestHeaders(),
 | 
						||
        body: JSON.stringify({ name: worldInfoName }),
 | 
						||
    });
 | 
						||
 | 
						||
    if (response.ok) {
 | 
						||
        await updateWorldInfoList();
 | 
						||
 | 
						||
        const selectedIndex = world_names.indexOf(selectWorldName);
 | 
						||
        if (selectedIndex !== -1) {
 | 
						||
            $("#world_info").val(selectedIndex).trigger('change');
 | 
						||
        } else {
 | 
						||
            $("#world_info").val("").trigger('change');
 | 
						||
        }
 | 
						||
 | 
						||
        $('#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;
 | 
						||
    }
 | 
						||
 | 
						||
    world_info = worldInfoName;
 | 
						||
    await saveWorldInfo(world_info, worldInfoTemplate, true);
 | 
						||
    await updateWorldInfoList();
 | 
						||
 | 
						||
    const selectedIndex = world_names.indexOf(worldInfoName);
 | 
						||
    if (selectedIndex !== -1) {
 | 
						||
        $("#world_info").val(selectedIndex).trigger('change');
 | 
						||
        $('#world_editor_select').val(selectedIndex).trigger('change');
 | 
						||
    } else {
 | 
						||
        $("#world_info").val("").trigger('change');
 | 
						||
        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 (name === world_info) {
 | 
						||
        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 [];
 | 
						||
    }
 | 
						||
 | 
						||
    if (!world_names.includes(world_info)) {
 | 
						||
        console.log(`Global ${characters[this_chid]?.name} world info does not exist: ${world_info}`);
 | 
						||
        return [];
 | 
						||
    }
 | 
						||
 | 
						||
    const data = await loadWorldInfoData(world_info);
 | 
						||
    const entries = data ? Object.keys(data.entries).map((x) => data.entries[x]) : [];
 | 
						||
    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 selectedWorld;
 | 
						||
    if (_ !== '__notSlashCommand__') { //if it's a slash command
 | 
						||
        if (text !== undefined) { //and args are provided
 | 
						||
            let slashInputWorld = text.toLowerCase();
 | 
						||
            $("#world_info").find(`option`).filter(function () {
 | 
						||
                return $(this).text().toLowerCase() === slashInputWorld;
 | 
						||
            }).prop('selected', true); //matches arg with worldnames and selects; if none found, unsets world
 | 
						||
            let setWorldName = $("#world_info").find(":selected").text(); //only for toastr display
 | 
						||
            toastr.success(`Active world: ${setWorldName}`);
 | 
						||
            selectedWorld = $("#world_info").find(":selected").val();
 | 
						||
        } else { //if no args, unset world
 | 
						||
            toastr.success('Deselected World')
 | 
						||
            $("#world_info").val("");
 | 
						||
        }
 | 
						||
    } else { //if it's a pointer selection
 | 
						||
        selectedWorld = $("#world_info").find(":selected").val();
 | 
						||
    }
 | 
						||
    world_info = null;
 | 
						||
    if (selectedWorld !== "") {
 | 
						||
        const worldIndex = Number(selectedWorld);
 | 
						||
        world_info = !isNaN(worldIndex) ? world_names[worldIndex] : null;
 | 
						||
    }
 | 
						||
    saveSettingsDebounced();
 | 
						||
}
 | 
						||
 | 
						||
jQuery(() => {
 | 
						||
 | 
						||
    $(document).ready(function () {
 | 
						||
        registerSlashCommand('world', onWorldInfoChange, [], "– sets active World, or unsets if no args provided", true, true);
 | 
						||
    })
 | 
						||
    $("#world_info").on('change', async function () { 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();
 | 
						||
    })
 | 
						||
});
 |