diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index bf9a2a43d..2cadea719 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -18,33 +18,13 @@ import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js'; import { StructuredCloneMap } from './util/StructuredCloneMap.js'; -export { - world_info, - world_info_budget, - world_info_depth, - world_info_min_activations, - world_info_min_activations_depth_max, - world_info_include_names, - 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 = { +export const world_info_insertion_strategy = { evenly: 0, character_first: 1, global_first: 2, }; -const world_info_logic = { +export const world_info_logic = { AND_ANY: 0, NOT_ALL: 1, NOT_ANY: 2, @@ -54,7 +34,7 @@ const world_info_logic = { /** * @enum {number} Possible states of the WI evaluation */ -const scan_state = { +export const scan_state = { /** * The scan will be stopped. */ @@ -75,23 +55,23 @@ const scan_state = { const WI_ENTRY_EDIT_TEMPLATE = $('#entry_edit_template .world_entry'); -let world_info = {}; -let selected_world_info = []; +export let world_info = {}; +export let selected_world_info = []; /** @type {string[]} */ -let world_names; -let world_info_depth = 2; -let world_info_min_activations = 0; // if > 0, will continue seeking chat until minimum world infos are activated -let world_info_min_activations_depth_max = 0; // used when (world_info_min_activations > 0) +export let world_names; +export let world_info_depth = 2; +export let world_info_min_activations = 0; // if > 0, will continue seeking chat until minimum world infos are activated +export let world_info_min_activations_depth_max = 0; // used when (world_info_min_activations > 0) -let world_info_budget = 25; -let world_info_include_names = true; -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_use_group_scoring = false; -let world_info_character_strategy = world_info_insertion_strategy.character_first; -let world_info_budget_cap = 0; +export let world_info_budget = 25; +export let world_info_include_names = true; +export let world_info_recursive = false; +export let world_info_overflow_alert = false; +export let world_info_case_sensitive = false; +export let world_info_match_whole_words = false; +export let world_info_use_group_scoring = false; +export let world_info_character_strategy = world_info_insertion_strategy.character_first; +export let world_info_budget_cap = 0; const saveWorldDebounced = debounce(async (name, data) => await _save(name, data), debounce_timeout.relaxed); const saveSettingsDebounced = debounce(() => { Object.assign(world_info, { globalSelect: selected_world_info }); @@ -101,13 +81,13 @@ const sortFn = (a, b) => b.order - a.order; let updateEditor = (navigation, flashOnNav = true) => { console.debug('Triggered WI navigation', navigation, flashOnNav); }; // 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 METADATA_KEY = 'world_info'; +export const worldInfoFilter = new FilterHelper(() => updateEditor()); +export const SORT_ORDER_KEY = 'world_info_sort_order'; +export const METADATA_KEY = 'world_info'; -const DEFAULT_DEPTH = 4; -const DEFAULT_WEIGHT = 100; -const MAX_SCAN_DEPTH = 1000; +export const DEFAULT_DEPTH = 4; +export const DEFAULT_WEIGHT = 100; +export const MAX_SCAN_DEPTH = 1000; const KNOWN_DECORATORS = ['@@activate', '@@dont_activate']; // Typedef area @@ -732,7 +712,7 @@ export function getWorldInfoSettings() { }; } -const world_info_position = { +export const world_info_position = { before: 0, after: 1, ANTop: 2, @@ -747,8 +727,18 @@ export const wi_anchor_position = { after: 1, }; -/** @type {StructuredCloneMap} */ -const worldInfoCache = new StructuredCloneMap({ cloneOnGet: true, cloneOnSet: false }); +/** + * The cache of all world info data that was loaded from the backend. + * + * Calling `loadWorldInfo` will fill this cache and utilize this cache, so should be the preferred way to load any world info data. + * Only use the cache directly if you need synchronous access. + * + * This will return a deep clone of the data, so no way to modify the data without actually saving it. + * Should generally be only used for readonly access. + * + * @type {StructuredCloneMap} + * */ +export const worldInfoCache = new StructuredCloneMap({ cloneOnGet: true, cloneOnSet: false }); /** * Gets the world info based on chat messages. @@ -758,7 +748,7 @@ const worldInfoCache = new StructuredCloneMap({ cloneOnGet: true, cloneOnSet: fa * @typedef {{worldInfoString: string, worldInfoBefore: string, worldInfoAfter: string, worldInfoExamples: any[], worldInfoDepth: any[]}} WIPromptResult * @returns {Promise} The world info string and depth. */ -async function getWorldInfoPrompt(chat, maxContext, isDryRun) { +export async function getWorldInfoPrompt(chat, maxContext, isDryRun) { let worldInfoString = '', worldInfoBefore = '', worldInfoAfter = ''; const activatedWorldInfo = await checkWorldInfo(chat, maxContext, isDryRun); @@ -780,7 +770,7 @@ async function getWorldInfoPrompt(chat, maxContext, isDryRun) { }; } -function setWorldInfoSettings(settings, data) { +export function setWorldInfoSettings(settings, data) { if (settings.world_info_depth !== undefined) world_info_depth = Number(settings.world_info_depth); if (settings.world_info_min_activations !== undefined) @@ -916,7 +906,7 @@ function registerWorldInfoSlashCommands() { return ''; } - const data = await loadWorldInfoData(file); + const data = await loadWorldInfo(file); if (!data || !('entries' in data)) { toastr.warning('World Info file has an invalid format'); @@ -965,7 +955,7 @@ function registerWorldInfoSlashCommands() { return ''; } - if (typeof newEntryTemplate[field] === 'boolean') { + if (typeof newWorldInfoEntryTemplate[field] === 'boolean') { const isTrue = isTrueBoolean(value); const isFalse = isFalseBoolean(value); @@ -1016,7 +1006,7 @@ function registerWorldInfoSlashCommands() { return ''; } - if (newEntryTemplate[field] === undefined) { + if (newWorldInfoEntryTemplate[field] === undefined) { toastr.warning('Valid field name is required'); return ''; } @@ -1038,7 +1028,7 @@ function registerWorldInfoSlashCommands() { const file = args.file; const key = args.key; - const data = await loadWorldInfoData(file); + const data = await loadWorldInfo(file); if (!data || !('entries' in data)) { toastr.warning('Valid World Info file name is required'); @@ -1075,7 +1065,7 @@ function registerWorldInfoSlashCommands() { value = value.replace(/\\([{}|])/g, '$1'); - const data = await loadWorldInfoData(file); + const data = await loadWorldInfo(file); if (!data || !('entries' in data)) { toastr.warning('Valid World Info file name is required'); @@ -1089,7 +1079,7 @@ function registerWorldInfoSlashCommands() { return ''; } - if (newEntryTemplate[field] === undefined) { + if (newWorldInfoEntryTemplate[field] === undefined) { toastr.warning('Valid field name is required'); return ''; } @@ -1104,8 +1094,8 @@ function registerWorldInfoSlashCommands() { entry[field] = value; } - if (originalDataKeyMap[field]) { - setOriginalDataValue(data, uid, originalDataKeyMap[field], entry[field]); + if (originalWIDataKeyMap[field]) { + setWIOriginalDataValue(data, uid, originalWIDataKeyMap[field], entry[field]); } await saveWorldInfo(file, data); @@ -1226,7 +1216,7 @@ function registerWorldInfoSlashCommands() { /** A collection of local enum providers for this context of world info */ const localEnumProviders = { /** All possible fields that can be set in a WI entry */ - wiEntryFields: () => Object.entries(newEntryDefinition).map(([key, value]) => + wiEntryFields: () => Object.entries(newWorldInfoEntryDefinition).map(([key, value]) => new SlashCommandEnumValue(key, `[${value.type}] default: ${(typeof value.default === 'string' ? `'${value.default}'` : value.default)}`, enumTypes.enum, enumIcons.getDataTypeIcon(value.type))), @@ -1566,18 +1556,32 @@ function registerWorldInfoSlashCommands() { })); } -// World Info Editor -async function showWorldEditor(name) { + +/** + * Loads the given world into the World Editor. + * + * @param {string} name - The name of the world + * @return {Promise} A promise that resolves when the world editor is loaded + */ +export async function showWorldEditor(name) { if (!name) { hideWorldEditor(); return; } - const wiData = await loadWorldInfoData(name); + const wiData = await loadWorldInfo(name); displayWorldEntries(name, wiData); } -async function loadWorldInfoData(name) { +/** + * Loads world info from the backend. + * + * This function will return from `worldInfoCache` if it has already been loaded before. + * + * @param {string} name - The name of the world to load + * @return {Promise} A promise that resolves to the loaded world information, or null if the request fails. + */ +export async function loadWorldInfo(name) { if (!name) { return; } @@ -1635,10 +1639,12 @@ function getWIElement(name) { } /** + * Sorts the given data based on the selected sort option + * * @param {any[]} data WI entries * @returns {any[]} Sorted data */ -function sortEntries(data) { +export function sortWorldInfoEntries(data) { const option = $('#world_info_sort_order').find(':selected'); const sortField = option.data('field'); const sortOrder = option.data('order'); @@ -1801,7 +1807,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl // Apply the filter and do the chosen sorting entriesArray = worldInfoFilter.applyFilters(entriesArray); - entriesArray = sortEntries(entriesArray); + entriesArray = sortWorldInfoEntries(entriesArray); // Cache keys const keys = entriesArray.flatMap(entry => [...entry.key, ...entry.keysecondary]); @@ -1919,7 +1925,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl 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); + setWIOriginalDataValue(data, entry.uid, 'comment', entry.comment); counter++; } } @@ -1954,7 +1960,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl // We need to sort the entries here, as the data source isn't sorted const entries = Object.values(data.entries); - sortEntries(entries); + sortWorldInfoEntries(entries); let updated = 0, current = start; for (const entry of entries) { @@ -1962,7 +1968,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl if (entry.order === newOrder) continue; entry.order = newOrder; - setOriginalDataValue(data, entry.order, 'order', entry.order); + setWIOriginalDataValue(data, entry.order, 'order', entry.order); updated++; } @@ -2025,7 +2031,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl } item.displayIndex = minDisplayIndex + index; - setOriginalDataValue(data, uid, 'extensions.display_index', item.displayIndex); + setWIOriginalDataValue(data, uid, 'extensions.display_index', item.displayIndex); }); console.table(Object.keys(data.entries).map(uid => data.entries[uid]).map(x => ({ uid: x.uid, key: x.key.join(','), displayIndex: x.displayIndex }))); @@ -2036,7 +2042,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl //$("#world_popup_entries_list").disableSelection(); } -const originalDataKeyMap = { +export const originalWIDataKeyMap = { 'displayIndex': 'extensions.display_index', 'excludeRecursion': 'extensions.exclude_recursion', 'preventRecursion': 'extensions.prevent_recursion', @@ -2087,7 +2093,17 @@ function verifyWorldInfoSearchSortRule() { } } -function setOriginalDataValue(data, uid, key, value) { +/** + * Sets the value of a specific key in the original data entry corresponding to the given uid + * This needs to be called whenever you update JSON data fields. + * Use `originalWIDataKeyMap` to find the correct value to be set. + * + * @param {object} data - The data object containing the original data entries. + * @param {string} uid - The unique identifier of the data entry. + * @param {string} key - The key of the value to be set. + * @param {any} value - The value to be set. + */ +export function setWIOriginalDataValue(data, uid, key, value) { if (data.originalData && Array.isArray(data.originalData.entries)) { let originalEntry = data.originalData.entries.find(x => x.uid === uid); @@ -2099,7 +2115,7 @@ function setOriginalDataValue(data, uid, key, value) { } } -function deleteOriginalDataValue(data, uid) { +function deleteWIOriginalDataValue(data, uid) { if (data.originalData && Array.isArray(data.originalData.entries)) { const originalIndex = data.originalData.entries.findIndex(x => x.uid === uid); @@ -2120,7 +2136,7 @@ function deleteOriginalDataValue(data, uid) { * @param {string} input - One or multiple keywords or regexes, separated by commas * @returns {string[]} An array of keywords and regexes */ -function splitKeywordsAndRegexes(input) { +export function splitKeywordsAndRegexes(input) { /** @type {string[]} */ let keywordsAndRegexes = []; @@ -2224,7 +2240,7 @@ function isValidRegex(input) { * @param {string} input - A delimited regex string * @returns {RegExp|null} The regex object, or null if not a valid regex */ -function parseRegexFromString(input) { +export function parseRegexFromString(input) { // Extracting the regex pattern and flags let match = input.match(/^\/([\w\W]+?)\/([gimsuy]*)$/); if (!match) { @@ -2310,7 +2326,7 @@ function getWorldEntry(name, data, entry) { !skipReset && resetScrollHeight(this); if (!noSave) { data.entries[uid][entryPropName] = keys; - setOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]); + setWIOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]); await saveWorldInfo(name, data); } }); @@ -2348,7 +2364,7 @@ function getWorldEntry(name, data, entry) { !skipReset && resetScrollHeight(this); if (!noSave) { data.entries[uid][entryPropName] = splitKeywordsAndRegexes(value); - setOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]); + setWIOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]); await saveWorldInfo(name, data); } }); @@ -2392,7 +2408,7 @@ function getWorldEntry(name, data, entry) { const uid = $(this).data('uid'); const value = Number($(this).val()); data.entries[uid].selectiveLogic = !isNaN(value) ? value : world_info_logic.AND_ANY; - setOriginalDataValue(data, uid, 'selectiveLogic', data.entries[uid].selectiveLogic); + setWIOriginalDataValue(data, uid, 'selectiveLogic', data.entries[uid].selectiveLogic); await saveWorldInfo(name, data); }); @@ -2441,7 +2457,7 @@ function getWorldEntry(name, data, entry) { } } - setOriginalDataValue(data, uid, 'character_filter', data.entries[uid].characterFilter); + setWIOriginalDataValue(data, uid, 'character_filter', data.entries[uid].characterFilter); await saveWorldInfo(name, data); }); characterExclusionInput.prop('checked', entry.characterFilter?.isExclude ?? false).trigger('input'); @@ -2503,7 +2519,7 @@ function getWorldEntry(name, data, entry) { }, ); } - setOriginalDataValue(data, uid, 'character_filter', data.entries[uid].characterFilter); + setWIOriginalDataValue(data, uid, 'character_filter', data.entries[uid].characterFilter); await saveWorldInfo(name, data); }); @@ -2517,7 +2533,7 @@ function getWorldEntry(name, data, entry) { !skipReset && resetScrollHeight(this); data.entries[uid].comment = value; - setOriginalDataValue(data, uid, 'comment', data.entries[uid].comment); + setWIOriginalDataValue(data, uid, 'comment', data.entries[uid].comment); await saveWorldInfo(name, data); }); commentToggle.data('uid', entry.uid); @@ -2552,7 +2568,7 @@ function getWorldEntry(name, data, entry) { const value = $(this).val(); data.entries[uid].content = value; - setOriginalDataValue(data, uid, 'content', data.entries[uid].content); + setWIOriginalDataValue(data, uid, 'content', data.entries[uid].content); await saveWorldInfo(name, data); if (skipCount) { @@ -2582,7 +2598,7 @@ function getWorldEntry(name, data, entry) { const value = $(this).prop('checked'); data.entries[uid].selective = value; - setOriginalDataValue(data, uid, 'selective', data.entries[uid].selective); + setWIOriginalDataValue(data, uid, 'selective', data.entries[uid].selective); await saveWorldInfo(name, data); const keysecondary = $(this) @@ -2631,7 +2647,7 @@ function getWorldEntry(name, data, entry) { data.entries[uid].order = !isNaN(value) ? value : 0; updatePosOrdDisplay(uid); - setOriginalDataValue(data, uid, 'insertion_order', data.entries[uid].order); + setWIOriginalDataValue(data, uid, 'insertion_order', data.entries[uid].order); await saveWorldInfo(name, data); }); orderInput.val(entry.order).trigger('input'); @@ -2645,7 +2661,7 @@ function getWorldEntry(name, data, entry) { const value = String($(this).val()).trim(); data.entries[uid].group = value; - setOriginalDataValue(data, uid, 'extensions.group', data.entries[uid].group); + setWIOriginalDataValue(data, uid, 'extensions.group', data.entries[uid].group); await saveWorldInfo(name, data); }); groupInput.val(entry.group ?? '').trigger('input'); @@ -2658,7 +2674,7 @@ function getWorldEntry(name, data, entry) { const uid = $(this).data('uid'); const value = $(this).prop('checked'); data.entries[uid].groupOverride = value; - setOriginalDataValue(data, uid, 'extensions.group_override', data.entries[uid].groupOverride); + setWIOriginalDataValue(data, uid, 'extensions.group_override', data.entries[uid].groupOverride); await saveWorldInfo(name, data); }); groupOverrideInput.prop('checked', entry.groupOverride).trigger('input'); @@ -2682,7 +2698,7 @@ function getWorldEntry(name, data, entry) { } data.entries[uid].groupWeight = !isNaN(value) ? Math.abs(value) : 1; - setOriginalDataValue(data, uid, 'extensions.group_weight', data.entries[uid].groupWeight); + setWIOriginalDataValue(data, uid, 'extensions.group_weight', data.entries[uid].groupWeight); await saveWorldInfo(name, data); }); groupWeightInput.val(entry.groupWeight ?? DEFAULT_WEIGHT).trigger('input'); @@ -2695,7 +2711,7 @@ function getWorldEntry(name, data, entry) { const value = Number($(this).val()); data.entries[uid].sticky = !isNaN(value) ? value : null; - setOriginalDataValue(data, uid, 'extensions.sticky', data.entries[uid].sticky); + setWIOriginalDataValue(data, uid, 'extensions.sticky', data.entries[uid].sticky); await saveWorldInfo(name, data); }); sticky.val(entry.sticky > 0 ? entry.sticky : '').trigger('input'); @@ -2708,7 +2724,7 @@ function getWorldEntry(name, data, entry) { const value = Number($(this).val()); data.entries[uid].cooldown = !isNaN(value) ? value : null; - setOriginalDataValue(data, uid, 'extensions.cooldown', data.entries[uid].cooldown); + setWIOriginalDataValue(data, uid, 'extensions.cooldown', data.entries[uid].cooldown); await saveWorldInfo(name, data); }); cooldown.val(entry.cooldown > 0 ? entry.cooldown : '').trigger('input'); @@ -2721,7 +2737,7 @@ function getWorldEntry(name, data, entry) { const value = Number($(this).val()); data.entries[uid].delay = !isNaN(value) ? value : null; - setOriginalDataValue(data, uid, 'extensions.delay', data.entries[uid].delay); + setWIOriginalDataValue(data, uid, 'extensions.delay', data.entries[uid].delay); await saveWorldInfo(name, data); }); delay.val(entry.delay > 0 ? entry.delay : '').trigger('input'); @@ -2741,7 +2757,7 @@ function getWorldEntry(name, data, entry) { data.entries[uid].depth = !isNaN(value) ? value : 0; updatePosOrdDisplay(uid); - setOriginalDataValue(data, uid, 'extensions.depth', data.entries[uid].depth); + setWIOriginalDataValue(data, uid, 'extensions.depth', data.entries[uid].depth); await saveWorldInfo(name, data); }); depthInput.val(entry.depth ?? DEFAULT_DEPTH).trigger('input'); @@ -2769,7 +2785,7 @@ function getWorldEntry(name, data, entry) { } } - setOriginalDataValue(data, uid, 'extensions.probability', data.entries[uid].probability); + setWIOriginalDataValue(data, uid, 'extensions.probability', data.entries[uid].probability); await saveWorldInfo(name, data); }); probabilityInput.val(entry.probability).trigger('input'); @@ -2836,10 +2852,10 @@ function getWorldEntry(name, data, entry) { } updatePosOrdDisplay(uid); // Spec v2 only supports before_char and after_char - setOriginalDataValue(data, uid, 'position', data.entries[uid].position == 0 ? 'before_char' : 'after_char'); + setWIOriginalDataValue(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); - setOriginalDataValue(data, uid, 'extensions.role', data.entries[uid].role); + setWIOriginalDataValue(data, uid, 'extensions.position', data.entries[uid].position); + setWIOriginalDataValue(data, uid, 'extensions.role', data.entries[uid].role); await saveWorldInfo(name, data); }); @@ -2881,36 +2897,36 @@ function getWorldEntry(name, data, entry) { data.entries[uid].constant = true; data.entries[uid].disable = false; data.entries[uid].vectorized = false; - setOriginalDataValue(data, uid, 'enabled', true); - setOriginalDataValue(data, uid, 'constant', true); - setOriginalDataValue(data, uid, 'extensions.vectorized', false); + setWIOriginalDataValue(data, uid, 'enabled', true); + setWIOriginalDataValue(data, uid, 'constant', true); + setWIOriginalDataValue(data, uid, 'extensions.vectorized', false); template.removeClass('disabledWIEntry'); break; case 'normal': data.entries[uid].constant = false; data.entries[uid].disable = false; data.entries[uid].vectorized = false; - setOriginalDataValue(data, uid, 'enabled', true); - setOriginalDataValue(data, uid, 'constant', false); - setOriginalDataValue(data, uid, 'extensions.vectorized', false); + setWIOriginalDataValue(data, uid, 'enabled', true); + setWIOriginalDataValue(data, uid, 'constant', false); + setWIOriginalDataValue(data, uid, 'extensions.vectorized', false); template.removeClass('disabledWIEntry'); break; case 'vectorized': data.entries[uid].constant = false; data.entries[uid].disable = false; data.entries[uid].vectorized = true; - setOriginalDataValue(data, uid, 'enabled', true); - setOriginalDataValue(data, uid, 'constant', false); - setOriginalDataValue(data, uid, 'extensions.vectorized', true); + setWIOriginalDataValue(data, uid, 'enabled', true); + setWIOriginalDataValue(data, uid, 'constant', false); + setWIOriginalDataValue(data, uid, 'extensions.vectorized', true); template.removeClass('disabledWIEntry'); break; case 'disabled': data.entries[uid].constant = false; data.entries[uid].disable = true; data.entries[uid].vectorized = false; - setOriginalDataValue(data, uid, 'enabled', false); - setOriginalDataValue(data, uid, 'constant', false); - setOriginalDataValue(data, uid, 'extensions.vectorized', false); + setWIOriginalDataValue(data, uid, 'enabled', false); + setWIOriginalDataValue(data, uid, 'constant', false); + setWIOriginalDataValue(data, uid, 'extensions.vectorized', false); template.addClass('disabledWIEntry'); break; } @@ -2941,7 +2957,7 @@ function getWorldEntry(name, data, entry) { 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); + setWIOriginalDataValue(data, uid, 'extensions.exclude_recursion', data.entries[uid].excludeRecursion); await saveWorldInfo(name, data); }); excludeRecursionInput.prop('checked', entry.excludeRecursion).trigger('input'); @@ -2953,7 +2969,7 @@ function getWorldEntry(name, data, entry) { const uid = $(this).data('uid'); const value = $(this).prop('checked'); data.entries[uid].preventRecursion = value; - setOriginalDataValue(data, uid, 'extensions.prevent_recursion', data.entries[uid].preventRecursion); + setWIOriginalDataValue(data, uid, 'extensions.prevent_recursion', data.entries[uid].preventRecursion); await saveWorldInfo(name, data); }); preventRecursionInput.prop('checked', entry.preventRecursion).trigger('input'); @@ -2965,7 +2981,7 @@ function getWorldEntry(name, data, entry) { const uid = $(this).data('uid'); const value = $(this).prop('checked'); data.entries[uid].delayUntilRecursion = value; - setOriginalDataValue(data, uid, 'extensions.delay_until_recursion', data.entries[uid].delayUntilRecursion); + setWIOriginalDataValue(data, uid, 'extensions.delay_until_recursion', data.entries[uid].delayUntilRecursion); await saveWorldInfo(name, data); }); delayUntilRecursionInput.prop('checked', entry.delayUntilRecursion).trigger('input'); @@ -2988,7 +3004,7 @@ function getWorldEntry(name, data, entry) { deleteButton.on('click', async function () { const uid = $(this).data('uid'); deleteWorldInfoEntry(data, uid); - deleteOriginalDataValue(data, uid); + deleteWIOriginalDataValue(data, uid); await saveWorldInfo(name, data); updateEditor(navigation_option.previous); }); @@ -3015,7 +3031,7 @@ function getWorldEntry(name, data, entry) { } data.entries[uid].scanDepth = !isEmpty && !isNaN(value) && value >= 0 && value <= MAX_SCAN_DEPTH ? Math.floor(value) : null; - setOriginalDataValue(data, uid, 'extensions.scan_depth', data.entries[uid].scanDepth); + setWIOriginalDataValue(data, uid, 'extensions.scan_depth', data.entries[uid].scanDepth); await saveWorldInfo(name, data); }); scanDepthInput.val(entry.scanDepth ?? null).trigger('input'); @@ -3028,7 +3044,7 @@ function getWorldEntry(name, data, entry) { const value = $(this).val(); data.entries[uid].caseSensitive = value === 'null' ? null : value === 'true'; - setOriginalDataValue(data, uid, 'extensions.case_sensitive', data.entries[uid].caseSensitive); + setWIOriginalDataValue(data, uid, 'extensions.case_sensitive', data.entries[uid].caseSensitive); await saveWorldInfo(name, data); }); caseSensitiveSelect.val((entry.caseSensitive === null || entry.caseSensitive === undefined) ? 'null' : entry.caseSensitive ? 'true' : 'false').trigger('input'); @@ -3041,7 +3057,7 @@ function getWorldEntry(name, data, entry) { const value = $(this).val(); data.entries[uid].matchWholeWords = value === 'null' ? null : value === 'true'; - setOriginalDataValue(data, uid, 'extensions.match_whole_words', data.entries[uid].matchWholeWords); + setWIOriginalDataValue(data, uid, 'extensions.match_whole_words', data.entries[uid].matchWholeWords); await saveWorldInfo(name, data); }); matchWholeWordsSelect.val((entry.matchWholeWords === null || entry.matchWholeWords === undefined) ? 'null' : entry.matchWholeWords ? 'true' : 'false').trigger('input'); @@ -3054,7 +3070,7 @@ function getWorldEntry(name, data, entry) { const value = $(this).val(); data.entries[uid].useGroupScoring = value === 'null' ? null : value === 'true'; - setOriginalDataValue(data, uid, 'extensions.use_group_scoring', data.entries[uid].useGroupScoring); + setWIOriginalDataValue(data, uid, 'extensions.use_group_scoring', data.entries[uid].useGroupScoring); await saveWorldInfo(name, data); }); useGroupScoringSelect.val((entry.useGroupScoring === null || entry.useGroupScoring === undefined) ? 'null' : entry.useGroupScoring ? 'true' : 'false').trigger('input'); @@ -3067,7 +3083,7 @@ function getWorldEntry(name, data, entry) { const value = $(this).val(); data.entries[uid].automationId = value; - setOriginalDataValue(data, uid, 'extensions.automation_id', data.entries[uid].automationId); + setWIOriginalDataValue(data, uid, 'extensions.automation_id', data.entries[uid].automationId); await saveWorldInfo(name, data); }); automationIdInput.val(entry.automationId ?? '').trigger('input'); @@ -3200,12 +3216,12 @@ function createEntryInputAutocomplete(input, callback, { allowMultiple = false } /** - * Duplicated a WI entry by copying all of its properties and assigning a new uid + * Duplicate a WI entry by copying all of its properties and assigning a new uid * @param {*} data - The data of the book * @param {number} uid - The uid of the entry to copy in this book * @returns {*} The new WI duplicated entry */ -function duplicateWorldInfoEntry(data, uid) { +export function duplicateWorldInfoEntry(data, uid) { if (!data || !('entries' in data) || !data.entries[uid]) { return; } @@ -3226,7 +3242,7 @@ function duplicateWorldInfoEntry(data, uid) { * @param {*[]} data - The data of the book * @param {number} uid - The uid of the entry to copy in this book */ -function deleteWorldInfoEntry(data, uid) { +export function deleteWorldInfoEntry(data, uid) { if (!data || !('entries' in data)) { return; } @@ -3245,7 +3261,7 @@ function deleteWorldInfoEntry(data, uid) { * * @type {{[key: string]: { default: any, type: string }}} */ -const newEntryDefinition = { +export const newWorldInfoEntryDefinition = { key: { default: [], type: 'array' }, keysecondary: { default: [], type: 'array' }, comment: { default: '', type: 'string' }, @@ -3278,8 +3294,8 @@ const newEntryDefinition = { delay: { default: null, type: 'number?' }, }; -const newEntryTemplate = Object.fromEntries( - Object.entries(newEntryDefinition).map(([key, value]) => [key, value.default]), +export const newWorldInfoEntryTemplate = Object.fromEntries( + Object.entries(newWorldInfoEntryDefinition).map(([key, value]) => [key, value.default]), ); /** @@ -3288,7 +3304,7 @@ const newEntryTemplate = Object.fromEntries( * @param {any} data WI data * @returns {object | undefined} New entry object or undefined if failed */ -function createWorldInfoEntry(_name, data) { +export function createWorldInfoEntry(_name, data) { const newUid = getFreeWorldEntryUid(data); if (!Number.isInteger(newUid)) { @@ -3296,7 +3312,7 @@ function createWorldInfoEntry(_name, data) { return; } - const newEntry = { uid: newUid, ...structuredClone(newEntryTemplate) }; + const newEntry = { uid: newUid, ...structuredClone(newWorldInfoEntryTemplate) }; data.entries[newUid] = newEntry; return newEntry; @@ -3314,7 +3330,21 @@ async function _save(name, data) { eventSource.emit(event_types.WORLDINFO_UPDATED, name, data); } -async function saveWorldInfo(name, data, immediately = false) { + +/** + * Saves the world info + * + * This will also refresh the `worldInfoCache`. + * Note, for performance reasons the saved cache will not make a deep clone of the data. + * It is your responsibility to not modify the saved data object after calling this function, or there will be data inconsistencies. + * Call `loadWorldInfoData` or query directly from cache if you need the object again. + * + * @param {string} name - The name of the world info + * @param {any} data - The data to be saved + * @param {boolean} [immediately=false] - Whether to save immediately or use debouncing + * @return {Promise} A promise that resolves when the world info is saved + */ +export async function saveWorldInfo(name, data, immediately = false) { if (!name || !data) { return; } @@ -3371,7 +3401,7 @@ async function renameWorldInfo(name, data) { * @param {string} worldInfoName - The name of the world info to delete * @returns {Promise} A promise that resolves to true if the world info was successfully deleted, false otherwise */ -async function deleteWorldInfo(worldInfoName) { +export async function deleteWorldInfo(worldInfoName) { if (!world_names.includes(worldInfoName)) { return false; } @@ -3406,7 +3436,7 @@ async function deleteWorldInfo(worldInfoName) { return true; } -function getFreeWorldEntryUid(data) { +export function getFreeWorldEntryUid(data) { if (!data || !('entries' in data)) { return null; } @@ -3422,7 +3452,7 @@ function getFreeWorldEntryUid(data) { return null; } -function getFreeWorldName() { +export function getFreeWorldName() { const MAX_FREE_NAME = 100_000; for (let index = 1; index < MAX_FREE_NAME; index++) { const newName = `New World (${index})`; @@ -3444,7 +3474,7 @@ function getFreeWorldName() { * @param {boolean} [options.interactive=false] - Whether to show a confirmation dialog when overwriting an existing world * @returns {Promise} - True if the world info was successfully created, false otherwise */ -async function createNewWorldInfo(worldName, { interactive = false } = {}) { +export async function createNewWorldInfo(worldName, { interactive = false } = {}) { const worldInfoTemplate = { entries: {} }; if (!worldName) { @@ -3505,7 +3535,7 @@ async function getCharacterLore() { continue; } - const data = await loadWorldInfoData(worldName); + const data = await loadWorldInfo(worldName); const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(({ uid, ...rest }) => ({ uid, world: worldName, ...rest })) : []; entries = entries.concat(newEntries); @@ -3525,7 +3555,7 @@ async function getGlobalLore() { let entries = []; for (const worldName of selected_world_info) { - const data = await loadWorldInfoData(worldName); + const data = await loadWorldInfo(worldName); const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(({ uid, ...rest }) => ({ uid, world: worldName, ...rest })) : []; entries = entries.concat(newEntries); } @@ -3547,7 +3577,7 @@ async function getChatLore() { return []; } - const data = await loadWorldInfoData(chatWorld); + const data = await loadWorldInfo(chatWorld); const entries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(({ uid, ...rest }) => ({ uid, world: chatWorld, ...rest })) : []; console.debug(`[WI] Chat lore has ${entries.length} entries`, [chatWorld]); @@ -3663,7 +3693,7 @@ function parseDecorators(content) { * @typedef {{ worldInfoBefore: string, worldInfoAfter: string, EMEntries: any[], WIDepthEntries: any[], allActivatedEntries: Set }} WIActivated * @returns {Promise} The world info activated. */ -async function checkWorldInfo(chat, maxContext, isDryRun) { +export async function checkWorldInfo(chat, maxContext, isDryRun) { const context = getContext(); const buffer = new WorldInfoBuffer(chat); @@ -4233,7 +4263,7 @@ function convertAgnaiMemoryBook(inputObj) { inputObj.entries.forEach((entry, index) => { outputObj.entries[index] = { - ...newEntryTemplate, + ...newWorldInfoEntryTemplate, uid: index, key: entry.keywords, keysecondary: [], @@ -4275,7 +4305,7 @@ function convertRisuLorebook(inputObj) { inputObj.data.forEach((entry, index) => { outputObj.entries[index] = { - ...newEntryTemplate, + ...newWorldInfoEntryTemplate, uid: index, key: entry.key.split(',').map(x => x.trim()), keysecondary: entry.secondkey ? entry.secondkey.split(',').map(x => x.trim()) : [], @@ -4322,7 +4352,7 @@ function convertNovelLorebook(inputObj) { const addMemo = displayName !== undefined && displayName.trim() !== ''; outputObj.entries[index] = { - ...newEntryTemplate, + ...newWorldInfoEntryTemplate, uid: index, key: entry.keys, keysecondary: [], @@ -4369,7 +4399,7 @@ function convertCharacterBook(characterBook) { } result.entries[entry.id] = { - ...newEntryTemplate, + ...newWorldInfoEntryTemplate, uid: entry.id, key: entry.keys, keysecondary: entry.secondary_keys || [], @@ -4499,7 +4529,7 @@ export async function importEmbeddedWorldInfo(skipPopup = false) { setWorldInfoButtonClass(chid, true); } -function onWorldInfoChange(args, text) { +export function onWorldInfoChange(args, text) { if (args !== '__notSlashCommand__') { // if it's a slash command const silent = isTrueBoolean(args.silent); if (text.trim() !== '') { // and args are provided @@ -4650,7 +4680,7 @@ export async function importWorldInfo(file) { }); } -function assignLorebookToChat() { +export function assignLorebookToChat() { const selectedName = chat_metadata[METADATA_KEY]; const template = $('#chat_world_template .chat_world').clone();