mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2024-12-14 02:16:14 +01:00
2c1a6ca67c
Add support for adding extension prompts into WI scanning. Doing this required adding a method to get extension prompts by name. Now, AN and depth prompts can be added to world info scanning. However, since scanning just looks for keys in the chatlog, append the extension prompts on the top of scanned text. Signed-off-by: kingbri <bdashore3@proton.me>
2065 lines
72 KiB
JavaScript
2065 lines
72 KiB
JavaScript
import { saveSettings, callPopup, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPrompt, MAX_INJECTION_DEPTH, extension_prompt_types, getExtensionPromptByName } from "../script.js";
|
||
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition } from "./utils.js";
|
||
import { extension_settings, getContext } from "./extensions.js";
|
||
import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from "./authors-note.js";
|
||
import { registerSlashCommand } from "./slash-commands.js";
|
||
import { getDeviceInfo } from "./RossAscends-mods.js";
|
||
import { FILTER_TYPES, FilterHelper } from "./filters.js";
|
||
import { getTokenCount } from "./tokenizers.js";
|
||
import { power_user } from "./power-user.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());
|
||
const SORT_ORDER_KEY = 'world_info_sort_order';
|
||
|
||
const InputWidthReference = $("#WIInputWidthReference");
|
||
|
||
const DEFAULT_DEPTH = 4;
|
||
|
||
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,
|
||
atDepth: 4,
|
||
};
|
||
|
||
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,
|
||
worldInfoDepth: activatedWorldInfo.WIDepthEntries
|
||
};
|
||
}
|
||
|
||
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_info_sort_order').val(localStorage.getItem(SORT_ORDER_KEY) || '0');
|
||
$("#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;
|
||
}
|
||
|
||
/**
|
||
* @param {any[]} data WI entries
|
||
* @returns {any[]} Sorted data
|
||
*/
|
||
function sortEntries(data) {
|
||
const option = $('#world_info_sort_order').find(":selected");
|
||
const sortField = option.data('field');
|
||
const sortOrder = option.data('order');
|
||
const sortRule = option.data('rule');
|
||
const orderSign = sortOrder === 'asc' ? 1 : -1;
|
||
|
||
if (sortRule === 'priority') {
|
||
// First constant, then normal, then disabled. Then sort by order
|
||
data.sort((a, b) => {
|
||
const aValue = a.constant ? 0 : a.disable ? 2 : 1;
|
||
const bValue = b.constant ? 0 : b.disable ? 2 : 1;
|
||
|
||
return (aValue - bValue || b.order - a.order);
|
||
});
|
||
} else {
|
||
const primarySort = (a, b) => {
|
||
const aValue = a[sortField];
|
||
const bValue = b[sortField];
|
||
|
||
// Sort strings
|
||
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
||
if (sortRule === 'length') {
|
||
// Sort by string length
|
||
return orderSign * (aValue.length - bValue.length);
|
||
} else {
|
||
// Sort by A-Z ordinal
|
||
return orderSign * aValue.localeCompare(bValue);
|
||
}
|
||
}
|
||
|
||
// Sort numbers
|
||
return orderSign * (Number(aValue) - Number(bValue));
|
||
};
|
||
const secondarySort = (a, b) => a.order - b.order;
|
||
const tertiarySort = (a, b) => a.uid - b.uid;
|
||
|
||
data.sort((a, b) => {
|
||
const primary = primarySort(a, b);
|
||
|
||
if (primary !== 0) {
|
||
return primary;
|
||
}
|
||
|
||
const secondary = secondarySort(a, b);
|
||
|
||
if (secondary !== 0) {
|
||
return secondary;
|
||
}
|
||
|
||
return tertiarySort(a, b);
|
||
});
|
||
}
|
||
|
||
return data;
|
||
}
|
||
|
||
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 = sortEntries(entriesArray);
|
||
entriesArray = worldInfoFilter.applyFilters(entriesArray);
|
||
typeof callback === 'function' && callback(entriesArray);
|
||
return entriesArray;
|
||
}
|
||
|
||
let startPage = 1;
|
||
|
||
if (navigation === navigation_option.previous) {
|
||
startPage = $("#world_info_pagination").pagination('getCurrentPageNum');
|
||
}
|
||
|
||
const storageKey = 'WI_PerPage';
|
||
const perPageDefault = 25;
|
||
$("#world_info_pagination").pagination({
|
||
dataSource: getDataArray,
|
||
pageSize: Number(localStorage.getItem(storageKey)) || perPageDefault,
|
||
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 keywordHeaders = `
|
||
<div id="WIEntryHeaderTitlesPC" class="flex-container wide100p spaceBetween justifyCenter textAlignCenter" style="padding:0 2.5em;">
|
||
<small class="flex1">
|
||
Title/Memo
|
||
</small>
|
||
<small style="width:${InputWidthReference.width() + 5 + 'px'}">
|
||
Status
|
||
</small>
|
||
<small style="width:${InputWidthReference.width() + 20 + 'px'}">
|
||
Position
|
||
</small>
|
||
<small style="width:${InputWidthReference.width() + 15 + 'px'}">
|
||
Depth
|
||
</small>
|
||
<small style="width:${InputWidthReference.width() + 15 + 'px'}">
|
||
Order
|
||
</small>
|
||
<small style="width:${InputWidthReference.width() + 15 + 'px'}">
|
||
Trigger %
|
||
</small>
|
||
|
||
</div>`
|
||
const blocks = page.map(entry => getWorldEntry(name, data, entry));
|
||
$("#world_popup_entries_list").append(keywordHeaders);
|
||
$("#world_popup_entries_list").append(blocks);
|
||
},
|
||
afterSizeSelectorChange: function (e) {
|
||
localStorage.setItem(storageKey, e.target.value);
|
||
}
|
||
});
|
||
|
||
if (typeof navigation === 'number' && Number(navigation) >= 0) {
|
||
const selector = `#world_popup_entries_list [uid="${navigation}"]`;
|
||
const data = getDataArray();
|
||
const uidIndex = data.findIndex(x => x.uid === navigation);
|
||
const perPage = Number(localStorage.getItem(storageKey)) || perPageDefault;
|
||
const page = Math.floor(uidIndex / perPage) + 1;
|
||
$("#world_info_pagination").pagination('go', page);
|
||
waitUntilCondition(() => document.querySelector(selector) !== null).finally(() => {
|
||
const element = $(selector);
|
||
|
||
if (element.length === 0) {
|
||
console.log(`Could not find element for uid ${navigation}`);
|
||
return;
|
||
}
|
||
|
||
const elementOffset = element.offset();
|
||
const parentOffset = element.parent().offset();
|
||
const scrollOffset = elementOffset.top - parentOffset.top;
|
||
$('#WorldInfo').scrollTop(scrollOffset);
|
||
element.addClass('flash animated');
|
||
setTimeout(() => element.removeClass('flash animated'), 2000);
|
||
});
|
||
}
|
||
|
||
$("#world_popup_new").off('click').on('click', () => {
|
||
createWorldInfoEntry(name, data);
|
||
});
|
||
|
||
$("#world_popup_name_button").off('click').on('click', async () => {
|
||
await renameWorldInfo(name, data);
|
||
});
|
||
|
||
$("#world_backfill_memos").off('click').on('click', async () => {
|
||
let counter = 0;
|
||
for (const entry of Object.values(data.entries)) {
|
||
if (!entry.comment && Array.isArray(entry.key) && entry.key.length > 0) {
|
||
entry.comment = entry.key[0];
|
||
setOriginalDataValue(data, entry.uid, "comment", entry.comment);
|
||
counter++;
|
||
}
|
||
}
|
||
|
||
if (counter > 0) {
|
||
toastr.info(`Backfilled ${counter} titles`);
|
||
await saveWorldInfo(name, data, true);
|
||
updateEditor(navigation_option.previous);
|
||
}
|
||
});
|
||
|
||
$("#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);
|
||
template.attr("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("click", function (event) {
|
||
event.stopPropagation();
|
||
})
|
||
|
||
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");
|
||
|
||
// Character filter
|
||
const characterFilterLabel = template.find(`label[for="characterFilter"] > small`);
|
||
characterFilterLabel.text(!!(entry.characterFilter?.isExclude) ? "Exclude Character(s)" : "Filter to Character(s)");
|
||
|
||
// exclude characters checkbox
|
||
const characterExclusionInput = template.find(`input[name="character_exclusion"]`);
|
||
characterExclusionInput.data("uid", entry.uid);
|
||
characterExclusionInput.on("input", function () {
|
||
const uid = $(this).data("uid");
|
||
const value = $(this).prop("checked");
|
||
characterFilterLabel.text(value ? "Exclude Character(s)" : "Filter to Character(s)");
|
||
if (data.entries[uid].characterFilter) {
|
||
if (!value && data.entries[uid].characterFilter.names.length === 0) {
|
||
delete data.entries[uid].characterFilter;
|
||
} else {
|
||
data.entries[uid].characterFilter.isExclude = value
|
||
}
|
||
} else if (value) {
|
||
Object.assign(
|
||
data.entries[uid],
|
||
{
|
||
characterFilter: {
|
||
isExclude: true,
|
||
names: []
|
||
}
|
||
}
|
||
);
|
||
}
|
||
|
||
setOriginalDataValue(data, uid, "character_filter", data.entries[uid].characterFilter);
|
||
saveWorldInfo(name, data);
|
||
});
|
||
characterExclusionInput.prop("checked", entry.characterFilter?.isExclude ?? false).trigger("input");
|
||
|
||
const characterFilter = template.find(`select[name="characterFilter"]`);
|
||
characterFilter.data("uid", entry.uid)
|
||
const deviceInfo = getDeviceInfo();
|
||
if (deviceInfo && deviceInfo.device.type === 'desktop') {
|
||
$(characterFilter).select2({
|
||
width: '100%',
|
||
placeholder: 'All characters will pull from this entry.',
|
||
allowClear: true,
|
||
closeOnSelect: false,
|
||
});
|
||
}
|
||
const characters = getContext().characters;
|
||
characters.forEach((character) => {
|
||
const option = document.createElement('option');
|
||
const name = character.avatar.replace(/\.[^/.]+$/, "") ?? character.name
|
||
option.innerText = name
|
||
option.selected = entry.characterFilter?.names.includes(name)
|
||
characterFilter.append(option)
|
||
});
|
||
|
||
characterFilter.on('mousedown change', async function (e) {
|
||
// If there's no world names, don't do anything
|
||
if (world_names.length === 0) {
|
||
e.preventDefault();
|
||
return;
|
||
}
|
||
|
||
const uid = $(this).data("uid");
|
||
const value = $(this).val();
|
||
if ((!value || value?.length === 0) && !data.entries[uid].characterFilter?.isExclude) {
|
||
delete data.entries[uid].characterFilter;
|
||
} else {
|
||
Object.assign(
|
||
data.entries[uid],
|
||
{
|
||
characterFilter: {
|
||
isExclude: data.entries[uid].characterFilter?.isExclude ?? false,
|
||
names: value
|
||
}
|
||
}
|
||
);
|
||
}
|
||
setOriginalDataValue(data, uid, "character_filter", data.entries[uid].characterFilter);
|
||
saveWorldInfo(name, data);
|
||
});
|
||
|
||
// 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();
|
||
resetScrollHeight(this);
|
||
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");
|
||
initScrollHeight(commentInput);
|
||
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;
|
||
updatePosOrdDisplay(uid)
|
||
setOriginalDataValue(data, uid, "insertion_order", data.entries[uid].order);
|
||
saveWorldInfo(name, data);
|
||
});
|
||
orderInput.val(entry.order).trigger("input");
|
||
orderInput.width(InputWidthReference.width() + 15 + 'px')
|
||
|
||
// probability
|
||
if (entry.probability === undefined) {
|
||
entry.probability = null;
|
||
}
|
||
|
||
// depth
|
||
const depthInput = template.find('input[name="depth"]');
|
||
depthInput.data("uid", entry.uid);
|
||
|
||
depthInput.on("input", function () {
|
||
const uid = $(this).data("uid");
|
||
const value = Number($(this).val());
|
||
|
||
data.entries[uid].depth = !isNaN(value) ? value : 0;
|
||
updatePosOrdDisplay(uid)
|
||
setOriginalDataValue(data, uid, "extensions.depth", data.entries[uid].depth);
|
||
saveWorldInfo(name, data);
|
||
});
|
||
depthInput.val(entry.depth ?? DEFAULT_DEPTH).trigger("input");
|
||
depthInput.width(InputWidthReference.width() + 15 + 'px');
|
||
|
||
// Hide by default unless depth is specified
|
||
if (entry.position === world_info_position.atDepth) {
|
||
//depthInput.parent().hide();
|
||
}
|
||
|
||
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");
|
||
probabilityInput.width(InputWidthReference.width() + 15 + 'px')
|
||
|
||
// 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"]');
|
||
initScrollHeight(positionInput);
|
||
positionInput.data("uid", entry.uid);
|
||
positionInput.on("click", function (event) {
|
||
// Prevent closing the drawer on clicking the input
|
||
event.stopPropagation();
|
||
});
|
||
positionInput.on("input", function () {
|
||
const uid = $(this).data("uid");
|
||
const value = Number($(this).val());
|
||
data.entries[uid].position = !isNaN(value) ? value : 0;
|
||
if (value === world_info_position.atDepth) {
|
||
depthInput.prop('disabled', false);
|
||
depthInput.css('visibility', 'visible')
|
||
//depthInput.parent().show();
|
||
} else {
|
||
depthInput.prop('disabled', true);
|
||
depthInput.css('visibility', 'hidden')
|
||
//depthInput.parent().hide();
|
||
}
|
||
updatePosOrdDisplay(uid)
|
||
// 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");
|
||
|
||
//add UID above content box (less important doesn't need to be always visible)
|
||
template.find(".world_entry_form_uid_value").text(`(UID: ${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");
|
||
*/
|
||
|
||
//new tri-state selector for constant/normal/disabled
|
||
const entryStateSelector = template.find('select[name="entryStateSelector"]');
|
||
entryStateSelector.data("uid", entry.uid);
|
||
console.log(entry.uid)
|
||
entryStateSelector.on("click", function (event) {
|
||
// Prevent closing the drawer on clicking the input
|
||
event.stopPropagation();
|
||
});
|
||
entryStateSelector.on("input", function () {
|
||
const uid = entry.uid;
|
||
const value = $(this).val();
|
||
switch (value) {
|
||
case "constant":
|
||
data.entries[uid].constant = true;
|
||
data.entries[uid].disable = false;
|
||
setOriginalDataValue(data, uid, "enabled", true);
|
||
setOriginalDataValue(data, uid, "constant", true);
|
||
template.removeClass('disabledWIEntry');
|
||
console.debug("set to constant")
|
||
break
|
||
case "normal":
|
||
data.entries[uid].constant = false;
|
||
data.entries[uid].disable = false;
|
||
setOriginalDataValue(data, uid, "enabled", true);
|
||
setOriginalDataValue(data, uid, "constant", false);
|
||
template.removeClass('disabledWIEntry');
|
||
console.debug("set to normal")
|
||
break
|
||
case "disabled":
|
||
data.entries[uid].constant = false;
|
||
data.entries[uid].disable = true;
|
||
setOriginalDataValue(data, uid, "enabled", false);
|
||
setOriginalDataValue(data, uid, "constant", false);
|
||
template.addClass('disabledWIEntry');
|
||
console.debug("set to disabled")
|
||
break
|
||
}
|
||
saveWorldInfo(name, data);
|
||
|
||
})
|
||
|
||
const entryState = function () {
|
||
|
||
console.log(`constant: ${entry.constant}, disabled: ${entry.disable}`)
|
||
if (entry.constant === true) {
|
||
console.debug('found constant')
|
||
return "constant"
|
||
} else if (entry.disable === true) {
|
||
console.debug('found disabled')
|
||
return "disabled"
|
||
} else {
|
||
console.debug('found normal')
|
||
return "normal"
|
||
}
|
||
|
||
}
|
||
template
|
||
.find(`select[name="entryStateSelector"] option[value=${entryState()}]`)
|
||
.prop("selected", true)
|
||
.trigger("input");
|
||
|
||
saveWorldInfo(name, data);
|
||
|
||
// exclude recursion
|
||
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(".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
|
||
|
||
function updatePosOrdDisplay(uid) {
|
||
// display position/order info left of keyword box
|
||
let entry = data.entries[uid]
|
||
let posText = entry.position
|
||
switch (entry.position) {
|
||
case 0:
|
||
posText = '↑CD';
|
||
break
|
||
case 1:
|
||
posText = 'CD↓';
|
||
break
|
||
case 2:
|
||
posText = '↑AN';
|
||
break
|
||
case 3:
|
||
posText = 'AN↓';
|
||
break
|
||
case 4:
|
||
posText = `@D${entry.depth}`;
|
||
break
|
||
}
|
||
template.find(".world_entry_form_position_value").text(`(${posText} ${entry.order})`);
|
||
}
|
||
|
||
return template;
|
||
}
|
||
|
||
async function deleteWorldInfoEntry(data, uid) {
|
||
if (!data || !("entries" in data)) {
|
||
return;
|
||
}
|
||
|
||
if (!confirm(`Delete the entry with UID: ${uid}? This action is irreversible!`)) {
|
||
throw new Error("User cancelled deletion");
|
||
}
|
||
|
||
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(newUid);
|
||
}
|
||
|
||
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 structuredClone(entries);
|
||
}
|
||
catch (e) {
|
||
console.error(e);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
async function checkWorldInfo(chat, maxContext) {
|
||
const context = getContext();
|
||
const messagesToLookBack = world_info_depth * 2 || 1;
|
||
|
||
// Combine the chat
|
||
let textToScan = chat.slice(0, messagesToLookBack).join("");
|
||
|
||
// Add the depth or AN if enabled
|
||
// Put this code here since otherwise, the chat reference is modified
|
||
if (extension_settings.note.allowWIScan) {
|
||
let depthPrompt = getExtensionPromptByName("DEPTH_PROMPT")
|
||
if (depthPrompt) {
|
||
textToScan = `${depthPrompt}\n${textToScan}`
|
||
}
|
||
|
||
let anPrompt = getExtensionPromptByName(NOTE_MODULE_NAME);
|
||
if (anPrompt) {
|
||
textToScan = `${anPrompt}\n${textToScan}`
|
||
}
|
||
}
|
||
|
||
// Transform the resulting string
|
||
textToScan = transformString(textToScan);
|
||
|
||
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) {
|
||
// Check if this entry applies to the character or if it's excluded
|
||
if (entry.characterFilter && entry.characterFilter?.names.length > 0) {
|
||
const nameIncluded = entry.characterFilter.names.includes(getCharaFilename());
|
||
const filtered = entry.characterFilter.isExclude ? nameIncluded : !nameIncluded
|
||
|
||
if (filtered) {
|
||
continue;
|
||
}
|
||
}
|
||
|
||
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 ${allActivatedEntries.size} 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 = [];
|
||
const WIDepthEntries = [];
|
||
|
||
// 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;
|
||
case world_info_position.atDepth:
|
||
const existingDepthIndex = WIDepthEntries.findIndex((e) => e.depth === entry.depth ?? DEFAULT_DEPTH);
|
||
if (existingDepthIndex !== -1) {
|
||
WIDepthEntries[existingDepthIndex].entries.unshift(entry.content);
|
||
} else {
|
||
WIDepthEntries.push({
|
||
depth: entry.depth,
|
||
entries: [entry.content]
|
||
});
|
||
}
|
||
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, WIDepthEntries };
|
||
}
|
||
|
||
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,
|
||
depth: entry.extensions?.depth ?? DEFAULT_DEPTH,
|
||
};
|
||
});
|
||
|
||
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))) {
|
||
localStorage.setItem(checkKey, 1);
|
||
|
||
if (power_user.world_import_dialog) {
|
||
callPopup(`<h3>This character has an embedded World/Lorebook.</h3>
|
||
<h3>Would you like to import it now?</h3>
|
||
<div class="m-b-1">If you want to import it later, select "Import Card Lore" in the "More..." dropdown menu on the character panel.</div>`,
|
||
'confirm',
|
||
'',
|
||
{ okButton: 'Yes', })
|
||
.then((result) => {
|
||
if (result) {
|
||
importEmbeddedWorldInfo(true);
|
||
}
|
||
});
|
||
}
|
||
else {
|
||
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: 5000, extendedTimeOut: 10000, positionClass: 'toast-top-center' },
|
||
);
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
export async function importEmbeddedWorldInfo(skipPopup = false) {
|
||
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.' : '');
|
||
|
||
if (!skipPopup) {
|
||
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) {
|
||
//show&draw the WI panel before..
|
||
$("#WIDrawerIcon").trigger('click');
|
||
//..auto-opening the new imported WI
|
||
$("#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, [], '<span class="monospace">(optional name)</span> – 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);
|
||
});
|
||
|
||
$('#world_refresh').on('click', () => {
|
||
updateEditor(navigation_option.previous);
|
||
});
|
||
|
||
$('#world_info_sort_order').on('change', function (e) {
|
||
if (e.target instanceof HTMLOptionElement) {
|
||
localStorage.setItem(SORT_ORDER_KEY, e.target.value);
|
||
}
|
||
|
||
updateEditor(navigation_option.none);
|
||
})
|
||
|
||
// Not needed on mobile
|
||
const deviceInfo = getDeviceInfo();
|
||
if (deviceInfo && deviceInfo.device.type === 'desktop') {
|
||
$('#world_info').select2({
|
||
width: '100%',
|
||
placeholder: 'No Worlds active. Click here to select.',
|
||
allowClear: true,
|
||
closeOnSelect: false,
|
||
});
|
||
}
|
||
})
|