Merge pull request #2601 from SillyTavern/wi-blackbox-be-gone

WI blackbox begone (Export, document and refactor a bit of the WI API)
This commit is contained in:
Cohee 2024-08-03 11:57:52 +03:00 committed by GitHub
commit d5576cd7b7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 189 additions and 144 deletions

View File

@ -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<string,object>} */
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<string,object>}
* */
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<WIPromptResult>} 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<void>} 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<Object|null>} A promise that resolves to the loaded world information, or null if the request fails.
*/
export async function loadWorldInfo(name) {
if (!name) {
return;
}
@ -1635,14 +1639,18 @@ function getWIElement(name) {
}
/**
* Sorts the given data based on the selected sort option
*
* @param {any[]} data WI entries
* @param {object} [options={}] - Optional arguments
* @param {{sortField?: string, sortOrder?: string, sortRule?: string}} [options.customSort={}] - Custom sort options, instead of the chosen UI sort
* @returns {any[]} Sorted data
*/
function sortEntries(data) {
export function sortWorldInfoEntries(data, { customSort = null } = {}) {
const option = $('#world_info_sort_order').find(':selected');
const sortField = option.data('field');
const sortOrder = option.data('order');
const sortRule = option.data('rule');
const sortField = customSort?.sortField ?? option.data('field');
const sortOrder = customSort?.sortOrder ?? option.data('order');
const sortRule = customSort?.sortRule ?? option.data('rule');
const orderSign = sortOrder === 'asc' ? 1 : -1;
if (!data.length) return data;
@ -1801,7 +1809,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 +1927,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 +1962,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 +1970,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 +2033,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 +2044,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 +2095,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 +2117,13 @@ function setOriginalDataValue(data, uid, key, value) {
}
}
function deleteOriginalDataValue(data, uid) {
/**
* Deletes the original data entry corresponding to the given uid from the provided data object
*
* @param {object} data - The data object containing the original data entries
* @param {string} uid - The unique identifier of the data entry to be deleted
*/
export function deleteWIOriginalDataValue(data, uid) {
if (data.originalData && Array.isArray(data.originalData.entries)) {
const originalIndex = data.originalData.entries.findIndex(x => x.uid === uid);
@ -2120,7 +2144,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 +2248,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 +2334,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 +2372,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 +2416,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 +2465,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 +2527,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 +2541,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 +2576,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 +2606,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 +2655,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 +2669,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 +2682,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 +2706,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 +2719,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 +2732,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 +2745,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 +2765,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 +2793,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 +2860,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 +2905,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 +2965,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 +2977,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 +2989,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');
@ -2985,10 +3009,12 @@ function getWorldEntry(name, data, entry) {
// delete button
const deleteButton = template.find('.delete_entry_button');
deleteButton.data('uid', entry.uid);
deleteButton.on('click', async function () {
deleteButton.on('click', async function (e) {
e.stopPropagation();
const uid = $(this).data('uid');
deleteWorldInfoEntry(data, uid);
deleteOriginalDataValue(data, uid);
const deleted = await deleteWorldInfoEntry(data, uid);
if (!deleted) return;
deleteWIOriginalDataValue(data, uid);
await saveWorldInfo(name, data);
updateEditor(navigation_option.previous);
});
@ -3015,7 +3041,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 +3054,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 +3067,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 +3080,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 +3093,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 +3226,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;
}
@ -3225,17 +3251,22 @@ function duplicateWorldInfoEntry(data, uid) {
* Deletes a WI entry, with a user confirmation dialog
* @param {*[]} data - The data of the book
* @param {number} uid - The uid of the entry to copy in this book
* @param {object} [options={}] - Optional arguments
* @param {boolean} [options.silent=false] - Whether to prompt the user for deletion or just do it
* @returns {Promise<boolean>} Whether the entry deletion was successful
*/
function deleteWorldInfoEntry(data, uid) {
export async function deleteWorldInfoEntry(data, uid, { silent = false } = {}) {
if (!data || !('entries' in data)) {
return;
}
if (!confirm(`Delete the entry with UID: ${uid}? This action is irreversible!`)) {
throw new Error('User cancelled deletion');
const confirmation = silent || await Popup.show.confirm(`Delete the entry with UID: ${uid}?`, 'This action is irreversible!');
if (!confirmation) {
return false;
}
delete data.entries[uid];
return true;
}
/**
@ -3245,7 +3276,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 +3309,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 +3319,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 +3327,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 +3345,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<void>} A promise that resolves when the world info is saved
*/
export async function saveWorldInfo(name, data, immediately = false) {
if (!name || !data) {
return;
}
@ -3371,7 +3416,7 @@ async function renameWorldInfo(name, data) {
* @param {string} worldInfoName - The name of the world info to delete
* @returns {Promise<boolean>} 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 +3451,7 @@ async function deleteWorldInfo(worldInfoName) {
return true;
}
function getFreeWorldEntryUid(data) {
export function getFreeWorldEntryUid(data) {
if (!data || !('entries' in data)) {
return null;
}
@ -3422,7 +3467,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 +3489,7 @@ function getFreeWorldName() {
* @param {boolean} [options.interactive=false] - Whether to show a confirmation dialog when overwriting an existing world
* @returns {Promise<boolean>} - 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 +3550,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 +3570,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 +3592,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 +3708,7 @@ function parseDecorators(content) {
* @typedef {{ worldInfoBefore: string, worldInfoAfter: string, EMEntries: any[], WIDepthEntries: any[], allActivatedEntries: Set<any> }} WIActivated
* @returns {Promise<WIActivated>} 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 +4278,7 @@ function convertAgnaiMemoryBook(inputObj) {
inputObj.entries.forEach((entry, index) => {
outputObj.entries[index] = {
...newEntryTemplate,
...newWorldInfoEntryTemplate,
uid: index,
key: entry.keywords,
keysecondary: [],
@ -4275,7 +4320,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 +4367,7 @@ function convertNovelLorebook(inputObj) {
const addMemo = displayName !== undefined && displayName.trim() !== '';
outputObj.entries[index] = {
...newEntryTemplate,
...newWorldInfoEntryTemplate,
uid: index,
key: entry.keys,
keysecondary: [],
@ -4369,7 +4414,7 @@ function convertCharacterBook(characterBook) {
}
result.entries[entry.id] = {
...newEntryTemplate,
...newWorldInfoEntryTemplate,
uid: entry.id,
key: entry.keys,
keysecondary: entry.secondary_keys || [],
@ -4499,7 +4544,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 +4695,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();