Merge pull request #2601 from SillyTavern/wi-blackbox-be-gone WI blackbox begone (Export, document and refactor a bit of the WI API)

d5576cd7b78cb84990c4b3c47fbb39c9ed07ce63

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
1 files changed, +190 -145Ignore whitespace
public/scripts/world-info.js+190 -145
@@ -18,33 +18,13 @@ import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
1818import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js';
1919import { StructuredCloneMap } from './util/StructuredCloneMap.js';
2020
2121export const world_info_insertion_strategy = {
22- world_info,
23- world_info_budget,
24- world_info_depth,
25- world_info_min_activations,
26- world_info_min_activations_depth_max,
27- world_info_include_names,
28- world_info_recursive,
29- world_info_overflow_alert,
30- world_info_case_sensitive,
31- world_info_match_whole_words,
32- world_info_character_strategy,
33- world_info_budget_cap,
34- world_names,
35- checkWorldInfo,
36- deleteWorldInfo,
37- setWorldInfoSettings,
38- getWorldInfoPrompt,
39-};
40-
41-const world_info_insertion_strategy = {
4222 evenly: 0,
4323 character_first: 1,
4424 global_first: 2,
4525};
4626
4727export const world_info_logic = {
4828 AND_ANY: 0,
4929 NOT_ALL: 1,
5030 NOT_ANY: 2,
@@ -54,7 +34,7 @@ const world_info_logic = {
5434/**
5535 * @enum {number} Possible states of the WI evaluation
5636 */
5737export const scan_state = {
5838 /**
5939 * The scan will be stopped.
6040 */
@@ -75,23 +55,23 @@ const scan_state = {
7555
7656const WI_ENTRY_EDIT_TEMPLATE = $('#entry_edit_template .world_entry');
7757
7858export let world_info = {};
7959export let selected_world_info = [];
8060/** @type {string[]} */
8161export let world_names;
8262export let world_info_depth = 2;
8363export let world_info_min_activations = 0; // if > 0, will continue seeking chat until minimum world infos are activated
8464export let world_info_min_activations_depth_max = 0; // used when (world_info_min_activations > 0)
8565
8666export let world_info_budget = 25;
8767export let world_info_include_names = true;
8868export let world_info_recursive = false;
8969export let world_info_overflow_alert = false;
9070export let world_info_case_sensitive = false;
9171export let world_info_match_whole_words = false;
9272export let world_info_use_group_scoring = false;
9373export let world_info_character_strategy = world_info_insertion_strategy.character_first;
9474export let world_info_budget_cap = 0;
9575const saveWorldDebounced = debounce(async (name, data) => await _save(name, data), debounce_timeout.relaxed);
9676const saveSettingsDebounced = debounce(() => {
9777 Object.assign(world_info, { globalSelect: selected_world_info });
@@ -101,13 +81,13 @@ const sortFn = (a, b) => b.order - a.order;
10181let updateEditor = (navigation, flashOnNav = true) => { console.debug('Triggered WI navigation', navigation, flashOnNav); };
10282
10383// Do not optimize. updateEditor is a function that is updated by the displayWorldEntries with new data.
10484export const worldInfoFilter = new FilterHelper(() => updateEditor());
10585export const SORT_ORDER_KEY = 'world_info_sort_order';
10686export const METADATA_KEY = 'world_info';
10787
10888export const DEFAULT_DEPTH = 4;
10989export const DEFAULT_WEIGHT = 100;
11090export const MAX_SCAN_DEPTH = 1000;
11191const KNOWN_DECORATORS = ['@@activate', '@@dont_activate'];
11292
11393// Typedef area
@@ -732,7 +712,7 @@ export function getWorldInfoSettings() {
732712 };
733713}
734714
735715export const world_info_position = {
736716 before: 0,
737717 after: 1,
738718 ANTop: 2,
@@ -747,8 +727,18 @@ export const wi_anchor_position = {
747727 after: 1,
748728};
749729
750-/** @type {StructuredCloneMap<string,object>} */
730+/**
751-const worldInfoCache = new StructuredCloneMap({ cloneOnGet: true, cloneOnSet: false });
731+ * The cache of all world info data that was loaded from the backend.
732+ *
733+ * Calling `loadWorldInfo` will fill this cache and utilize this cache, so should be the preferred way to load any world info data.
734+ * Only use the cache directly if you need synchronous access.
735+ *
736+ * This will return a deep clone of the data, so no way to modify the data without actually saving it.
737+ * Should generally be only used for readonly access.
738+ *
739+ * @type {StructuredCloneMap<string,object>}
740+ * */
741+export const worldInfoCache = new StructuredCloneMap({ cloneOnGet: true, cloneOnSet: false });
752742
753743/**
754744 * Gets the world info based on chat messages.
@@ -758,7 +748,7 @@ const worldInfoCache = new StructuredCloneMap({ cloneOnGet: true, cloneOnSet: fa
758748 * @typedef {{worldInfoString: string, worldInfoBefore: string, worldInfoAfter: string, worldInfoExamples: any[], worldInfoDepth: any[]}} WIPromptResult
759749 * @returns {Promise<WIPromptResult>} The world info string and depth.
760750 */
761751export async function getWorldInfoPrompt(chat, maxContext, isDryRun) {
762752 let worldInfoString = '', worldInfoBefore = '', worldInfoAfter = '';
763753
764754 const activatedWorldInfo = await checkWorldInfo(chat, maxContext, isDryRun);
@@ -780,7 +770,7 @@ async function getWorldInfoPrompt(chat, maxContext, isDryRun) {
780770 };
781771}
782772
783773export function setWorldInfoSettings(settings, data) {
784774 if (settings.world_info_depth !== undefined)
785775 world_info_depth = Number(settings.world_info_depth);
786776 if (settings.world_info_min_activations !== undefined)
@@ -916,7 +906,7 @@ function registerWorldInfoSlashCommands() {
916906 return '';
917907 }
918908
919909 const data = await loadWorldInfoDataloadWorldInfo(file);
920910
921911 if (!data || !('entries' in data)) {
922912 toastr.warning('World Info file has an invalid format');
@@ -965,7 +955,7 @@ function registerWorldInfoSlashCommands() {
965955 return '';
966956 }
967957
968958 if (typeof newEntryTemplatenewWorldInfoEntryTemplate[field] === 'boolean') {
969959 const isTrue = isTrueBoolean(value);
970960 const isFalse = isFalseBoolean(value);
971961
@@ -1016,7 +1006,7 @@ function registerWorldInfoSlashCommands() {
10161006 return '';
10171007 }
10181008
10191009 if (newEntryTemplatenewWorldInfoEntryTemplate[field] === undefined) {
10201010 toastr.warning('Valid field name is required');
10211011 return '';
10221012 }
@@ -1038,7 +1028,7 @@ function registerWorldInfoSlashCommands() {
10381028 const file = args.file;
10391029 const key = args.key;
10401030
10411031 const data = await loadWorldInfoDataloadWorldInfo(file);
10421032
10431033 if (!data || !('entries' in data)) {
10441034 toastr.warning('Valid World Info file name is required');
@@ -1075,7 +1065,7 @@ function registerWorldInfoSlashCommands() {
10751065
10761066 value = value.replace(/\\([{}|])/g, '$1');
10771067
10781068 const data = await loadWorldInfoDataloadWorldInfo(file);
10791069
10801070 if (!data || !('entries' in data)) {
10811071 toastr.warning('Valid World Info file name is required');
@@ -1089,7 +1079,7 @@ function registerWorldInfoSlashCommands() {
10891079 return '';
10901080 }
10911081
10921082 if (newEntryTemplatenewWorldInfoEntryTemplate[field] === undefined) {
10931083 toastr.warning('Valid field name is required');
10941084 return '';
10951085 }
@@ -1104,8 +1094,8 @@ function registerWorldInfoSlashCommands() {
11041094 entry[field] = value;
11051095 }
11061096
11071097 if (originalDataKeyMaporiginalWIDataKeyMap[field]) {
11081098 setOriginalDataValuesetWIOriginalDataValue(data, uid, originalDataKeyMaporiginalWIDataKeyMap[field], entry[field]);
11091099 }
11101100
11111101 await saveWorldInfo(file, data);
@@ -1226,7 +1216,7 @@ function registerWorldInfoSlashCommands() {
12261216 /** A collection of local enum providers for this context of world info */
12271217 const localEnumProviders = {
12281218 /** All possible fields that can be set in a WI entry */
12291219 wiEntryFields: () => Object.entries(newEntryDefinitionnewWorldInfoEntryDefinition).map(([key, value]) =>
12301220 new SlashCommandEnumValue(key, `[${value.type}] default: ${(typeof value.default === 'string' ? `'${value.default}'` : value.default)}`,
12311221 enumTypes.enum, enumIcons.getDataTypeIcon(value.type))),
12321222
@@ -1566,18 +1556,32 @@ function registerWorldInfoSlashCommands() {
15661556 }));
15671557}
15681558
1569-// World Info Editor
1559+
1570-async function showWorldEditor(name) {
1560+/**
1561+ * Loads the given world into the World Editor.
1562+ *
1563+ * @param {string} name - The name of the world
1564+ * @return {Promise<void>} A promise that resolves when the world editor is loaded
1565+ */
1566+export async function showWorldEditor(name) {
15711567 if (!name) {
15721568 hideWorldEditor();
15731569 return;
15741570 }
15751571
15761572 const wiData = await loadWorldInfoDataloadWorldInfo(name);
15771573 displayWorldEntries(name, wiData);
15781574}
15791575
1580-async function loadWorldInfoData(name) {
1576+/**
1577+ * Loads world info from the backend.
1578+ *
1579+ * This function will return from `worldInfoCache` if it has already been loaded before.
1580+ *
1581+ * @param {string} name - The name of the world to load
1582+ * @return {Promise<Object|null>} A promise that resolves to the loaded world information, or null if the request fails.
1583+ */
1584+export async function loadWorldInfo(name) {
15811585 if (!name) {
15821586 return;
15831587 }
@@ -1635,14 +1639,18 @@ function getWIElement(name) {
16351639}
16361640
16371641/**
1642+ * Sorts the given data based on the selected sort option
1643+ *
16381644 * @param {any[]} data WI entries
1645+ * @param {object} [options={}] - Optional arguments
1646+ * @param {{sortField?: string, sortOrder?: string, sortRule?: string}} [options.customSort={}] - Custom sort options, instead of the chosen UI sort
16391647 * @returns {any[]} Sorted data
16401648 */
1641-function sortEntries(data) {
1649+export function sortWorldInfoEntries(data, { customSort = null } = {}) {
16421650 const option = $('#world_info_sort_order').find(':selected');
16431651 const sortField = customSort?.sortField ?? option.data('field');
16441652 const sortOrder = customSort?.sortOrder ?? option.data('order');
16451653 const sortRule = customSort?.sortRule ?? option.data('rule');
16461654 const orderSign = sortOrder === 'asc' ? 1 : -1;
16471655
16481656 if (!data.length) return data;
@@ -1801,7 +1809,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
18011809
18021810 // Apply the filter and do the chosen sorting
18031811 entriesArray = worldInfoFilter.applyFilters(entriesArray);
18041812 entriesArray = sortEntriessortWorldInfoEntries(entriesArray);
18051813
18061814 // Cache keys
18071815 const keys = entriesArray.flatMap(entry => [...entry.key, ...entry.keysecondary]);
@@ -1919,7 +1927,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
19191927 for (const entry of Object.values(data.entries)) {
19201928 if (!entry.comment && Array.isArray(entry.key) && entry.key.length > 0) {
19211929 entry.comment = entry.key[0];
19221930 setOriginalDataValuesetWIOriginalDataValue(data, entry.uid, 'comment', entry.comment);
19231931 counter++;
19241932 }
19251933 }
@@ -1954,7 +1962,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
19541962
19551963 // We need to sort the entries here, as the data source isn't sorted
19561964 const entries = Object.values(data.entries);
19571965 sortEntriessortWorldInfoEntries(entries);
19581966
19591967 let updated = 0, current = start;
19601968 for (const entry of entries) {
@@ -1962,7 +1970,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
19621970 if (entry.order === newOrder) continue;
19631971
19641972 entry.order = newOrder;
19651973 setOriginalDataValuesetWIOriginalDataValue(data, entry.order, 'order', entry.order);
19661974 updated++;
19671975 }
19681976
@@ -2025,7 +2033,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
20252033 }
20262034
20272035 item.displayIndex = minDisplayIndex + index;
20282036 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'extensions.display_index', item.displayIndex);
20292037 });
20302038
20312039 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 +2044,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
20362044 //$("#world_popup_entries_list").disableSelection();
20372045}
20382046
20392047export const originalDataKeyMaporiginalWIDataKeyMap = {
20402048 'displayIndex': 'extensions.display_index',
20412049 'excludeRecursion': 'extensions.exclude_recursion',
20422050 'preventRecursion': 'extensions.prevent_recursion',
@@ -2087,7 +2095,17 @@ function verifyWorldInfoSearchSortRule() {
20872095 }
20882096}
20892097
2090-function setOriginalDataValue(data, uid, key, value) {
2098+/**
2099+ * Sets the value of a specific key in the original data entry corresponding to the given uid
2100+ * This needs to be called whenever you update JSON data fields.
2101+ * Use `originalWIDataKeyMap` to find the correct value to be set.
2102+ *
2103+ * @param {object} data - The data object containing the original data entries.
2104+ * @param {string} uid - The unique identifier of the data entry.
2105+ * @param {string} key - The key of the value to be set.
2106+ * @param {any} value - The value to be set.
2107+ */
2108+export function setWIOriginalDataValue(data, uid, key, value) {
20912109 if (data.originalData && Array.isArray(data.originalData.entries)) {
20922110 let originalEntry = data.originalData.entries.find(x => x.uid === uid);
20932111
@@ -2099,7 +2117,13 @@ function setOriginalDataValue(data, uid, key, value) {
20992117 }
21002118}
21012119
2102-function deleteOriginalDataValue(data, uid) {
2120+/**
2121+ * Deletes the original data entry corresponding to the given uid from the provided data object
2122+ *
2123+ * @param {object} data - The data object containing the original data entries
2124+ * @param {string} uid - The unique identifier of the data entry to be deleted
2125+ */
2126+export function deleteWIOriginalDataValue(data, uid) {
21032127 if (data.originalData && Array.isArray(data.originalData.entries)) {
21042128 const originalIndex = data.originalData.entries.findIndex(x => x.uid === uid);
21052129
@@ -2120,7 +2144,7 @@ function deleteOriginalDataValue(data, uid) {
21202144 * @param {string} input - One or multiple keywords or regexes, separated by commas
21212145 * @returns {string[]} An array of keywords and regexes
21222146 */
21232147export function splitKeywordsAndRegexes(input) {
21242148 /** @type {string[]} */
21252149 let keywordsAndRegexes = [];
21262150
@@ -2224,7 +2248,7 @@ function isValidRegex(input) {
22242248 * @param {string} input - A delimited regex string
22252249 * @returns {RegExp|null} The regex object, or null if not a valid regex
22262250 */
22272251export function parseRegexFromString(input) {
22282252 // Extracting the regex pattern and flags
22292253 let match = input.match(/^\/([\w\W]+?)\/([gimsuy]*)$/);
22302254 if (!match) {
@@ -2310,7 +2334,7 @@ function getWorldEntry(name, data, entry) {
23102334 !skipReset && resetScrollHeight(this);
23112335 if (!noSave) {
23122336 data.entries[uid][entryPropName] = keys;
23132337 setOriginalDataValuesetWIOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
23142338 await saveWorldInfo(name, data);
23152339 }
23162340 });
@@ -2348,7 +2372,7 @@ function getWorldEntry(name, data, entry) {
23482372 !skipReset && resetScrollHeight(this);
23492373 if (!noSave) {
23502374 data.entries[uid][entryPropName] = splitKeywordsAndRegexes(value);
23512375 setOriginalDataValuesetWIOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
23522376 await saveWorldInfo(name, data);
23532377 }
23542378 });
@@ -2392,7 +2416,7 @@ function getWorldEntry(name, data, entry) {
23922416 const uid = $(this).data('uid');
23932417 const value = Number($(this).val());
23942418 data.entries[uid].selectiveLogic = !isNaN(value) ? value : world_info_logic.AND_ANY;
23952419 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'selectiveLogic', data.entries[uid].selectiveLogic);
23962420 await saveWorldInfo(name, data);
23972421 });
23982422
@@ -2441,7 +2465,7 @@ function getWorldEntry(name, data, entry) {
24412465 }
24422466 }
24432467
24442468 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'character_filter', data.entries[uid].characterFilter);
24452469 await saveWorldInfo(name, data);
24462470 });
24472471 characterExclusionInput.prop('checked', entry.characterFilter?.isExclude ?? false).trigger('input');
@@ -2503,7 +2527,7 @@ function getWorldEntry(name, data, entry) {
25032527 },
25042528 );
25052529 }
25062530 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'character_filter', data.entries[uid].characterFilter);
25072531 await saveWorldInfo(name, data);
25082532 });
25092533
@@ -2517,7 +2541,7 @@ function getWorldEntry(name, data, entry) {
25172541 !skipReset && resetScrollHeight(this);
25182542 data.entries[uid].comment = value;
25192543
25202544 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'comment', data.entries[uid].comment);
25212545 await saveWorldInfo(name, data);
25222546 });
25232547 commentToggle.data('uid', entry.uid);
@@ -2552,7 +2576,7 @@ function getWorldEntry(name, data, entry) {
25522576 const value = $(this).val();
25532577 data.entries[uid].content = value;
25542578
25552579 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'content', data.entries[uid].content);
25562580 await saveWorldInfo(name, data);
25572581
25582582 if (skipCount) {
@@ -2582,7 +2606,7 @@ function getWorldEntry(name, data, entry) {
25822606 const value = $(this).prop('checked');
25832607 data.entries[uid].selective = value;
25842608
25852609 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'selective', data.entries[uid].selective);
25862610 await saveWorldInfo(name, data);
25872611
25882612 const keysecondary = $(this)
@@ -2631,7 +2655,7 @@ function getWorldEntry(name, data, entry) {
26312655
26322656 data.entries[uid].order = !isNaN(value) ? value : 0;
26332657 updatePosOrdDisplay(uid);
26342658 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'insertion_order', data.entries[uid].order);
26352659 await saveWorldInfo(name, data);
26362660 });
26372661 orderInput.val(entry.order).trigger('input');
@@ -2645,7 +2669,7 @@ function getWorldEntry(name, data, entry) {
26452669 const value = String($(this).val()).trim();
26462670
26472671 data.entries[uid].group = value;
26482672 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'extensions.group', data.entries[uid].group);
26492673 await saveWorldInfo(name, data);
26502674 });
26512675 groupInput.val(entry.group ?? '').trigger('input');
@@ -2658,7 +2682,7 @@ function getWorldEntry(name, data, entry) {
26582682 const uid = $(this).data('uid');
26592683 const value = $(this).prop('checked');
26602684 data.entries[uid].groupOverride = value;
26612685 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'extensions.group_override', data.entries[uid].groupOverride);
26622686 await saveWorldInfo(name, data);
26632687 });
26642688 groupOverrideInput.prop('checked', entry.groupOverride).trigger('input');
@@ -2682,7 +2706,7 @@ function getWorldEntry(name, data, entry) {
26822706 }
26832707
26842708 data.entries[uid].groupWeight = !isNaN(value) ? Math.abs(value) : 1;
26852709 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'extensions.group_weight', data.entries[uid].groupWeight);
26862710 await saveWorldInfo(name, data);
26872711 });
26882712 groupWeightInput.val(entry.groupWeight ?? DEFAULT_WEIGHT).trigger('input');
@@ -2695,7 +2719,7 @@ function getWorldEntry(name, data, entry) {
26952719 const value = Number($(this).val());
26962720 data.entries[uid].sticky = !isNaN(value) ? value : null;
26972721
26982722 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'extensions.sticky', data.entries[uid].sticky);
26992723 await saveWorldInfo(name, data);
27002724 });
27012725 sticky.val(entry.sticky > 0 ? entry.sticky : '').trigger('input');
@@ -2708,7 +2732,7 @@ function getWorldEntry(name, data, entry) {
27082732 const value = Number($(this).val());
27092733 data.entries[uid].cooldown = !isNaN(value) ? value : null;
27102734
27112735 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'extensions.cooldown', data.entries[uid].cooldown);
27122736 await saveWorldInfo(name, data);
27132737 });
27142738 cooldown.val(entry.cooldown > 0 ? entry.cooldown : '').trigger('input');
@@ -2721,7 +2745,7 @@ function getWorldEntry(name, data, entry) {
27212745 const value = Number($(this).val());
27222746 data.entries[uid].delay = !isNaN(value) ? value : null;
27232747
27242748 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'extensions.delay', data.entries[uid].delay);
27252749 await saveWorldInfo(name, data);
27262750 });
27272751 delay.val(entry.delay > 0 ? entry.delay : '').trigger('input');
@@ -2741,7 +2765,7 @@ function getWorldEntry(name, data, entry) {
27412765
27422766 data.entries[uid].depth = !isNaN(value) ? value : 0;
27432767 updatePosOrdDisplay(uid);
27442768 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'extensions.depth', data.entries[uid].depth);
27452769 await saveWorldInfo(name, data);
27462770 });
27472771 depthInput.val(entry.depth ?? DEFAULT_DEPTH).trigger('input');
@@ -2769,7 +2793,7 @@ function getWorldEntry(name, data, entry) {
27692793 }
27702794 }
27712795
27722796 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'extensions.probability', data.entries[uid].probability);
27732797 await saveWorldInfo(name, data);
27742798 });
27752799 probabilityInput.val(entry.probability).trigger('input');
@@ -2836,10 +2860,10 @@ function getWorldEntry(name, data, entry) {
28362860 }
28372861 updatePosOrdDisplay(uid);
28382862 // Spec v2 only supports before_char and after_char
28392863 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'position', data.entries[uid].position == 0 ? 'before_char' : 'after_char');
28402864 // Write the original value as extensions field
28412865 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'extensions.position', data.entries[uid].position);
28422866 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'extensions.role', data.entries[uid].role);
28432867 await saveWorldInfo(name, data);
28442868 });
28452869
@@ -2881,36 +2905,36 @@ function getWorldEntry(name, data, entry) {
28812905 data.entries[uid].constant = true;
28822906 data.entries[uid].disable = false;
28832907 data.entries[uid].vectorized = false;
28842908 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'enabled', true);
28852909 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'constant', true);
28862910 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'extensions.vectorized', false);
28872911 template.removeClass('disabledWIEntry');
28882912 break;
28892913 case 'normal':
28902914 data.entries[uid].constant = false;
28912915 data.entries[uid].disable = false;
28922916 data.entries[uid].vectorized = false;
28932917 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'enabled', true);
28942918 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'constant', false);
28952919 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'extensions.vectorized', false);
28962920 template.removeClass('disabledWIEntry');
28972921 break;
28982922 case 'vectorized':
28992923 data.entries[uid].constant = false;
29002924 data.entries[uid].disable = false;
29012925 data.entries[uid].vectorized = true;
29022926 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'enabled', true);
29032927 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'constant', false);
29042928 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'extensions.vectorized', true);
29052929 template.removeClass('disabledWIEntry');
29062930 break;
29072931 case 'disabled':
29082932 data.entries[uid].constant = false;
29092933 data.entries[uid].disable = true;
29102934 data.entries[uid].vectorized = false;
29112935 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'enabled', false);
29122936 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'constant', false);
29132937 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'extensions.vectorized', false);
29142938 template.addClass('disabledWIEntry');
29152939 break;
29162940 }
@@ -2941,7 +2965,7 @@ function getWorldEntry(name, data, entry) {
29412965 const uid = $(this).data('uid');
29422966 const value = $(this).prop('checked');
29432967 data.entries[uid].excludeRecursion = value;
29442968 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'extensions.exclude_recursion', data.entries[uid].excludeRecursion);
29452969 await saveWorldInfo(name, data);
29462970 });
29472971 excludeRecursionInput.prop('checked', entry.excludeRecursion).trigger('input');
@@ -2953,7 +2977,7 @@ function getWorldEntry(name, data, entry) {
29532977 const uid = $(this).data('uid');
29542978 const value = $(this).prop('checked');
29552979 data.entries[uid].preventRecursion = value;
29562980 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'extensions.prevent_recursion', data.entries[uid].preventRecursion);
29572981 await saveWorldInfo(name, data);
29582982 });
29592983 preventRecursionInput.prop('checked', entry.preventRecursion).trigger('input');
@@ -2965,7 +2989,7 @@ function getWorldEntry(name, data, entry) {
29652989 const uid = $(this).data('uid');
29662990 const value = $(this).prop('checked');
29672991 data.entries[uid].delayUntilRecursion = value;
29682992 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'extensions.delay_until_recursion', data.entries[uid].delayUntilRecursion);
29692993 await saveWorldInfo(name, data);
29702994 });
29712995 delayUntilRecursionInput.prop('checked', entry.delayUntilRecursion).trigger('input');
@@ -2985,10 +3009,12 @@ function getWorldEntry(name, data, entry) {
29853009 // delete button
29863010 const deleteButton = template.find('.delete_entry_button');
29873011 deleteButton.data('uid', entry.uid);
29883012 deleteButton.on('click', async function (e) {
3013+ e.stopPropagation();
29893014 const uid = $(this).data('uid');
29903015 const deleted = await deleteWorldInfoEntry(data, uid);
2991- deleteOriginalDataValue(data, uid);
3016+ if (!deleted) return;
3017+ deleteWIOriginalDataValue(data, uid);
29923018 await saveWorldInfo(name, data);
29933019 updateEditor(navigation_option.previous);
29943020 });
@@ -3015,7 +3041,7 @@ function getWorldEntry(name, data, entry) {
30153041 }
30163042
30173043 data.entries[uid].scanDepth = !isEmpty && !isNaN(value) && value >= 0 && value <= MAX_SCAN_DEPTH ? Math.floor(value) : null;
30183044 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'extensions.scan_depth', data.entries[uid].scanDepth);
30193045 await saveWorldInfo(name, data);
30203046 });
30213047 scanDepthInput.val(entry.scanDepth ?? null).trigger('input');
@@ -3028,7 +3054,7 @@ function getWorldEntry(name, data, entry) {
30283054 const value = $(this).val();
30293055
30303056 data.entries[uid].caseSensitive = value === 'null' ? null : value === 'true';
30313057 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'extensions.case_sensitive', data.entries[uid].caseSensitive);
30323058 await saveWorldInfo(name, data);
30333059 });
30343060 caseSensitiveSelect.val((entry.caseSensitive === null || entry.caseSensitive === undefined) ? 'null' : entry.caseSensitive ? 'true' : 'false').trigger('input');
@@ -3041,7 +3067,7 @@ function getWorldEntry(name, data, entry) {
30413067 const value = $(this).val();
30423068
30433069 data.entries[uid].matchWholeWords = value === 'null' ? null : value === 'true';
30443070 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'extensions.match_whole_words', data.entries[uid].matchWholeWords);
30453071 await saveWorldInfo(name, data);
30463072 });
30473073 matchWholeWordsSelect.val((entry.matchWholeWords === null || entry.matchWholeWords === undefined) ? 'null' : entry.matchWholeWords ? 'true' : 'false').trigger('input');
@@ -3054,7 +3080,7 @@ function getWorldEntry(name, data, entry) {
30543080 const value = $(this).val();
30553081
30563082 data.entries[uid].useGroupScoring = value === 'null' ? null : value === 'true';
30573083 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'extensions.use_group_scoring', data.entries[uid].useGroupScoring);
30583084 await saveWorldInfo(name, data);
30593085 });
30603086 useGroupScoringSelect.val((entry.useGroupScoring === null || entry.useGroupScoring === undefined) ? 'null' : entry.useGroupScoring ? 'true' : 'false').trigger('input');
@@ -3067,7 +3093,7 @@ function getWorldEntry(name, data, entry) {
30673093 const value = $(this).val();
30683094
30693095 data.entries[uid].automationId = value;
30703096 setOriginalDataValuesetWIOriginalDataValue(data, uid, 'extensions.automation_id', data.entries[uid].automationId);
30713097 await saveWorldInfo(name, data);
30723098 });
30733099 automationIdInput.val(entry.automationId ?? '').trigger('input');
@@ -3200,12 +3226,12 @@ function createEntryInputAutocomplete(input, callback, { allowMultiple = false }
32003226
32013227
32023228/**
32033229 * DuplicatedDuplicate a WI entry by copying all of its properties and assigning a new uid
32043230 * @param {*} data - The data of the book
32053231 * @param {number} uid - The uid of the entry to copy in this book
32063232 * @returns {*} The new WI duplicated entry
32073233 */
32083234export function duplicateWorldInfoEntry(data, uid) {
32093235 if (!data || !('entries' in data) || !data.entries[uid]) {
32103236 return;
32113237 }
@@ -3225,17 +3251,22 @@ function duplicateWorldInfoEntry(data, uid) {
32253251 * Deletes a WI entry, with a user confirmation dialog
32263252 * @param {*[]} data - The data of the book
32273253 * @param {number} uid - The uid of the entry to copy in this book
3254+ * @param {object} [options={}] - Optional arguments
3255+ * @param {boolean} [options.silent=false] - Whether to prompt the user for deletion or just do it
3256+ * @returns {Promise<boolean>} Whether the entry deletion was successful
32283257 */
32293258export async function deleteWorldInfoEntry(data, uid, { silent = false } = {}) {
32303259 if (!data || !('entries' in data)) {
32313260 return;
32323261 }
32333262
32343263 ifconst (!confirmation = silent || await Popup.show.confirm(`Delete the entry with UID: ${uid}?`, 'This action is irreversible!`)') {;
3235- throw new Error('User cancelled deletion');
3264+ if (!confirmation) {
3265+ return false;
32363266 }
32373267
32383268 delete data.entries[uid];
3269+ return true;
32393270}
32403271
32413272/**
@@ -3245,7 +3276,7 @@ function deleteWorldInfoEntry(data, uid) {
32453276 *
32463277 * @type {{[key: string]: { default: any, type: string }}}
32473278 */
32483279export const newEntryDefinitionnewWorldInfoEntryDefinition = {
32493280 key: { default: [], type: 'array' },
32503281 keysecondary: { default: [], type: 'array' },
32513282 comment: { default: '', type: 'string' },
@@ -3278,8 +3309,8 @@ const newEntryDefinition = {
32783309 delay: { default: null, type: 'number?' },
32793310};
32803311
32813312export const newEntryTemplatenewWorldInfoEntryTemplate = Object.fromEntries(
32823313 Object.entries(newEntryDefinitionnewWorldInfoEntryDefinition).map(([key, value]) => [key, value.default]),
32833314);
32843315
32853316/**
@@ -3288,7 +3319,7 @@ const newEntryTemplate = Object.fromEntries(
32883319 * @param {any} data WI data
32893320 * @returns {object | undefined} New entry object or undefined if failed
32903321 */
32913322export function createWorldInfoEntry(_name, data) {
32923323 const newUid = getFreeWorldEntryUid(data);
32933324
32943325 if (!Number.isInteger(newUid)) {
@@ -3296,7 +3327,7 @@ function createWorldInfoEntry(_name, data) {
32963327 return;
32973328 }
32983329
32993330 const newEntry = { uid: newUid, ...structuredClone(newEntryTemplatenewWorldInfoEntryTemplate) };
33003331 data.entries[newUid] = newEntry;
33013332
33023333 return newEntry;
@@ -3314,7 +3345,21 @@ async function _save(name, data) {
33143345 eventSource.emit(event_types.WORLDINFO_UPDATED, name, data);
33153346}
33163347
3317-async function saveWorldInfo(name, data, immediately = false) {
3348+
3349+/**
3350+ * Saves the world info
3351+ *
3352+ * This will also refresh the `worldInfoCache`.
3353+ * Note, for performance reasons the saved cache will not make a deep clone of the data.
3354+ * It is your responsibility to not modify the saved data object after calling this function, or there will be data inconsistencies.
3355+ * Call `loadWorldInfoData` or query directly from cache if you need the object again.
3356+ *
3357+ * @param {string} name - The name of the world info
3358+ * @param {any} data - The data to be saved
3359+ * @param {boolean} [immediately=false] - Whether to save immediately or use debouncing
3360+ * @return {Promise<void>} A promise that resolves when the world info is saved
3361+ */
3362+export async function saveWorldInfo(name, data, immediately = false) {
33183363 if (!name || !data) {
33193364 return;
33203365 }
@@ -3371,7 +3416,7 @@ async function renameWorldInfo(name, data) {
33713416 * @param {string} worldInfoName - The name of the world info to delete
33723417 * @returns {Promise<boolean>} A promise that resolves to true if the world info was successfully deleted, false otherwise
33733418 */
33743419export async function deleteWorldInfo(worldInfoName) {
33753420 if (!world_names.includes(worldInfoName)) {
33763421 return false;
33773422 }
@@ -3406,7 +3451,7 @@ async function deleteWorldInfo(worldInfoName) {
34063451 return true;
34073452}
34083453
34093454export function getFreeWorldEntryUid(data) {
34103455 if (!data || !('entries' in data)) {
34113456 return null;
34123457 }
@@ -3422,7 +3467,7 @@ function getFreeWorldEntryUid(data) {
34223467 return null;
34233468}
34243469
34253470export function getFreeWorldName() {
34263471 const MAX_FREE_NAME = 100_000;
34273472 for (let index = 1; index < MAX_FREE_NAME; index++) {
34283473 const newName = `New World (${index})`;
@@ -3444,7 +3489,7 @@ function getFreeWorldName() {
34443489 * @param {boolean} [options.interactive=false] - Whether to show a confirmation dialog when overwriting an existing world
34453490 * @returns {Promise<boolean>} - True if the world info was successfully created, false otherwise
34463491 */
34473492export async function createNewWorldInfo(worldName, { interactive = false } = {}) {
34483493 const worldInfoTemplate = { entries: {} };
34493494
34503495 if (!worldName) {
@@ -3505,7 +3550,7 @@ async function getCharacterLore() {
35053550 continue;
35063551 }
35073552
35083553 const data = await loadWorldInfoDataloadWorldInfo(worldName);
35093554 const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(({ uid, ...rest }) => ({ uid, world: worldName, ...rest })) : [];
35103555 entries = entries.concat(newEntries);
35113556
@@ -3525,7 +3570,7 @@ async function getGlobalLore() {
35253570
35263571 let entries = [];
35273572 for (const worldName of selected_world_info) {
35283573 const data = await loadWorldInfoDataloadWorldInfo(worldName);
35293574 const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(({ uid, ...rest }) => ({ uid, world: worldName, ...rest })) : [];
35303575 entries = entries.concat(newEntries);
35313576 }
@@ -3547,7 +3592,7 @@ async function getChatLore() {
35473592 return [];
35483593 }
35493594
35503595 const data = await loadWorldInfoDataloadWorldInfo(chatWorld);
35513596 const entries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(({ uid, ...rest }) => ({ uid, world: chatWorld, ...rest })) : [];
35523597
35533598 console.debug(`[WI] Chat lore has ${entries.length} entries`, [chatWorld]);
@@ -3663,7 +3708,7 @@ function parseDecorators(content) {
36633708 * @typedef {{ worldInfoBefore: string, worldInfoAfter: string, EMEntries: any[], WIDepthEntries: any[], allActivatedEntries: Set<any> }} WIActivated
36643709 * @returns {Promise<WIActivated>} The world info activated.
36653710 */
36663711export async function checkWorldInfo(chat, maxContext, isDryRun) {
36673712 const context = getContext();
36683713 const buffer = new WorldInfoBuffer(chat);
36693714
@@ -4233,7 +4278,7 @@ function convertAgnaiMemoryBook(inputObj) {
42334278
42344279 inputObj.entries.forEach((entry, index) => {
42354280 outputObj.entries[index] = {
42364281 ...newEntryTemplatenewWorldInfoEntryTemplate,
42374282 uid: index,
42384283 key: entry.keywords,
42394284 keysecondary: [],
@@ -4275,7 +4320,7 @@ function convertRisuLorebook(inputObj) {
42754320
42764321 inputObj.data.forEach((entry, index) => {
42774322 outputObj.entries[index] = {
42784323 ...newEntryTemplatenewWorldInfoEntryTemplate,
42794324 uid: index,
42804325 key: entry.key.split(',').map(x => x.trim()),
42814326 keysecondary: entry.secondkey ? entry.secondkey.split(',').map(x => x.trim()) : [],
@@ -4322,7 +4367,7 @@ function convertNovelLorebook(inputObj) {
43224367 const addMemo = displayName !== undefined && displayName.trim() !== '';
43234368
43244369 outputObj.entries[index] = {
43254370 ...newEntryTemplatenewWorldInfoEntryTemplate,
43264371 uid: index,
43274372 key: entry.keys,
43284373 keysecondary: [],
@@ -4369,7 +4414,7 @@ function convertCharacterBook(characterBook) {
43694414 }
43704415
43714416 result.entries[entry.id] = {
43724417 ...newEntryTemplatenewWorldInfoEntryTemplate,
43734418 uid: entry.id,
43744419 key: entry.keys,
43754420 keysecondary: entry.secondary_keys || [],
@@ -4499,7 +4544,7 @@ export async function importEmbeddedWorldInfo(skipPopup = false) {
44994544 setWorldInfoButtonClass(chid, true);
45004545}
45014546
45024547export function onWorldInfoChange(args, text) {
45034548 if (args !== '__notSlashCommand__') { // if it's a slash command
45044549 const silent = isTrueBoolean(args.silent);
45054550 if (text.trim() !== '') { // and args are provided
@@ -4650,7 +4695,7 @@ export async function importWorldInfo(file) {
46504695 });
46514696}
46524697
46534698export function assignLorebookToChat() {
46544699 const selectedName = chat_metadata[METADATA_KEY];
46554700 const template = $('#chat_world_template .chat_world').clone();
46564701